1use std::borrow::Cow;
16use std::collections::BTreeSet;
17use std::collections::HashMap;
18use std::env;
19use std::env::split_paths;
20use std::fmt;
21use std::path::Path;
22use std::path::PathBuf;
23use std::process::Command;
24use std::sync::LazyLock;
25
26use etcetera::BaseStrategy as _;
27use itertools::Itertools as _;
28use jj_lib::config::ConfigFile;
29use jj_lib::config::ConfigGetError;
30use jj_lib::config::ConfigLayer;
31use jj_lib::config::ConfigLoadError;
32use jj_lib::config::ConfigMigrationRule;
33use jj_lib::config::ConfigNamePathBuf;
34use jj_lib::config::ConfigResolutionContext;
35use jj_lib::config::ConfigSource;
36use jj_lib::config::ConfigValue;
37use jj_lib::config::StackedConfig;
38use regex::Captures;
39use regex::Regex;
40use serde::Serialize as _;
41use tracing::instrument;
42
43use crate::command_error::CommandError;
44use crate::command_error::config_error;
45use crate::command_error::config_error_with_message;
46
47pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");
49
50pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> {
53 match value_str.parse() {
54 Ok(value) => Ok(value),
55 Err(_) if is_bare_string(value_str) => Ok(value_str.into()),
56 Err(err) => Err(err),
57 }
58}
59
60fn is_bare_string(value_str: &str) -> bool {
61 let trimmed = value_str.trim_ascii().as_bytes();
64 if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {
65 !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')
67 } else {
68 true }
70}
71
72pub fn to_serializable_value(value: ConfigValue) -> toml::Value {
75 match value {
76 ConfigValue::String(v) => toml::Value::String(v.into_value()),
77 ConfigValue::Integer(v) => toml::Value::Integer(v.into_value()),
78 ConfigValue::Float(v) => toml::Value::Float(v.into_value()),
79 ConfigValue::Boolean(v) => toml::Value::Boolean(v.into_value()),
80 ConfigValue::Datetime(v) => toml::Value::Datetime(v.into_value()),
81 ConfigValue::Array(array) => {
82 let array = array.into_iter().map(to_serializable_value).collect();
83 toml::Value::Array(array)
84 }
85 ConfigValue::InlineTable(table) => {
86 let table = table
87 .into_iter()
88 .map(|(k, v)| (k, to_serializable_value(v)))
89 .collect();
90 toml::Value::Table(table)
91 }
92 }
93}
94
95#[derive(Clone, Debug, serde::Serialize)]
97pub struct AnnotatedValue {
98 #[serde(serialize_with = "serialize_name")]
100 pub name: ConfigNamePathBuf,
101 #[serde(serialize_with = "serialize_value")]
103 pub value: ConfigValue,
104 #[serde(serialize_with = "serialize_source")]
106 pub source: ConfigSource,
107 pub path: Option<PathBuf>,
109 pub is_overridden: bool,
111}
112
113fn serialize_name<S>(name: &ConfigNamePathBuf, serializer: S) -> Result<S::Ok, S::Error>
114where
115 S: serde::Serializer,
116{
117 name.to_string().serialize(serializer)
118}
119
120fn serialize_value<S>(value: &ConfigValue, serializer: S) -> Result<S::Ok, S::Error>
121where
122 S: serde::Serializer,
123{
124 to_serializable_value(value.clone()).serialize(serializer)
125}
126
127fn serialize_source<S>(source: &ConfigSource, serializer: S) -> Result<S::Ok, S::Error>
128where
129 S: serde::Serializer,
130{
131 source.to_string().serialize(serializer)
132}
133
134pub fn resolved_config_values(
137 stacked_config: &StackedConfig,
138 filter_prefix: &ConfigNamePathBuf,
139) -> Vec<AnnotatedValue> {
140 let mut config_vals = vec![];
143 let mut upper_value_names = BTreeSet::new();
144 for layer in stacked_config.layers().iter().rev() {
145 let top_item = match layer.look_up_item(filter_prefix) {
146 Ok(Some(item)) => item,
147 Ok(None) => continue, Err(_) => {
149 upper_value_names.insert(filter_prefix.clone());
151 continue;
152 }
153 };
154 let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];
155 while let Some((name, item, is_parent_overridden)) = config_stack.pop() {
156 if let Some(table) = item.as_table_like() {
159 let is_overridden = is_parent_overridden || upper_value_names.contains(&name);
161 for (k, v) in table.iter() {
162 let mut sub_name = name.clone();
163 sub_name.push(k);
164 config_stack.push((sub_name, v, is_overridden)); }
166 } else {
167 let maybe_child = upper_value_names
169 .range(&name..)
170 .next()
171 .filter(|next| next.starts_with(&name));
172 let is_overridden = is_parent_overridden || maybe_child.is_some();
173 if maybe_child != Some(&name) {
174 upper_value_names.insert(name.clone());
175 }
176 let value = item
177 .clone()
178 .into_value()
179 .expect("Item::None should not exist in table");
180 config_vals.push(AnnotatedValue {
181 name,
182 value,
183 source: layer.source,
184 path: layer.path.clone(),
185 is_overridden,
186 });
187 }
188 }
189 }
190 config_vals.reverse();
191 config_vals
192}
193
194#[derive(Clone, Debug)]
199pub struct RawConfig(StackedConfig);
200
201impl AsRef<StackedConfig> for RawConfig {
202 fn as_ref(&self) -> &StackedConfig {
203 &self.0
204 }
205}
206
207impl AsMut<StackedConfig> for RawConfig {
208 fn as_mut(&mut self) -> &mut StackedConfig {
209 &mut self.0
210 }
211}
212
213#[derive(Clone, Debug)]
214enum ConfigPathState {
215 New,
216 Exists,
217}
218
219#[derive(Clone, Debug)]
225struct ConfigPath {
226 path: PathBuf,
227 state: ConfigPathState,
228}
229
230impl ConfigPath {
231 fn new(path: PathBuf) -> Self {
232 use ConfigPathState::*;
233 Self {
234 state: if path.exists() { Exists } else { New },
235 path,
236 }
237 }
238
239 fn as_path(&self) -> &Path {
240 &self.path
241 }
242 fn exists(&self) -> bool {
243 match self.state {
244 ConfigPathState::Exists => true,
245 ConfigPathState::New => false,
246 }
247 }
248}
249
250fn create_dir_all(path: &Path) -> std::io::Result<()> {
253 let mut dir = std::fs::DirBuilder::new();
254 dir.recursive(true);
255 #[cfg(unix)]
256 {
257 use std::os::unix::fs::DirBuilderExt as _;
258 dir.mode(0o700);
259 }
260 dir.create(path)
261}
262
263#[derive(Clone, Default, Debug)]
265struct UnresolvedConfigEnv {
266 config_dir: Option<PathBuf>,
267 home_dir: Option<PathBuf>,
268 jj_config: Option<String>,
269}
270
271impl UnresolvedConfigEnv {
272 fn resolve(self) -> Vec<ConfigPath> {
273 if let Some(paths) = self.jj_config {
274 return split_paths(&paths)
275 .filter(|path| !path.as_os_str().is_empty())
276 .map(ConfigPath::new)
277 .collect();
278 }
279
280 let mut paths = vec![];
281 let home_config_path = self.home_dir.map(|mut home_dir| {
282 home_dir.push(".jjconfig.toml");
283 ConfigPath::new(home_dir)
284 });
285 let platform_config_path = self.config_dir.clone().map(|mut config_dir| {
286 config_dir.push("jj");
287 config_dir.push("config.toml");
288 ConfigPath::new(config_dir)
289 });
290 let platform_config_dir = self.config_dir.map(|mut config_dir| {
291 config_dir.push("jj");
292 config_dir.push("conf.d");
293 ConfigPath::new(config_dir)
294 });
295
296 if let Some(path) = home_config_path
297 && (path.exists() || platform_config_path.is_none())
298 {
299 paths.push(path);
300 }
301
302 if let Some(path) = platform_config_path {
305 paths.push(path);
306 }
307
308 if let Some(path) = platform_config_dir
309 && path.exists()
310 {
311 paths.push(path);
312 }
313
314 paths
315 }
316}
317
318#[derive(Clone, Debug)]
319pub struct ConfigEnv {
320 home_dir: Option<PathBuf>,
321 repo_path: Option<PathBuf>,
322 workspace_path: Option<PathBuf>,
323 user_config_paths: Vec<ConfigPath>,
324 repo_config_path: Option<ConfigPath>,
325 workspace_config_path: Option<ConfigPath>,
326 command: Option<String>,
327 hostname: Option<String>,
328}
329
330impl ConfigEnv {
331 pub fn from_environment() -> Self {
333 let config_dir = etcetera::choose_base_strategy()
334 .ok()
335 .map(|s| s.config_dir());
336
337 let home_dir = etcetera::home_dir()
340 .ok()
341 .map(|d| dunce::canonicalize(&d).unwrap_or(d));
342
343 let env = UnresolvedConfigEnv {
344 config_dir,
345 home_dir: home_dir.clone(),
346 jj_config: env::var("JJ_CONFIG").ok(),
347 };
348 Self {
349 home_dir,
350 repo_path: None,
351 workspace_path: None,
352 user_config_paths: env.resolve(),
353 repo_config_path: None,
354 workspace_config_path: None,
355 command: None,
356 hostname: whoami::hostname().ok(),
357 }
358 }
359
360 pub fn set_command_name(&mut self, command: String) {
361 self.command = Some(command);
362 }
363
364 pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
366 self.user_config_paths.iter().map(ConfigPath::as_path)
367 }
368
369 pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
372 self.user_config_paths
373 .iter()
374 .filter(|p| p.exists())
375 .map(ConfigPath::as_path)
376 }
377
378 pub fn user_config_files(
385 &self,
386 config: &RawConfig,
387 ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
388 config_files_for(config, ConfigSource::User, || self.new_user_config_file())
389 }
390
391 fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
392 self.user_config_paths()
393 .next()
394 .map(|path| {
395 if let Some(dir) = path.parent() {
398 create_dir_all(dir).ok();
399 }
400 ConfigFile::load_or_empty(ConfigSource::User, path)
403 })
404 .transpose()
405 }
406
407 #[instrument]
410 pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
411 config.as_mut().remove_layers(ConfigSource::User);
412 for path in self.existing_user_config_paths() {
413 if path.is_dir() {
414 config.as_mut().load_dir(ConfigSource::User, path)?;
415 } else {
416 config.as_mut().load_file(ConfigSource::User, path)?;
417 }
418 }
419 Ok(())
420 }
421
422 pub fn reset_repo_path(&mut self, path: &Path) {
425 self.repo_path = Some(path.to_owned());
426 self.repo_config_path = Some(ConfigPath::new(path.join("config.toml")));
427 }
428
429 pub fn repo_config_path(&self) -> Option<&Path> {
431 self.repo_config_path.as_ref().map(|p| p.as_path())
432 }
433
434 fn existing_repo_config_path(&self) -> Option<&Path> {
436 match self.repo_config_path {
437 Some(ref path) if path.exists() => Some(path.as_path()),
438 _ => None,
439 }
440 }
441
442 pub fn repo_config_files(
449 &self,
450 config: &RawConfig,
451 ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
452 config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file())
453 }
454
455 fn new_repo_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
456 self.repo_config_path()
457 .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
460 .transpose()
461 }
462
463 #[instrument]
466 pub fn reload_repo_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
467 config.as_mut().remove_layers(ConfigSource::Repo);
468 if let Some(path) = self.existing_repo_config_path() {
469 config.as_mut().load_file(ConfigSource::Repo, path)?;
470 }
471 Ok(())
472 }
473
474 pub fn reset_workspace_path(&mut self, path: &Path) {
477 self.workspace_path = Some(path.to_owned());
478 self.workspace_config_path = Some(ConfigPath::new(
479 path.join(".jj").join("workspace-config.toml"),
480 ));
481 }
482
483 pub fn workspace_config_path(&self) -> Option<&Path> {
485 self.workspace_config_path.as_ref().map(|p| p.as_path())
486 }
487
488 fn existing_workspace_config_path(&self) -> Option<&Path> {
490 match self.workspace_config_path {
491 Some(ref path) if path.exists() => Some(path.as_path()),
492 _ => None,
493 }
494 }
495
496 pub fn workspace_config_files(
503 &self,
504 config: &RawConfig,
505 ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
506 config_files_for(config, ConfigSource::Workspace, || {
507 self.new_workspace_config_file()
508 })
509 }
510
511 fn new_workspace_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
512 self.workspace_config_path()
513 .map(|path| ConfigFile::load_or_empty(ConfigSource::Workspace, path))
514 .transpose()
515 }
516
517 #[instrument]
520 pub fn reload_workspace_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
521 config.as_mut().remove_layers(ConfigSource::Workspace);
522 if let Some(path) = self.existing_workspace_config_path() {
523 config.as_mut().load_file(ConfigSource::Workspace, path)?;
524 }
525 Ok(())
526 }
527
528 pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
531 let context = ConfigResolutionContext {
532 home_dir: self.home_dir.as_deref(),
533 repo_path: self.repo_path.as_deref(),
534 workspace_path: self.workspace_path.as_deref(),
535 command: self.command.as_deref(),
536 hostname: self.hostname.as_deref().unwrap_or(""),
537 };
538 jj_lib::config::resolve(config.as_ref(), &context)
539 }
540}
541
542fn config_files_for(
543 config: &RawConfig,
544 source: ConfigSource,
545 new_file: impl FnOnce() -> Result<Option<ConfigFile>, ConfigLoadError>,
546) -> Result<Vec<ConfigFile>, ConfigLoadError> {
547 let mut files = config
548 .as_ref()
549 .layers_for(source)
550 .iter()
551 .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
552 .collect_vec();
553 if files.is_empty() {
554 files.extend(new_file()?);
555 }
556 Ok(files)
557}
558
559pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
573 let mut config = StackedConfig::with_defaults();
574 config.extend_layers(default_layers);
575 config.add_layer(env_base_layer());
576 config.add_layer(env_overrides_layer());
577 RawConfig(config)
578}
579
580const OP_HOSTNAME: &str = "operation.hostname";
581const OP_USERNAME: &str = "operation.username";
582
583fn env_base_layer() -> ConfigLayer {
585 let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
586 if let Ok(value) =
587 whoami::hostname().inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
588 {
589 layer.set_value(OP_HOSTNAME, value).unwrap();
590 }
591 if let Ok(value) =
592 whoami::username().inspect_err(|err| tracing::warn!(?err, "failed to get username"))
593 {
594 layer.set_value(OP_USERNAME, value).unwrap();
595 } else if let Ok(value) = env::var("USER") {
596 layer.set_value(OP_USERNAME, value).unwrap();
599 }
600 if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
601 layer.set_value("ui.color", "never").unwrap();
604 }
605 if let Ok(value) = env::var("VISUAL") {
606 layer.set_value("ui.editor", value).unwrap();
607 } else if let Ok(value) = env::var("EDITOR") {
608 layer.set_value("ui.editor", value).unwrap();
609 }
610 layer
613}
614
615pub fn default_config_layers() -> Vec<ConfigLayer> {
616 let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
619 let mut layers = vec![
620 parse(include_str!("config/colors.toml")),
621 parse(include_str!("config/hints.toml")),
622 parse(include_str!("config/merge_tools.toml")),
623 parse(include_str!("config/misc.toml")),
624 parse(include_str!("config/revsets.toml")),
625 parse(include_str!("config/templates.toml")),
626 ];
627 if cfg!(unix) {
628 layers.push(parse(include_str!("config/unix.toml")));
629 }
630 if cfg!(windows) {
631 layers.push(parse(include_str!("config/windows.toml")));
632 }
633 layers
634}
635
636fn env_overrides_layer() -> ConfigLayer {
638 let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
639 if let Ok(value) = env::var("JJ_USER") {
640 layer.set_value("user.name", value).unwrap();
641 }
642 if let Ok(value) = env::var("JJ_EMAIL") {
643 layer.set_value("user.email", value).unwrap();
644 }
645 if let Ok(value) = env::var("JJ_TIMESTAMP") {
646 layer.set_value("debug.commit-timestamp", value).unwrap();
647 }
648 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
649 layer.set_value("debug.randomness-seed", value).unwrap();
650 }
651 if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
652 layer.set_value("debug.operation-timestamp", value).unwrap();
653 }
654 if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
655 layer.set_value(OP_HOSTNAME, value).unwrap();
656 }
657 if let Ok(value) = env::var("JJ_OP_USERNAME") {
658 layer.set_value(OP_USERNAME, value).unwrap();
659 }
660 if let Ok(value) = env::var("JJ_EDITOR") {
661 layer.set_value("ui.editor", value).unwrap();
662 }
663 layer
664}
665
666#[derive(Clone, Copy, Debug, Eq, PartialEq)]
668pub enum ConfigArgKind {
669 Item,
671 File,
673}
674
675pub fn parse_config_args(
677 toml_strs: &[(ConfigArgKind, &str)],
678) -> Result<Vec<ConfigLayer>, CommandError> {
679 let source = ConfigSource::CommandArg;
680 let mut layers = Vec::new();
681 for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
682 match kind {
683 ConfigArgKind::Item => {
684 let mut layer = ConfigLayer::empty(source);
685 for (_, item) in chunk {
686 let (name, value) = parse_config_arg_item(item)?;
687 layer.set_value(name, value).map_err(|err| {
690 config_error_with_message("--config argument cannot be set", err)
691 })?;
692 }
693 layers.push(layer);
694 }
695 ConfigArgKind::File => {
696 for (_, path) in chunk {
697 layers.push(ConfigLayer::load_from_file(source, path.into())?);
698 }
699 }
700 }
701 }
702 Ok(layers)
703}
704
705fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
707 let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
709 let Some((name, value_str)) = split_candidates
710 .map(|p| (&item_str[..p], &item_str[p + 1..]))
711 .map(|(name, value)| name.parse().map(|name| (name, value)))
712 .find_or_last(Result::is_ok)
713 .transpose()
714 .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
715 else {
716 return Err(config_error("--config must be specified as NAME=VALUE"));
717 };
718 let value = parse_value_or_bare_string(value_str)
719 .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
720 Ok((name, value))
721}
722
723pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
725 vec![
726 ConfigMigrationRule::rename_value("core.fsmonitor", "fsmonitor.backend"),
728 ConfigMigrationRule::rename_value(
730 "core.watchman.register-snapshot-trigger",
731 "fsmonitor.watchman.register-snapshot-trigger",
732 ),
733 ConfigMigrationRule::custom(
735 |layer| {
736 let Ok(Some(val)) = layer.look_up_item("git.auto-local-bookmark") else {
737 return false;
738 };
739 val.as_bool().is_some_and(|b| b)
740 },
741 |_| {
742 Ok("`git.auto-local-bookmark` is deprecated; use \
743 `remotes.<name>.auto-track-bookmarks` instead.
744Example: jj config set --user remotes.origin.auto-track-bookmarks '*'
745For details, see: https://docs.jj-vcs.dev/latest/config/#automatic-tracking-of-bookmarks"
746 .into())
747 },
748 ),
749 ConfigMigrationRule::custom(
751 |layer| {
752 let Ok(Some(val)) = layer.look_up_item("git.push-new-bookmarks") else {
753 return false;
754 };
755 val.as_bool().is_some_and(|b| b)
756 },
757 |_| {
758 Ok("`git.push-new-bookmarks` is deprecated; use \
759 `remotes.<name>.auto-track-bookmarks` instead.
760Example: jj config set --user remotes.origin.auto-track-bookmarks '*'
761For details, see: https://docs.jj-vcs.dev/latest/config/#automatic-tracking-of-bookmarks"
762 .into())
763 },
764 ),
765 ]
766}
767
768#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
770#[serde(untagged)]
771pub enum CommandNameAndArgs {
772 String(String),
773 Vec(NonEmptyCommandArgsVec),
774 Structured {
775 env: HashMap<String, String>,
776 command: NonEmptyCommandArgsVec,
777 },
778}
779
780impl CommandNameAndArgs {
781 pub fn split_name(&self) -> Cow<'_, str> {
783 let (name, _) = self.split_name_and_args();
784 name
785 }
786
787 pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
791 match self {
792 Self::String(s) => {
793 let mut args = s.split(' ').map(|s| s.to_owned());
795 (args.next().unwrap().into(), args.collect())
796 }
797 Self::Vec(NonEmptyCommandArgsVec(a)) => (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])),
798 Self::Structured {
799 env: _,
800 command: cmd,
801 } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
802 }
803 }
804
805 pub fn as_str(&self) -> Option<&str> {
810 match self {
811 Self::String(s) => Some(s),
812 Self::Vec(_) | Self::Structured { .. } => None,
813 }
814 }
815
816 pub fn to_command(&self) -> Command {
818 let empty: HashMap<&str, &str> = HashMap::new();
819 self.to_command_with_variables(&empty)
820 }
821
822 pub fn to_command_with_variables<V: AsRef<str>>(
825 &self,
826 variables: &HashMap<&str, V>,
827 ) -> Command {
828 let (name, args) = self.split_name_and_args();
829 let mut cmd = Command::new(interpolate_variables_single(name.as_ref(), variables));
830 if let Self::Structured { env, .. } = self {
831 cmd.envs(env);
832 }
833 cmd.args(interpolate_variables(&args, variables));
834 cmd
835 }
836}
837
838impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
839 fn from(s: &T) -> Self {
840 Self::String(s.as_ref().to_owned())
841 }
842}
843
844impl fmt::Display for CommandNameAndArgs {
845 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
846 match self {
847 Self::String(s) => write!(f, "{s}"),
848 Self::Vec(a) => write!(f, "{}", a.0.join(" ")),
850 Self::Structured { env, command } => {
851 for (k, v) in env {
852 write!(f, "{k}={v} ")?;
853 }
854 write!(f, "{}", command.0.join(" "))
855 }
856 }
857 }
858}
859
860static VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
862
863pub fn interpolate_variables<V: AsRef<str>>(
864 args: &[String],
865 variables: &HashMap<&str, V>,
866) -> Vec<String> {
867 args.iter()
868 .map(|arg| interpolate_variables_single(arg, variables))
869 .collect()
870}
871
872fn interpolate_variables_single<V: AsRef<str>>(arg: &str, variables: &HashMap<&str, V>) -> String {
873 VARIABLE_REGEX
874 .replace_all(arg, |caps: &Captures| {
875 let name = &caps[1];
876 if let Some(subst) = variables.get(name) {
877 subst.as_ref().to_owned()
878 } else {
879 caps[0].to_owned()
880 }
881 })
882 .into_owned()
883}
884
885pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
887 let regex = &*VARIABLE_REGEX;
888 args.iter()
889 .flat_map(|arg| regex.find_iter(arg))
890 .map(|single_match| {
891 let s = single_match.as_str();
892 &s[1..]
893 })
894}
895
896#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
899#[serde(try_from = "Vec<String>")]
900pub struct NonEmptyCommandArgsVec(Vec<String>);
901
902impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
903 type Error = &'static str;
904
905 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
906 if args.is_empty() {
907 Err("command arguments should not be empty")
908 } else {
909 Ok(Self(args))
910 }
911 }
912}
913
914#[cfg(test)]
915mod tests {
916 use std::env::join_paths;
917 use std::fmt::Write as _;
918
919 use indoc::indoc;
920 use maplit::hashmap;
921 use test_case::test_case;
922
923 use super::*;
924
925 fn insta_settings() -> insta::Settings {
926 let mut settings = insta::Settings::clone_current();
927 settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
929 settings
930 }
931
932 #[test]
933 fn test_parse_value_or_bare_string() {
934 let parse = |s: &str| parse_value_or_bare_string(s);
935
936 assert_eq!(parse("true").unwrap().as_bool(), Some(true));
938 assert_eq!(parse("42").unwrap().as_integer(), Some(42));
939 assert_eq!(parse("-1").unwrap().as_integer(), Some(-1));
940 assert_eq!(parse("'a'").unwrap().as_str(), Some("a"));
941 assert!(parse("[]").unwrap().is_array());
942 assert!(parse("{ a = 'b' }").unwrap().is_inline_table());
943
944 assert_eq!(parse("").unwrap().as_str(), Some(""));
946 assert_eq!(parse("John Doe").unwrap().as_str(), Some("John Doe"));
947 assert_eq!(parse("Doe, John").unwrap().as_str(), Some("Doe, John"));
948 assert_eq!(parse("It's okay").unwrap().as_str(), Some("It's okay"));
949 assert_eq!(
950 parse("<foo+bar@example.org>").unwrap().as_str(),
951 Some("<foo+bar@example.org>")
952 );
953 assert_eq!(parse("#ff00aa").unwrap().as_str(), Some("#ff00aa"));
954 assert_eq!(parse("all()").unwrap().as_str(), Some("all()"));
955 assert_eq!(parse("glob:*.*").unwrap().as_str(), Some("glob:*.*"));
956 assert_eq!(parse("柔術").unwrap().as_str(), Some("柔術"));
957
958 assert!(parse("'foo").is_err());
960 assert!(parse(r#" bar" "#).is_err());
961 assert!(parse("[0 1]").is_err());
962 assert!(parse("{ x = }").is_err());
963 assert!(parse("\n { x").is_err());
964 assert!(parse(" x ] ").is_err());
965 assert!(parse("[table]\nkey = 'value'").is_err());
966 }
967
968 #[test]
969 fn test_parse_config_arg_item() {
970 assert!(parse_config_arg_item("").is_err());
971 assert!(parse_config_arg_item("a").is_err());
972 assert!(parse_config_arg_item("=").is_err());
973 assert!(parse_config_arg_item("a = 'b'").is_err());
976
977 let (name, value) = parse_config_arg_item("a=b").unwrap();
978 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
979 assert_eq!(value.as_str(), Some("b"));
980
981 let (name, value) = parse_config_arg_item("a=").unwrap();
982 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
983 assert_eq!(value.as_str(), Some(""));
984
985 let (name, value) = parse_config_arg_item("a= ").unwrap();
986 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
987 assert_eq!(value.as_str(), Some(" "));
988
989 let (name, value) = parse_config_arg_item("a=b=c").unwrap();
991 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
992 assert_eq!(value.as_str(), Some("b=c"));
993
994 let (name, value) = parse_config_arg_item("a.b=true").unwrap();
995 assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
996 assert_eq!(value.as_bool(), Some(true));
997
998 let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
999 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1000 assert_eq!(value.as_str(), Some("b=c"));
1001
1002 let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
1003 assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
1004 assert_eq!(value.as_str(), Some("c"));
1005
1006 let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
1007 assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
1008 assert!(value.is_inline_table());
1009 assert_eq!(value.to_string(), "{d = 'e=f'}");
1010 }
1011
1012 #[test]
1013 fn test_command_args() {
1014 let mut config = StackedConfig::empty();
1015 config.add_layer(
1016 ConfigLayer::parse(
1017 ConfigSource::User,
1018 indoc! {"
1019 empty_array = []
1020 empty_string = ''
1021 array = ['emacs', '-nw']
1022 string = 'emacs -nw'
1023 structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1024 structured.command = ['emacs', '-nw']
1025 "},
1026 )
1027 .unwrap(),
1028 );
1029
1030 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1031
1032 let command_args: CommandNameAndArgs = config.get("empty_string").unwrap();
1033 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1034 let (name, args) = command_args.split_name_and_args();
1035 assert_eq!(name, "");
1036 assert!(args.is_empty());
1037
1038 let command_args: CommandNameAndArgs = config.get("array").unwrap();
1039 assert_eq!(
1040 command_args,
1041 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1042 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1043 ))
1044 );
1045 let (name, args) = command_args.split_name_and_args();
1046 assert_eq!(name, "emacs");
1047 assert_eq!(args, ["-nw"].as_ref());
1048
1049 let command_args: CommandNameAndArgs = config.get("string").unwrap();
1050 assert_eq!(
1051 command_args,
1052 CommandNameAndArgs::String("emacs -nw".to_owned())
1053 );
1054 let (name, args) = command_args.split_name_and_args();
1055 assert_eq!(name, "emacs");
1056 assert_eq!(args, ["-nw"].as_ref());
1057
1058 let command_args: CommandNameAndArgs = config.get("structured").unwrap();
1059 assert_eq!(
1060 command_args,
1061 CommandNameAndArgs::Structured {
1062 env: hashmap! {
1063 "KEY1".to_string() => "value1".to_string(),
1064 "KEY2".to_string() => "value2".to_string(),
1065 },
1066 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1067 }
1068 );
1069 let (name, args) = command_args.split_name_and_args();
1070 assert_eq!(name, "emacs");
1071 assert_eq!(args, ["-nw"].as_ref());
1072 }
1073
1074 #[test]
1075 fn test_resolved_config_values_empty() {
1076 let config = StackedConfig::empty();
1077 assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
1078 }
1079
1080 #[test]
1081 fn test_resolved_config_values_single_key() {
1082 let settings = insta_settings();
1083 let _guard = settings.bind_to_scope();
1084 let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
1085 env_base_layer
1086 .set_value("user.name", "base-user-name")
1087 .unwrap();
1088 env_base_layer
1089 .set_value("user.email", "base@user.email")
1090 .unwrap();
1091 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1092 repo_layer
1093 .set_value("user.email", "repo@user.email")
1094 .unwrap();
1095 let mut config = StackedConfig::empty();
1096 config.add_layer(env_base_layer);
1097 config.add_layer(repo_layer);
1098 insta::assert_debug_snapshot!(
1100 resolved_config_values(&config, &ConfigNamePathBuf::root()),
1101 @r#"
1102 [
1103 AnnotatedValue {
1104 name: ConfigNamePathBuf(
1105 [
1106 Key {
1107 key: "user",
1108 repr: None,
1109 leaf_decor: Decor { .. },
1110 dotted_decor: Decor { .. },
1111 },
1112 Key {
1113 key: "name",
1114 repr: None,
1115 leaf_decor: Decor { .. },
1116 dotted_decor: Decor { .. },
1117 },
1118 ],
1119 ),
1120 value: String(
1121 Formatted {
1122 value: "base-user-name",
1123 repr: "default",
1124 decor: Decor { .. },
1125 },
1126 ),
1127 source: EnvBase,
1128 path: None,
1129 is_overridden: false,
1130 },
1131 AnnotatedValue {
1132 name: ConfigNamePathBuf(
1133 [
1134 Key {
1135 key: "user",
1136 repr: None,
1137 leaf_decor: Decor { .. },
1138 dotted_decor: Decor { .. },
1139 },
1140 Key {
1141 key: "email",
1142 repr: None,
1143 leaf_decor: Decor { .. },
1144 dotted_decor: Decor { .. },
1145 },
1146 ],
1147 ),
1148 value: String(
1149 Formatted {
1150 value: "base@user.email",
1151 repr: "default",
1152 decor: Decor { .. },
1153 },
1154 ),
1155 source: EnvBase,
1156 path: None,
1157 is_overridden: true,
1158 },
1159 AnnotatedValue {
1160 name: ConfigNamePathBuf(
1161 [
1162 Key {
1163 key: "user",
1164 repr: None,
1165 leaf_decor: Decor { .. },
1166 dotted_decor: Decor { .. },
1167 },
1168 Key {
1169 key: "email",
1170 repr: None,
1171 leaf_decor: Decor { .. },
1172 dotted_decor: Decor { .. },
1173 },
1174 ],
1175 ),
1176 value: String(
1177 Formatted {
1178 value: "repo@user.email",
1179 repr: "default",
1180 decor: Decor { .. },
1181 },
1182 ),
1183 source: Repo,
1184 path: None,
1185 is_overridden: false,
1186 },
1187 ]
1188 "#
1189 );
1190 }
1191
1192 #[test]
1193 fn test_resolved_config_values_filter_path() {
1194 let settings = insta_settings();
1195 let _guard = settings.bind_to_scope();
1196 let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1197 user_layer.set_value("test-table1.foo", "user-FOO").unwrap();
1198 user_layer.set_value("test-table2.bar", "user-BAR").unwrap();
1199 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1200 repo_layer.set_value("test-table1.bar", "repo-BAR").unwrap();
1201 let mut config = StackedConfig::empty();
1202 config.add_layer(user_layer);
1203 config.add_layer(repo_layer);
1204 insta::assert_debug_snapshot!(
1205 resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1206 @r#"
1207 [
1208 AnnotatedValue {
1209 name: ConfigNamePathBuf(
1210 [
1211 Key {
1212 key: "test-table1",
1213 repr: None,
1214 leaf_decor: Decor { .. },
1215 dotted_decor: Decor { .. },
1216 },
1217 Key {
1218 key: "foo",
1219 repr: None,
1220 leaf_decor: Decor { .. },
1221 dotted_decor: Decor { .. },
1222 },
1223 ],
1224 ),
1225 value: String(
1226 Formatted {
1227 value: "user-FOO",
1228 repr: "default",
1229 decor: Decor { .. },
1230 },
1231 ),
1232 source: User,
1233 path: None,
1234 is_overridden: false,
1235 },
1236 AnnotatedValue {
1237 name: ConfigNamePathBuf(
1238 [
1239 Key {
1240 key: "test-table1",
1241 repr: None,
1242 leaf_decor: Decor { .. },
1243 dotted_decor: Decor { .. },
1244 },
1245 Key {
1246 key: "bar",
1247 repr: None,
1248 leaf_decor: Decor { .. },
1249 dotted_decor: Decor { .. },
1250 },
1251 ],
1252 ),
1253 value: String(
1254 Formatted {
1255 value: "repo-BAR",
1256 repr: "default",
1257 decor: Decor { .. },
1258 },
1259 ),
1260 source: Repo,
1261 path: None,
1262 is_overridden: false,
1263 },
1264 ]
1265 "#
1266 );
1267 }
1268
1269 #[test]
1270 fn test_resolved_config_values_overridden() {
1271 let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1272 let mut config = StackedConfig::empty();
1273 config.extend_layers(layers.iter().copied().cloned());
1274 let prefix = if prefix.is_empty() {
1275 ConfigNamePathBuf::root()
1276 } else {
1277 prefix.parse().unwrap()
1278 };
1279 let mut output = String::new();
1280 for annotated in resolved_config_values(&config, &prefix) {
1281 let AnnotatedValue { name, value, .. } = &annotated;
1282 let sigil = if annotated.is_overridden { '!' } else { ' ' };
1283 writeln!(output, "{sigil}{name} = {value}").unwrap();
1284 }
1285 output
1286 };
1287
1288 let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1289 layer0.set_value("a.b.e", "0.0").unwrap();
1290 layer0.set_value("a.b.c.f", "0.1").unwrap();
1291 layer0.set_value("a.b.d", "0.2").unwrap();
1292 let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1293 layer1.set_value("a.b", "1.0").unwrap();
1294 layer1.set_value("a.c", "1.1").unwrap();
1295 let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1296 layer2.set_value("a.b.g", "2.0").unwrap();
1297 layer2.set_value("a.b.d", "2.1").unwrap();
1298
1299 let layers = [&layer0, &layer1];
1301 insta::assert_snapshot!(list(&layers, ""), @r#"
1302 !a.b.e = "0.0"
1303 !a.b.c.f = "0.1"
1304 !a.b.d = "0.2"
1305 a.b = "1.0"
1306 a.c = "1.1"
1307 "#);
1308 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1309 !a.b.e = "0.0"
1310 !a.b.c.f = "0.1"
1311 !a.b.d = "0.2"
1312 a.b = "1.0"
1313 "#);
1314 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1315 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1316
1317 let layers = [&layer1, &layer2];
1319 insta::assert_snapshot!(list(&layers, ""), @r#"
1320 !a.b = "1.0"
1321 a.c = "1.1"
1322 a.b.g = "2.0"
1323 a.b.d = "2.1"
1324 "#);
1325 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1326 !a.b = "1.0"
1327 a.b.g = "2.0"
1328 a.b.d = "2.1"
1329 "#);
1330
1331 let layers = [&layer0, &layer2];
1333 insta::assert_snapshot!(list(&layers, ""), @r#"
1334 a.b.e = "0.0"
1335 a.b.c.f = "0.1"
1336 !a.b.d = "0.2"
1337 a.b.g = "2.0"
1338 a.b.d = "2.1"
1339 "#);
1340 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1341 a.b.e = "0.0"
1342 a.b.c.f = "0.1"
1343 !a.b.d = "0.2"
1344 a.b.g = "2.0"
1345 a.b.d = "2.1"
1346 "#);
1347 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1348 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1349 !a.b.d = "0.2"
1350 a.b.d = "2.1"
1351 "#);
1352
1353 let layers = [&layer0, &layer1, &layer2];
1355 insta::assert_snapshot!(list(&layers, ""), @r#"
1356 !a.b.e = "0.0"
1357 !a.b.c.f = "0.1"
1358 !a.b.d = "0.2"
1359 !a.b = "1.0"
1360 a.c = "1.1"
1361 a.b.g = "2.0"
1362 a.b.d = "2.1"
1363 "#);
1364 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1365 !a.b.e = "0.0"
1366 !a.b.c.f = "0.1"
1367 !a.b.d = "0.2"
1368 !a.b = "1.0"
1369 a.b.g = "2.0"
1370 a.b.d = "2.1"
1371 "#);
1372 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1373 }
1374
1375 struct TestCase {
1376 files: &'static [&'static str],
1377 env: UnresolvedConfigEnv,
1378 wants: Vec<Want>,
1379 }
1380
1381 #[derive(Debug)]
1382 enum WantState {
1383 New,
1384 Existing,
1385 }
1386 #[derive(Debug)]
1387 struct Want {
1388 path: &'static str,
1389 state: WantState,
1390 }
1391
1392 impl Want {
1393 const fn new(path: &'static str) -> Self {
1394 Self {
1395 path,
1396 state: WantState::New,
1397 }
1398 }
1399
1400 const fn existing(path: &'static str) -> Self {
1401 Self {
1402 path,
1403 state: WantState::Existing,
1404 }
1405 }
1406
1407 fn rooted_path(&self, root: &Path) -> PathBuf {
1408 root.join(self.path)
1409 }
1410
1411 fn exists(&self) -> bool {
1412 matches!(self.state, WantState::Existing)
1413 }
1414 }
1415
1416 fn config_path_home_existing() -> TestCase {
1417 TestCase {
1418 files: &["home/.jjconfig.toml"],
1419 env: UnresolvedConfigEnv {
1420 home_dir: Some("home".into()),
1421 ..Default::default()
1422 },
1423 wants: vec![Want::existing("home/.jjconfig.toml")],
1424 }
1425 }
1426
1427 fn config_path_home_new() -> TestCase {
1428 TestCase {
1429 files: &[],
1430 env: UnresolvedConfigEnv {
1431 home_dir: Some("home".into()),
1432 ..Default::default()
1433 },
1434 wants: vec![Want::new("home/.jjconfig.toml")],
1435 }
1436 }
1437
1438 fn config_path_home_existing_platform_new() -> TestCase {
1439 TestCase {
1440 files: &["home/.jjconfig.toml"],
1441 env: UnresolvedConfigEnv {
1442 home_dir: Some("home".into()),
1443 config_dir: Some("config".into()),
1444 ..Default::default()
1445 },
1446 wants: vec![
1447 Want::existing("home/.jjconfig.toml"),
1448 Want::new("config/jj/config.toml"),
1449 ],
1450 }
1451 }
1452
1453 fn config_path_platform_existing() -> TestCase {
1454 TestCase {
1455 files: &["config/jj/config.toml"],
1456 env: UnresolvedConfigEnv {
1457 home_dir: Some("home".into()),
1458 config_dir: Some("config".into()),
1459 ..Default::default()
1460 },
1461 wants: vec![Want::existing("config/jj/config.toml")],
1462 }
1463 }
1464
1465 fn config_path_platform_new() -> TestCase {
1466 TestCase {
1467 files: &[],
1468 env: UnresolvedConfigEnv {
1469 config_dir: Some("config".into()),
1470 ..Default::default()
1471 },
1472 wants: vec![Want::new("config/jj/config.toml")],
1473 }
1474 }
1475
1476 fn config_path_new_prefer_platform() -> TestCase {
1477 TestCase {
1478 files: &[],
1479 env: UnresolvedConfigEnv {
1480 home_dir: Some("home".into()),
1481 config_dir: Some("config".into()),
1482 ..Default::default()
1483 },
1484 wants: vec![Want::new("config/jj/config.toml")],
1485 }
1486 }
1487
1488 fn config_path_jj_config_existing() -> TestCase {
1489 TestCase {
1490 files: &["custom.toml"],
1491 env: UnresolvedConfigEnv {
1492 jj_config: Some("custom.toml".into()),
1493 ..Default::default()
1494 },
1495 wants: vec![Want::existing("custom.toml")],
1496 }
1497 }
1498
1499 fn config_path_jj_config_new() -> TestCase {
1500 TestCase {
1501 files: &[],
1502 env: UnresolvedConfigEnv {
1503 jj_config: Some("custom.toml".into()),
1504 ..Default::default()
1505 },
1506 wants: vec![Want::new("custom.toml")],
1507 }
1508 }
1509
1510 fn config_path_jj_config_existing_multiple() -> TestCase {
1511 TestCase {
1512 files: &["custom1.toml", "custom2.toml"],
1513 env: UnresolvedConfigEnv {
1514 jj_config: Some(
1515 join_paths(["custom1.toml", "custom2.toml"])
1516 .unwrap()
1517 .into_string()
1518 .unwrap(),
1519 ),
1520 ..Default::default()
1521 },
1522 wants: vec![
1523 Want::existing("custom1.toml"),
1524 Want::existing("custom2.toml"),
1525 ],
1526 }
1527 }
1528
1529 fn config_path_jj_config_new_multiple() -> TestCase {
1530 TestCase {
1531 files: &["custom1.toml"],
1532 env: UnresolvedConfigEnv {
1533 jj_config: Some(
1534 join_paths(["custom1.toml", "custom2.toml"])
1535 .unwrap()
1536 .into_string()
1537 .unwrap(),
1538 ),
1539 ..Default::default()
1540 },
1541 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1542 }
1543 }
1544
1545 fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1546 TestCase {
1547 files: &["custom1.toml"],
1548 env: UnresolvedConfigEnv {
1549 jj_config: Some(
1550 join_paths(["custom1.toml", "", "custom2.toml"])
1551 .unwrap()
1552 .into_string()
1553 .unwrap(),
1554 ),
1555 ..Default::default()
1556 },
1557 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1558 }
1559 }
1560
1561 fn config_path_jj_config_empty() -> TestCase {
1562 TestCase {
1563 files: &[],
1564 env: UnresolvedConfigEnv {
1565 jj_config: Some("".to_owned()),
1566 ..Default::default()
1567 },
1568 wants: vec![],
1569 }
1570 }
1571
1572 fn config_path_config_pick_platform() -> TestCase {
1573 TestCase {
1574 files: &["config/jj/config.toml"],
1575 env: UnresolvedConfigEnv {
1576 home_dir: Some("home".into()),
1577 config_dir: Some("config".into()),
1578 ..Default::default()
1579 },
1580 wants: vec![Want::existing("config/jj/config.toml")],
1581 }
1582 }
1583
1584 fn config_path_config_pick_home() -> TestCase {
1585 TestCase {
1586 files: &["home/.jjconfig.toml"],
1587 env: UnresolvedConfigEnv {
1588 home_dir: Some("home".into()),
1589 config_dir: Some("config".into()),
1590 ..Default::default()
1591 },
1592 wants: vec![
1593 Want::existing("home/.jjconfig.toml"),
1594 Want::new("config/jj/config.toml"),
1595 ],
1596 }
1597 }
1598
1599 fn config_path_platform_new_conf_dir_existing() -> TestCase {
1600 TestCase {
1601 files: &["config/jj/conf.d/_"],
1602 env: UnresolvedConfigEnv {
1603 home_dir: Some("home".into()),
1604 config_dir: Some("config".into()),
1605 ..Default::default()
1606 },
1607 wants: vec![
1608 Want::new("config/jj/config.toml"),
1609 Want::existing("config/jj/conf.d"),
1610 ],
1611 }
1612 }
1613
1614 fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1615 TestCase {
1616 files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1617 env: UnresolvedConfigEnv {
1618 home_dir: Some("home".into()),
1619 config_dir: Some("config".into()),
1620 ..Default::default()
1621 },
1622 wants: vec![
1623 Want::existing("config/jj/config.toml"),
1624 Want::existing("config/jj/conf.d"),
1625 ],
1626 }
1627 }
1628
1629 fn config_path_all_existing() -> TestCase {
1630 TestCase {
1631 files: &[
1632 "config/jj/conf.d/_",
1633 "config/jj/config.toml",
1634 "home/.jjconfig.toml",
1635 ],
1636 env: UnresolvedConfigEnv {
1637 home_dir: Some("home".into()),
1638 config_dir: Some("config".into()),
1639 ..Default::default()
1640 },
1641 wants: vec![
1643 Want::existing("home/.jjconfig.toml"),
1644 Want::existing("config/jj/config.toml"),
1645 Want::existing("config/jj/conf.d"),
1646 ],
1647 }
1648 }
1649
1650 fn config_path_none() -> TestCase {
1651 TestCase {
1652 files: &[],
1653 env: Default::default(),
1654 wants: vec![],
1655 }
1656 }
1657
1658 #[test_case(config_path_home_existing())]
1659 #[test_case(config_path_home_new())]
1660 #[test_case(config_path_home_existing_platform_new())]
1661 #[test_case(config_path_platform_existing())]
1662 #[test_case(config_path_platform_new())]
1663 #[test_case(config_path_new_prefer_platform())]
1664 #[test_case(config_path_jj_config_existing())]
1665 #[test_case(config_path_jj_config_new())]
1666 #[test_case(config_path_jj_config_existing_multiple())]
1667 #[test_case(config_path_jj_config_new_multiple())]
1668 #[test_case(config_path_jj_config_empty_paths_filtered())]
1669 #[test_case(config_path_jj_config_empty())]
1670 #[test_case(config_path_config_pick_platform())]
1671 #[test_case(config_path_config_pick_home())]
1672 #[test_case(config_path_platform_new_conf_dir_existing())]
1673 #[test_case(config_path_platform_existing_conf_dir_existing())]
1674 #[test_case(config_path_all_existing())]
1675 #[test_case(config_path_none())]
1676 fn test_config_path(case: TestCase) {
1677 let tmp = setup_config_fs(case.files);
1678 let env = resolve_config_env(&case.env, tmp.path());
1679
1680 let all_expected_paths = case
1681 .wants
1682 .iter()
1683 .map(|w| w.rooted_path(tmp.path()))
1684 .collect_vec();
1685 let exists_expected_paths = case
1686 .wants
1687 .iter()
1688 .filter(|w| w.exists())
1689 .map(|w| w.rooted_path(tmp.path()))
1690 .collect_vec();
1691
1692 let all_paths = env.user_config_paths().collect_vec();
1693 let exists_paths = env.existing_user_config_paths().collect_vec();
1694
1695 assert_eq!(all_paths, all_expected_paths);
1696 assert_eq!(exists_paths, exists_expected_paths);
1697 }
1698
1699 fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1700 let tmp = testutils::new_temp_dir();
1701 for file in files {
1702 let path = tmp.path().join(file);
1703 if let Some(parent) = path.parent() {
1704 std::fs::create_dir_all(parent).unwrap();
1705 }
1706 std::fs::File::create(path).unwrap();
1707 }
1708 tmp
1709 }
1710
1711 fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1712 let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1713 let env = UnresolvedConfigEnv {
1714 config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1715 home_dir: home_dir.clone(),
1716 jj_config: env.jj_config.as_ref().map(|p| {
1717 join_paths(split_paths(p).map(|p| {
1718 if p.as_os_str().is_empty() {
1719 return p;
1720 }
1721 root.join(p)
1722 }))
1723 .unwrap()
1724 .into_string()
1725 .unwrap()
1726 }),
1727 };
1728 ConfigEnv {
1729 home_dir,
1730 repo_path: None,
1731 workspace_path: None,
1732 user_config_paths: env.resolve(),
1733 repo_config_path: None,
1734 workspace_config_path: None,
1735 command: None,
1736 hostname: None,
1737 }
1738 }
1739}