jj_cli/
config.rs

1// Copyright 2022 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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;
24
25use etcetera::BaseStrategy as _;
26use itertools::Itertools as _;
27use jj_lib::config::ConfigFile;
28use jj_lib::config::ConfigGetError;
29use jj_lib::config::ConfigLayer;
30use jj_lib::config::ConfigLoadError;
31use jj_lib::config::ConfigMigrationRule;
32use jj_lib::config::ConfigNamePathBuf;
33use jj_lib::config::ConfigResolutionContext;
34use jj_lib::config::ConfigSource;
35use jj_lib::config::ConfigValue;
36use jj_lib::config::StackedConfig;
37use jj_lib::dsl_util;
38use regex::Captures;
39use regex::Regex;
40use tracing::instrument;
41
42use crate::command_error::config_error;
43use crate::command_error::config_error_with_message;
44use crate::command_error::CommandError;
45use crate::text_util;
46use crate::ui::Ui;
47
48// TODO(#879): Consider generating entire schema dynamically vs. static file.
49pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");
50
51/// Parses a TOML value expression. Interprets the given value as string if it
52/// can't be parsed and doesn't look like a TOML expression.
53pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> {
54    match value_str.parse() {
55        Ok(value) => Ok(value),
56        Err(_) if is_bare_string(value_str) => Ok(value_str.into()),
57        Err(err) => Err(err),
58    }
59}
60
61fn is_bare_string(value_str: &str) -> bool {
62    // leading whitespace isn't ignored when parsing TOML value expression, but
63    // "\n[]" doesn't look like a bare string.
64    let trimmed = value_str.trim_ascii().as_bytes();
65    if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {
66        // string, array, or table constructs?
67        !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')
68    } else {
69        true // empty or whitespace only
70    }
71}
72
73/// Configuration variable with its source information.
74#[derive(Clone, Debug)]
75pub struct AnnotatedValue {
76    /// Dotted name path to the configuration variable.
77    pub name: ConfigNamePathBuf,
78    /// Configuration value.
79    pub value: ConfigValue,
80    /// Source of the configuration value.
81    pub source: ConfigSource,
82    /// Path to the source file, if available.
83    pub path: Option<PathBuf>,
84    /// True if this value is overridden in higher precedence layers.
85    pub is_overridden: bool,
86}
87
88/// Collects values under the given `filter_prefix` name recursively, from all
89/// layers.
90pub fn resolved_config_values(
91    stacked_config: &StackedConfig,
92    filter_prefix: &ConfigNamePathBuf,
93) -> Vec<AnnotatedValue> {
94    // Collect annotated values in reverse order and mark each value shadowed by
95    // value or table in upper layers.
96    let mut config_vals = vec![];
97    let mut upper_value_names = BTreeSet::new();
98    for layer in stacked_config.layers().iter().rev() {
99        let top_item = match layer.look_up_item(filter_prefix) {
100            Ok(Some(item)) => item,
101            Ok(None) => continue, // parent is a table, but no value found
102            Err(_) => {
103                // parent is not a table, shadows lower layers
104                upper_value_names.insert(filter_prefix.clone());
105                continue;
106            }
107        };
108        let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];
109        while let Some((name, item, is_parent_overridden)) = config_stack.pop() {
110            // Cannot retain inline table formatting because inner values may be
111            // overridden independently.
112            if let Some(table) = item.as_table_like() {
113                // current table and children may be shadowed by value in upper layer
114                let is_overridden = is_parent_overridden || upper_value_names.contains(&name);
115                for (k, v) in table.iter() {
116                    let mut sub_name = name.clone();
117                    sub_name.push(k);
118                    config_stack.push((sub_name, v, is_overridden)); // in reverse order
119                }
120            } else {
121                // current value may be shadowed by value or table in upper layer
122                let maybe_child = upper_value_names
123                    .range(&name..)
124                    .next()
125                    .filter(|next| next.starts_with(&name));
126                let is_overridden = is_parent_overridden || maybe_child.is_some();
127                if maybe_child != Some(&name) {
128                    upper_value_names.insert(name.clone());
129                }
130                let value = item
131                    .clone()
132                    .into_value()
133                    .expect("Item::None should not exist in table");
134                config_vals.push(AnnotatedValue {
135                    name,
136                    value,
137                    source: layer.source,
138                    path: layer.path.clone(),
139                    is_overridden,
140                });
141            }
142        }
143    }
144    config_vals.reverse();
145    config_vals
146}
147
148/// Newtype for unprocessed (or unresolved) [`StackedConfig`].
149///
150/// This doesn't provide any strict guarantee about the underlying config
151/// object. It just requires an explicit cast to access to the config object.
152#[derive(Clone, Debug)]
153pub struct RawConfig(StackedConfig);
154
155impl AsRef<StackedConfig> for RawConfig {
156    fn as_ref(&self) -> &StackedConfig {
157        &self.0
158    }
159}
160
161impl AsMut<StackedConfig> for RawConfig {
162    fn as_mut(&mut self) -> &mut StackedConfig {
163        &mut self.0
164    }
165}
166
167#[derive(Clone, Debug)]
168enum ConfigPathState {
169    New,
170    Exists,
171}
172
173/// A ConfigPath can be in one of two states:
174///
175/// - exists(): a config file exists at the path
176/// - !exists(): a config file doesn't exist here, but a new file _can_ be
177///   created at this path
178#[derive(Clone, Debug)]
179struct ConfigPath {
180    path: PathBuf,
181    state: ConfigPathState,
182}
183
184impl ConfigPath {
185    fn new(path: PathBuf) -> Self {
186        use ConfigPathState::*;
187        ConfigPath {
188            state: if path.exists() { Exists } else { New },
189            path,
190        }
191    }
192
193    fn as_path(&self) -> &Path {
194        &self.path
195    }
196    fn exists(&self) -> bool {
197        match self.state {
198            ConfigPathState::Exists => true,
199            ConfigPathState::New => false,
200        }
201    }
202}
203
204/// Like std::fs::create_dir_all but creates new directories to be accessible to
205/// the user only on Unix (chmod 700).
206fn create_dir_all(path: &Path) -> std::io::Result<()> {
207    let mut dir = std::fs::DirBuilder::new();
208    dir.recursive(true);
209    #[cfg(unix)]
210    {
211        use std::os::unix::fs::DirBuilderExt as _;
212        dir.mode(0o700);
213    }
214    dir.create(path)
215}
216
217// The struct exists so that we can mock certain global values in unit tests.
218#[derive(Clone, Default, Debug)]
219struct UnresolvedConfigEnv {
220    config_dir: Option<PathBuf>,
221    // TODO: remove after jj 0.35
222    macos_legacy_config_dir: Option<PathBuf>,
223    home_dir: Option<PathBuf>,
224    jj_config: Option<String>,
225}
226
227impl UnresolvedConfigEnv {
228    fn resolve(self, ui: &Ui) -> Vec<ConfigPath> {
229        if let Some(paths) = self.jj_config {
230            return split_paths(&paths)
231                .filter(|path| !path.as_os_str().is_empty())
232                .map(ConfigPath::new)
233                .collect();
234        }
235
236        let mut paths = vec![];
237        let home_config_path = self.home_dir.map(|mut home_dir| {
238            home_dir.push(".jjconfig.toml");
239            ConfigPath::new(home_dir)
240        });
241        let platform_config_path = self.config_dir.clone().map(|mut config_dir| {
242            config_dir.push("jj");
243            config_dir.push("config.toml");
244            ConfigPath::new(config_dir)
245        });
246        let platform_config_dir = self.config_dir.map(|mut config_dir| {
247            config_dir.push("jj");
248            config_dir.push("conf.d");
249            ConfigPath::new(config_dir)
250        });
251        let legacy_platform_config_path =
252            self.macos_legacy_config_dir.clone().map(|mut config_dir| {
253                config_dir.push("jj");
254                config_dir.push("config.toml");
255                ConfigPath::new(config_dir)
256            });
257        let legacy_platform_config_dir = self.macos_legacy_config_dir.map(|mut config_dir| {
258            config_dir.push("jj");
259            config_dir.push("conf.d");
260            ConfigPath::new(config_dir)
261        });
262
263        if let Some(path) = home_config_path {
264            if path.exists()
265                || (platform_config_path.is_none() && legacy_platform_config_path.is_none())
266            {
267                paths.push(path);
268            }
269        }
270
271        // This should be the default config created if there's
272        // no user config and `jj config edit` is executed.
273        if let Some(path) = platform_config_path {
274            paths.push(path);
275        }
276
277        // theoretically these should be an `if let Some(...) = ... && ..., but that
278        // isn't stable
279        if let Some(path) = platform_config_dir {
280            if path.exists() {
281                paths.push(path);
282            }
283        }
284
285        if let Some(path) = legacy_platform_config_path {
286            if path.exists() {
287                Self::warn_for_deprecated_path(
288                    ui,
289                    path.as_path(),
290                    "~/Library/Application Support/jj",
291                    "~/.config/jj",
292                );
293                paths.push(path);
294            }
295        }
296        if let Some(path) = legacy_platform_config_dir {
297            if path.exists() {
298                Self::warn_for_deprecated_path(
299                    ui,
300                    path.as_path(),
301                    "~/Library/Application Support/jj",
302                    "~/.config/jj",
303                );
304                paths.push(path);
305            }
306        }
307
308        paths
309    }
310
311    fn warn_for_deprecated_path(ui: &Ui, path: &Path, old: &str, new: &str) {
312        let _ = indoc::writedoc!(
313            ui.warning_default(),
314            r"
315            Deprecated configuration file `{}`.
316            Configuration files in `{old}` are deprecated, and support will be removed in a future release.
317            Instead, move your configuration files to `{new}`.
318            ",
319            path.display(),
320        );
321    }
322}
323
324#[derive(Clone, Debug)]
325pub struct ConfigEnv {
326    home_dir: Option<PathBuf>,
327    repo_path: Option<PathBuf>,
328    user_config_paths: Vec<ConfigPath>,
329    repo_config_path: Option<ConfigPath>,
330    command: Option<String>,
331}
332
333impl ConfigEnv {
334    /// Initializes configuration loader based on environment variables.
335    pub fn from_environment(ui: &Ui) -> Self {
336        let config_dir = etcetera::choose_base_strategy()
337            .ok()
338            .map(|s| s.config_dir());
339
340        // older versions of jj used a more "GUI" config option,
341        // which is not designed for user-editable configuration of CLI utilities.
342        let macos_legacy_config_dir = if cfg!(target_os = "macos") {
343            etcetera::base_strategy::choose_native_strategy()
344                .ok()
345                .map(|s| {
346                    // note that etcetera calls Library/Application Support the "data dir",
347                    // Library/Preferences is supposed to be exclusively plists
348                    s.data_dir()
349                })
350                .filter(|data_dir| {
351                    // User might've purposefully set their config dir to the deprecated one
352                    Some(data_dir) != config_dir.as_ref()
353                })
354        } else {
355            None
356        };
357
358        // Canonicalize home as we do canonicalize cwd in CliRunner. $HOME might
359        // point to symlink.
360        let home_dir = etcetera::home_dir()
361            .ok()
362            .map(|d| dunce::canonicalize(&d).unwrap_or(d));
363
364        let env = UnresolvedConfigEnv {
365            config_dir,
366            macos_legacy_config_dir,
367            home_dir: home_dir.clone(),
368            jj_config: env::var("JJ_CONFIG").ok(),
369        };
370        ConfigEnv {
371            home_dir,
372            repo_path: None,
373            user_config_paths: env.resolve(ui),
374            repo_config_path: None,
375            command: None,
376        }
377    }
378
379    pub fn set_command_name(&mut self, command: String) {
380        self.command = Some(command);
381    }
382
383    /// Returns the paths to the user-specific config files or directories.
384    pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
385        self.user_config_paths.iter().map(ConfigPath::as_path)
386    }
387
388    /// Returns the paths to the existing user-specific config files or
389    /// directories.
390    pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
391        self.user_config_paths
392            .iter()
393            .filter(|p| p.exists())
394            .map(ConfigPath::as_path)
395    }
396
397    /// Returns user configuration files for modification. Instantiates one if
398    /// `config` has no user configuration layers.
399    ///
400    /// The parent directory for the new file may be created by this function.
401    /// If the user configuration path is unknown, this function returns an
402    /// empty `Vec`.
403    pub fn user_config_files(
404        &self,
405        config: &RawConfig,
406    ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
407        config_files_for(config, ConfigSource::User, || self.new_user_config_file())
408    }
409
410    fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
411        self.user_config_paths()
412            .next()
413            .map(|path| {
414                // No need to propagate io::Error here. If the directory
415                // couldn't be created, file.save() would fail later.
416                if let Some(dir) = path.parent() {
417                    create_dir_all(dir).ok();
418                }
419                // The path doesn't usually exist, but we shouldn't overwrite it
420                // with an empty config if it did exist.
421                ConfigFile::load_or_empty(ConfigSource::User, path)
422            })
423            .transpose()
424    }
425
426    /// Loads user-specific config files into the given `config`. The old
427    /// user-config layers will be replaced if any.
428    #[instrument]
429    pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
430        config.as_mut().remove_layers(ConfigSource::User);
431        for path in self.existing_user_config_paths() {
432            if path.is_dir() {
433                config.as_mut().load_dir(ConfigSource::User, path)?;
434            } else {
435                config.as_mut().load_file(ConfigSource::User, path)?;
436            }
437        }
438        Ok(())
439    }
440
441    /// Sets the directory where repo-specific config file is stored. The path
442    /// is usually `.jj/repo`.
443    pub fn reset_repo_path(&mut self, path: &Path) {
444        self.repo_path = Some(path.to_owned());
445        self.repo_config_path = Some(ConfigPath::new(path.join("config.toml")));
446    }
447
448    /// Returns a path to the repo-specific config file.
449    pub fn repo_config_path(&self) -> Option<&Path> {
450        self.repo_config_path.as_ref().map(|p| p.as_path())
451    }
452
453    /// Returns a path to the existing repo-specific config file.
454    fn existing_repo_config_path(&self) -> Option<&Path> {
455        match self.repo_config_path {
456            Some(ref path) if path.exists() => Some(path.as_path()),
457            _ => None,
458        }
459    }
460
461    /// Returns repo configuration files for modification. Instantiates one if
462    /// `config` has no repo configuration layers.
463    ///
464    /// If the repo path is unknown, this function returns an empty `Vec`. Since
465    /// the repo config path cannot be a directory, the returned `Vec` should
466    /// have at most one config file.
467    pub fn repo_config_files(
468        &self,
469        config: &RawConfig,
470    ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
471        config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file())
472    }
473
474    fn new_repo_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
475        self.repo_config_path()
476            // The path doesn't usually exist, but we shouldn't overwrite it
477            // with an empty config if it did exist.
478            .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
479            .transpose()
480    }
481
482    /// Loads repo-specific config file into the given `config`. The old
483    /// repo-config layer will be replaced if any.
484    #[instrument]
485    pub fn reload_repo_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
486        config.as_mut().remove_layers(ConfigSource::Repo);
487        if let Some(path) = self.existing_repo_config_path() {
488            config.as_mut().load_file(ConfigSource::Repo, path)?;
489        }
490        Ok(())
491    }
492
493    /// Resolves conditional scopes within the current environment. Returns new
494    /// resolved config.
495    pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
496        let context = ConfigResolutionContext {
497            home_dir: self.home_dir.as_deref(),
498            repo_path: self.repo_path.as_deref(),
499            command: self.command.as_deref(),
500        };
501        jj_lib::config::resolve(config.as_ref(), &context)
502    }
503}
504
505fn config_files_for(
506    config: &RawConfig,
507    source: ConfigSource,
508    new_file: impl FnOnce() -> Result<Option<ConfigFile>, ConfigLoadError>,
509) -> Result<Vec<ConfigFile>, ConfigLoadError> {
510    let mut files = config
511        .as_ref()
512        .layers_for(source)
513        .iter()
514        .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
515        .collect_vec();
516    if files.is_empty() {
517        files.extend(new_file()?);
518    }
519    Ok(files)
520}
521
522/// Initializes stacked config with the given `default_layers` and infallible
523/// sources.
524///
525/// Sources from the lowest precedence:
526/// 1. Default
527/// 2. Base environment variables
528/// 3. [User configs](https://jj-vcs.github.io/jj/latest/config/)
529/// 4. Repo config `.jj/repo/config.toml`
530/// 5. TODO: Workspace config `.jj/config.toml`
531/// 6. Override environment variables
532/// 7. Command-line arguments `--config`, `--config-toml`, `--config-file`
533///
534/// This function sets up 1, 2, and 6.
535pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
536    let mut config = StackedConfig::with_defaults();
537    config.extend_layers(default_layers);
538    config.add_layer(env_base_layer());
539    config.add_layer(env_overrides_layer());
540    RawConfig(config)
541}
542
543const OP_HOSTNAME: &str = "operation.hostname";
544const OP_USERNAME: &str = "operation.username";
545
546/// Environment variables that should be overridden by config values
547fn env_base_layer() -> ConfigLayer {
548    let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
549    if let Ok(value) = whoami::fallible::hostname()
550        .inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
551    {
552        layer.set_value(OP_HOSTNAME, value).unwrap();
553    }
554    if let Ok(value) = whoami::fallible::username()
555        .inspect_err(|err| tracing::warn!(?err, "failed to get username"))
556    {
557        layer.set_value(OP_USERNAME, value).unwrap();
558    } else if let Ok(value) = env::var("USER") {
559        // On Unix, $USER is set by login(1). Use it as a fallback because
560        // getpwuid() of musl libc appears not (fully?) supporting nsswitch.
561        layer.set_value(OP_USERNAME, value).unwrap();
562    }
563    if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
564        // "User-level configuration files and per-instance command-line arguments
565        // should override $NO_COLOR." https://no-color.org/
566        layer.set_value("ui.color", "never").unwrap();
567    }
568    if let Ok(value) = env::var("PAGER") {
569        layer.set_value("ui.pager", value).unwrap();
570    }
571    if let Ok(value) = env::var("VISUAL") {
572        layer.set_value("ui.editor", value).unwrap();
573    } else if let Ok(value) = env::var("EDITOR") {
574        layer.set_value("ui.editor", value).unwrap();
575    }
576    layer
577}
578
579pub fn default_config_layers() -> Vec<ConfigLayer> {
580    // Syntax error in default config isn't a user error. That's why defaults are
581    // loaded by separate builder.
582    let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
583    let mut layers = vec![
584        parse(include_str!("config/colors.toml")),
585        parse(include_str!("config/hints.toml")),
586        parse(include_str!("config/merge_tools.toml")),
587        parse(include_str!("config/misc.toml")),
588        parse(include_str!("config/revsets.toml")),
589        parse(include_str!("config/templates.toml")),
590    ];
591    if cfg!(unix) {
592        layers.push(parse(include_str!("config/unix.toml")));
593    }
594    if cfg!(windows) {
595        layers.push(parse(include_str!("config/windows.toml")));
596    }
597    layers
598}
599
600/// Environment variables that override config values
601fn env_overrides_layer() -> ConfigLayer {
602    let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
603    if let Ok(value) = env::var("JJ_USER") {
604        layer.set_value("user.name", value).unwrap();
605    }
606    if let Ok(value) = env::var("JJ_EMAIL") {
607        layer.set_value("user.email", value).unwrap();
608    }
609    if let Ok(value) = env::var("JJ_TIMESTAMP") {
610        layer.set_value("debug.commit-timestamp", value).unwrap();
611    }
612    if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
613        layer.set_value("debug.randomness-seed", value).unwrap();
614    }
615    if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
616        layer.set_value("debug.operation-timestamp", value).unwrap();
617    }
618    if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
619        layer.set_value(OP_HOSTNAME, value).unwrap();
620    }
621    if let Ok(value) = env::var("JJ_OP_USERNAME") {
622        layer.set_value(OP_USERNAME, value).unwrap();
623    }
624    if let Ok(value) = env::var("JJ_EDITOR") {
625        layer.set_value("ui.editor", value).unwrap();
626    }
627    layer
628}
629
630/// Configuration source/data type provided as command-line argument.
631#[derive(Clone, Copy, Debug, Eq, PartialEq)]
632pub enum ConfigArgKind {
633    /// `--config=NAME=VALUE`
634    Item,
635    /// `--config-toml=TOML`
636    Toml,
637    /// `--config-file=PATH`
638    File,
639}
640
641/// Parses `--config*` arguments.
642pub fn parse_config_args(
643    toml_strs: &[(ConfigArgKind, &str)],
644) -> Result<Vec<ConfigLayer>, CommandError> {
645    let source = ConfigSource::CommandArg;
646    let mut layers = Vec::new();
647    for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
648        match kind {
649            ConfigArgKind::Item => {
650                let mut layer = ConfigLayer::empty(source);
651                for (_, item) in chunk {
652                    let (name, value) = parse_config_arg_item(item)?;
653                    // Can fail depending on the argument order, but that
654                    // wouldn't matter in practice.
655                    layer.set_value(name, value).map_err(|err| {
656                        config_error_with_message("--config argument cannot be set", err)
657                    })?;
658                }
659                layers.push(layer);
660            }
661            ConfigArgKind::Toml => {
662                for (_, text) in chunk {
663                    layers.push(ConfigLayer::parse(source, text)?);
664                }
665            }
666            ConfigArgKind::File => {
667                for (_, path) in chunk {
668                    layers.push(ConfigLayer::load_from_file(source, path.into())?);
669                }
670            }
671        }
672    }
673    Ok(layers)
674}
675
676/// Parses `NAME=VALUE` string.
677fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
678    // split NAME=VALUE at the first parsable position
679    let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
680    let Some((name, value_str)) = split_candidates
681        .map(|p| (&item_str[..p], &item_str[p + 1..]))
682        .map(|(name, value)| name.parse().map(|name| (name, value)))
683        .find_or_last(Result::is_ok)
684        .transpose()
685        .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
686    else {
687        return Err(config_error("--config must be specified as NAME=VALUE"));
688    };
689    let value = parse_value_or_bare_string(value_str)
690        .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
691    Ok((name, value))
692}
693
694/// List of rules to migrate deprecated config variables.
695pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
696    vec![
697        // TODO: Delete in jj 0.32+
698        ConfigMigrationRule::rename_value("git.auto-local-branch", "git.auto-local-bookmark"),
699        // TODO: Delete in jj 0.33+
700        ConfigMigrationRule::rename_update_value(
701            "signing.sign-all",
702            "signing.behavior",
703            |old_value| {
704                if old_value
705                    .as_bool()
706                    .ok_or("signing.sign-all expects a boolean")?
707                {
708                    Ok("own".into())
709                } else {
710                    Ok("keep".into())
711                }
712            },
713        ),
714        // TODO: Delete in jj 0.34+
715        ConfigMigrationRule::rename_value(
716            "core.watchman.register_snapshot_trigger",
717            "core.watchman.register-snapshot-trigger",
718        ),
719        // TODO: Delete in jj 0.34+
720        ConfigMigrationRule::rename_value("diff.format", "ui.diff.format"),
721        // TODO: Delete in jj 0.35.0+
722        ConfigMigrationRule::rename_update_value(
723            "ui.default-description",
724            "template-aliases.default_commit_description",
725            |old_value| {
726                let value = old_value.as_str().ok_or("expected a string")?;
727                // Trailing newline would be padded by templater
728                let value = text_util::complete_newline(value);
729                let escaped = dsl_util::escape_string(&value);
730                Ok(format!(r#""{escaped}""#).into())
731            },
732        ),
733        // TODO: Delete in jj 0.36+
734        ConfigMigrationRule::rename_value("ui.diff.tool", "ui.diff-formatter"),
735        // TODO: Delete in jj 0.36+
736        ConfigMigrationRule::rename_update_value(
737            "ui.diff.format",
738            "ui.diff-formatter",
739            |old_value| {
740                let value = old_value.as_str().ok_or("expected a string")?;
741                Ok(format!(":{value}").into())
742            },
743        ),
744    ]
745}
746
747/// Command name and arguments specified by config.
748#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
749#[serde(untagged)]
750pub enum CommandNameAndArgs {
751    String(String),
752    Vec(NonEmptyCommandArgsVec),
753    Structured {
754        env: HashMap<String, String>,
755        command: NonEmptyCommandArgsVec,
756    },
757}
758
759impl CommandNameAndArgs {
760    /// Returns command name without arguments.
761    pub fn split_name(&self) -> Cow<str> {
762        let (name, _) = self.split_name_and_args();
763        name
764    }
765
766    /// Returns command name and arguments.
767    ///
768    /// The command name may be an empty string (as well as each argument.)
769    pub fn split_name_and_args(&self) -> (Cow<str>, Cow<[String]>) {
770        match self {
771            CommandNameAndArgs::String(s) => {
772                // Handle things like `EDITOR=emacs -nw` (TODO: parse shell escapes)
773                let mut args = s.split(' ').map(|s| s.to_owned());
774                (args.next().unwrap().into(), args.collect())
775            }
776            CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(a)) => {
777                (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..]))
778            }
779            CommandNameAndArgs::Structured {
780                env: _,
781                command: cmd,
782            } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
783        }
784    }
785
786    /// Returns command string only if the underlying type is a string.
787    ///
788    /// Use this to parse enum strings such as `":builtin"`, which can be
789    /// escaped as `[":builtin"]`.
790    pub fn as_str(&self) -> Option<&str> {
791        match self {
792            CommandNameAndArgs::String(s) => Some(s),
793            CommandNameAndArgs::Vec(_) | CommandNameAndArgs::Structured { .. } => None,
794        }
795    }
796
797    /// Returns process builder configured with this.
798    pub fn to_command(&self) -> Command {
799        let empty: HashMap<&str, &str> = HashMap::new();
800        self.to_command_with_variables(&empty)
801    }
802
803    /// Returns process builder configured with this after interpolating
804    /// variables into the arguments.
805    pub fn to_command_with_variables<V: AsRef<str>>(
806        &self,
807        variables: &HashMap<&str, V>,
808    ) -> Command {
809        let (name, args) = self.split_name_and_args();
810        let mut cmd = Command::new(name.as_ref());
811        if let CommandNameAndArgs::Structured { env, .. } = self {
812            cmd.envs(env);
813        }
814        cmd.args(interpolate_variables(&args, variables));
815        cmd
816    }
817}
818
819impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
820    fn from(s: &T) -> Self {
821        CommandNameAndArgs::String(s.as_ref().to_owned())
822    }
823}
824
825impl fmt::Display for CommandNameAndArgs {
826    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
827        match self {
828            CommandNameAndArgs::String(s) => write!(f, "{s}"),
829            // TODO: format with shell escapes
830            CommandNameAndArgs::Vec(a) => write!(f, "{}", a.0.join(" ")),
831            CommandNameAndArgs::Structured { env, command } => {
832                for (k, v) in env {
833                    write!(f, "{k}={v} ")?;
834                }
835                write!(f, "{}", command.0.join(" "))
836            }
837        }
838    }
839}
840
841// Not interested in $UPPER_CASE_VARIABLES
842static VARIABLE_REGEX: once_cell::sync::Lazy<Regex> =
843    once_cell::sync::Lazy::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
844
845pub fn interpolate_variables<V: AsRef<str>>(
846    args: &[String],
847    variables: &HashMap<&str, V>,
848) -> Vec<String> {
849    args.iter()
850        .map(|arg| {
851            VARIABLE_REGEX
852                .replace_all(arg, |caps: &Captures| {
853                    let name = &caps[1];
854                    if let Some(subst) = variables.get(name) {
855                        subst.as_ref().to_owned()
856                    } else {
857                        caps[0].to_owned()
858                    }
859                })
860                .into_owned()
861        })
862        .collect()
863}
864
865/// Return all variable names found in the args, without the dollar sign
866pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
867    let regex = &*VARIABLE_REGEX;
868    args.iter()
869        .flat_map(|arg| regex.find_iter(arg))
870        .map(|single_match| {
871            let s = single_match.as_str();
872            &s[1..]
873        })
874}
875
876/// Wrapper to reject an array without command name.
877// Based on https://github.com/serde-rs/serde/issues/939
878#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
879#[serde(try_from = "Vec<String>")]
880pub struct NonEmptyCommandArgsVec(Vec<String>);
881
882impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
883    type Error = &'static str;
884
885    fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
886        if args.is_empty() {
887            Err("command arguments should not be empty")
888        } else {
889            Ok(NonEmptyCommandArgsVec(args))
890        }
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use std::env::join_paths;
897    use std::fmt::Write as _;
898
899    use indoc::indoc;
900    use maplit::hashmap;
901    use test_case::test_case;
902
903    use super::*;
904
905    fn insta_settings() -> insta::Settings {
906        let mut settings = insta::Settings::clone_current();
907        // Suppress Decor { .. } which is uninteresting
908        settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
909        settings
910    }
911
912    #[test]
913    fn test_parse_value_or_bare_string() {
914        let parse = |s: &str| parse_value_or_bare_string(s);
915
916        // Value in TOML syntax
917        assert_eq!(parse("true").unwrap().as_bool(), Some(true));
918        assert_eq!(parse("42").unwrap().as_integer(), Some(42));
919        assert_eq!(parse("-1").unwrap().as_integer(), Some(-1));
920        assert_eq!(parse("'a'").unwrap().as_str(), Some("a"));
921        assert!(parse("[]").unwrap().is_array());
922        assert!(parse("{ a = 'b' }").unwrap().is_inline_table());
923
924        // Bare string
925        assert_eq!(parse("").unwrap().as_str(), Some(""));
926        assert_eq!(parse("John Doe").unwrap().as_str(), Some("John Doe"));
927        assert_eq!(parse("Doe, John").unwrap().as_str(), Some("Doe, John"));
928        assert_eq!(parse("It's okay").unwrap().as_str(), Some("It's okay"));
929        assert_eq!(
930            parse("<foo+bar@example.org>").unwrap().as_str(),
931            Some("<foo+bar@example.org>")
932        );
933        assert_eq!(parse("#ff00aa").unwrap().as_str(), Some("#ff00aa"));
934        assert_eq!(parse("all()").unwrap().as_str(), Some("all()"));
935        assert_eq!(parse("glob:*.*").unwrap().as_str(), Some("glob:*.*"));
936        assert_eq!(parse("柔術").unwrap().as_str(), Some("柔術"));
937
938        // Error in TOML value
939        assert!(parse("'foo").is_err());
940        assert!(parse(r#" bar" "#).is_err());
941        assert!(parse("[0 1]").is_err());
942        assert!(parse("{ x = }").is_err());
943        assert!(parse("\n { x").is_err());
944        assert!(parse(" x ] ").is_err());
945        assert!(parse("[table]\nkey = 'value'").is_err());
946    }
947
948    #[test]
949    fn test_parse_config_arg_item() {
950        assert!(parse_config_arg_item("").is_err());
951        assert!(parse_config_arg_item("a").is_err());
952        assert!(parse_config_arg_item("=").is_err());
953        // The value parser is sensitive to leading whitespaces, which seems
954        // good because the parsing falls back to a bare string.
955        assert!(parse_config_arg_item("a = 'b'").is_err());
956
957        let (name, value) = parse_config_arg_item("a=b").unwrap();
958        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
959        assert_eq!(value.as_str(), Some("b"));
960
961        let (name, value) = parse_config_arg_item("a=").unwrap();
962        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
963        assert_eq!(value.as_str(), Some(""));
964
965        let (name, value) = parse_config_arg_item("a= ").unwrap();
966        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
967        assert_eq!(value.as_str(), Some(" "));
968
969        // This one is a bit cryptic, but b=c can be a bare string.
970        let (name, value) = parse_config_arg_item("a=b=c").unwrap();
971        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
972        assert_eq!(value.as_str(), Some("b=c"));
973
974        let (name, value) = parse_config_arg_item("a.b=true").unwrap();
975        assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
976        assert_eq!(value.as_bool(), Some(true));
977
978        let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
979        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
980        assert_eq!(value.as_str(), Some("b=c"));
981
982        let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
983        assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
984        assert_eq!(value.as_str(), Some("c"));
985
986        let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
987        assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
988        assert!(value.is_inline_table());
989        assert_eq!(value.to_string(), "{d = 'e=f'}");
990    }
991
992    #[test]
993    fn test_command_args() {
994        let mut config = StackedConfig::empty();
995        config.add_layer(
996            ConfigLayer::parse(
997                ConfigSource::User,
998                indoc! {"
999                    empty_array = []
1000                    empty_string = ''
1001                    array = ['emacs', '-nw']
1002                    string = 'emacs -nw'
1003                    structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1004                    structured.command = ['emacs', '-nw']
1005                "},
1006            )
1007            .unwrap(),
1008        );
1009
1010        assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1011
1012        let command_args: CommandNameAndArgs = config.get("empty_string").unwrap();
1013        assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1014        let (name, args) = command_args.split_name_and_args();
1015        assert_eq!(name, "");
1016        assert!(args.is_empty());
1017
1018        let command_args: CommandNameAndArgs = config.get("array").unwrap();
1019        assert_eq!(
1020            command_args,
1021            CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1022                ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1023            ))
1024        );
1025        let (name, args) = command_args.split_name_and_args();
1026        assert_eq!(name, "emacs");
1027        assert_eq!(args, ["-nw"].as_ref());
1028
1029        let command_args: CommandNameAndArgs = config.get("string").unwrap();
1030        assert_eq!(
1031            command_args,
1032            CommandNameAndArgs::String("emacs -nw".to_owned())
1033        );
1034        let (name, args) = command_args.split_name_and_args();
1035        assert_eq!(name, "emacs");
1036        assert_eq!(args, ["-nw"].as_ref());
1037
1038        let command_args: CommandNameAndArgs = config.get("structured").unwrap();
1039        assert_eq!(
1040            command_args,
1041            CommandNameAndArgs::Structured {
1042                env: hashmap! {
1043                    "KEY1".to_string() => "value1".to_string(),
1044                    "KEY2".to_string() => "value2".to_string(),
1045                },
1046                command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1047            }
1048        );
1049        let (name, args) = command_args.split_name_and_args();
1050        assert_eq!(name, "emacs");
1051        assert_eq!(args, ["-nw"].as_ref());
1052    }
1053
1054    #[test]
1055    fn test_resolved_config_values_empty() {
1056        let config = StackedConfig::empty();
1057        assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
1058    }
1059
1060    #[test]
1061    fn test_resolved_config_values_single_key() {
1062        let settings = insta_settings();
1063        let _guard = settings.bind_to_scope();
1064        let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
1065        env_base_layer
1066            .set_value("user.name", "base-user-name")
1067            .unwrap();
1068        env_base_layer
1069            .set_value("user.email", "base@user.email")
1070            .unwrap();
1071        let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1072        repo_layer
1073            .set_value("user.email", "repo@user.email")
1074            .unwrap();
1075        let mut config = StackedConfig::empty();
1076        config.add_layer(env_base_layer);
1077        config.add_layer(repo_layer);
1078        // Note: "email" is alphabetized, before "name" from same layer.
1079        insta::assert_debug_snapshot!(
1080            resolved_config_values(&config, &ConfigNamePathBuf::root()),
1081            @r#"
1082        [
1083            AnnotatedValue {
1084                name: ConfigNamePathBuf(
1085                    [
1086                        Key {
1087                            key: "user",
1088                            repr: None,
1089                            leaf_decor: Decor { .. },
1090                            dotted_decor: Decor { .. },
1091                        },
1092                        Key {
1093                            key: "name",
1094                            repr: None,
1095                            leaf_decor: Decor { .. },
1096                            dotted_decor: Decor { .. },
1097                        },
1098                    ],
1099                ),
1100                value: String(
1101                    Formatted {
1102                        value: "base-user-name",
1103                        repr: "default",
1104                        decor: Decor { .. },
1105                    },
1106                ),
1107                source: EnvBase,
1108                path: None,
1109                is_overridden: false,
1110            },
1111            AnnotatedValue {
1112                name: ConfigNamePathBuf(
1113                    [
1114                        Key {
1115                            key: "user",
1116                            repr: None,
1117                            leaf_decor: Decor { .. },
1118                            dotted_decor: Decor { .. },
1119                        },
1120                        Key {
1121                            key: "email",
1122                            repr: None,
1123                            leaf_decor: Decor { .. },
1124                            dotted_decor: Decor { .. },
1125                        },
1126                    ],
1127                ),
1128                value: String(
1129                    Formatted {
1130                        value: "base@user.email",
1131                        repr: "default",
1132                        decor: Decor { .. },
1133                    },
1134                ),
1135                source: EnvBase,
1136                path: None,
1137                is_overridden: true,
1138            },
1139            AnnotatedValue {
1140                name: ConfigNamePathBuf(
1141                    [
1142                        Key {
1143                            key: "user",
1144                            repr: None,
1145                            leaf_decor: Decor { .. },
1146                            dotted_decor: Decor { .. },
1147                        },
1148                        Key {
1149                            key: "email",
1150                            repr: None,
1151                            leaf_decor: Decor { .. },
1152                            dotted_decor: Decor { .. },
1153                        },
1154                    ],
1155                ),
1156                value: String(
1157                    Formatted {
1158                        value: "repo@user.email",
1159                        repr: "default",
1160                        decor: Decor { .. },
1161                    },
1162                ),
1163                source: Repo,
1164                path: None,
1165                is_overridden: false,
1166            },
1167        ]
1168        "#
1169        );
1170    }
1171
1172    #[test]
1173    fn test_resolved_config_values_filter_path() {
1174        let settings = insta_settings();
1175        let _guard = settings.bind_to_scope();
1176        let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1177        user_layer.set_value("test-table1.foo", "user-FOO").unwrap();
1178        user_layer.set_value("test-table2.bar", "user-BAR").unwrap();
1179        let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1180        repo_layer.set_value("test-table1.bar", "repo-BAR").unwrap();
1181        let mut config = StackedConfig::empty();
1182        config.add_layer(user_layer);
1183        config.add_layer(repo_layer);
1184        insta::assert_debug_snapshot!(
1185            resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1186            @r#"
1187        [
1188            AnnotatedValue {
1189                name: ConfigNamePathBuf(
1190                    [
1191                        Key {
1192                            key: "test-table1",
1193                            repr: None,
1194                            leaf_decor: Decor { .. },
1195                            dotted_decor: Decor { .. },
1196                        },
1197                        Key {
1198                            key: "foo",
1199                            repr: None,
1200                            leaf_decor: Decor { .. },
1201                            dotted_decor: Decor { .. },
1202                        },
1203                    ],
1204                ),
1205                value: String(
1206                    Formatted {
1207                        value: "user-FOO",
1208                        repr: "default",
1209                        decor: Decor { .. },
1210                    },
1211                ),
1212                source: User,
1213                path: None,
1214                is_overridden: false,
1215            },
1216            AnnotatedValue {
1217                name: ConfigNamePathBuf(
1218                    [
1219                        Key {
1220                            key: "test-table1",
1221                            repr: None,
1222                            leaf_decor: Decor { .. },
1223                            dotted_decor: Decor { .. },
1224                        },
1225                        Key {
1226                            key: "bar",
1227                            repr: None,
1228                            leaf_decor: Decor { .. },
1229                            dotted_decor: Decor { .. },
1230                        },
1231                    ],
1232                ),
1233                value: String(
1234                    Formatted {
1235                        value: "repo-BAR",
1236                        repr: "default",
1237                        decor: Decor { .. },
1238                    },
1239                ),
1240                source: Repo,
1241                path: None,
1242                is_overridden: false,
1243            },
1244        ]
1245        "#
1246        );
1247    }
1248
1249    #[test]
1250    fn test_resolved_config_values_overridden() {
1251        let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1252            let mut config = StackedConfig::empty();
1253            config.extend_layers(layers.iter().copied().cloned());
1254            let prefix = if prefix.is_empty() {
1255                ConfigNamePathBuf::root()
1256            } else {
1257                prefix.parse().unwrap()
1258            };
1259            let mut output = String::new();
1260            for annotated in resolved_config_values(&config, &prefix) {
1261                let AnnotatedValue { name, value, .. } = &annotated;
1262                let sigil = if annotated.is_overridden { '!' } else { ' ' };
1263                writeln!(output, "{sigil}{name} = {value}").unwrap();
1264            }
1265            output
1266        };
1267
1268        let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1269        layer0.set_value("a.b.e", "0.0").unwrap();
1270        layer0.set_value("a.b.c.f", "0.1").unwrap();
1271        layer0.set_value("a.b.d", "0.2").unwrap();
1272        let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1273        layer1.set_value("a.b", "1.0").unwrap();
1274        layer1.set_value("a.c", "1.1").unwrap();
1275        let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1276        layer2.set_value("a.b.g", "2.0").unwrap();
1277        layer2.set_value("a.b.d", "2.1").unwrap();
1278
1279        // a.b.* is shadowed by a.b
1280        let layers = [&layer0, &layer1];
1281        insta::assert_snapshot!(list(&layers, ""), @r#"
1282        !a.b.e = "0.0"
1283        !a.b.c.f = "0.1"
1284        !a.b.d = "0.2"
1285         a.b = "1.0"
1286         a.c = "1.1"
1287        "#);
1288        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1289        !a.b.e = "0.0"
1290        !a.b.c.f = "0.1"
1291        !a.b.d = "0.2"
1292         a.b = "1.0"
1293        "#);
1294        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1295        insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1296
1297        // a.b is shadowed by a.b.*
1298        let layers = [&layer1, &layer2];
1299        insta::assert_snapshot!(list(&layers, ""), @r#"
1300        !a.b = "1.0"
1301         a.c = "1.1"
1302         a.b.g = "2.0"
1303         a.b.d = "2.1"
1304        "#);
1305        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1306        !a.b = "1.0"
1307         a.b.g = "2.0"
1308         a.b.d = "2.1"
1309        "#);
1310
1311        // a.b.d is shadowed by a.b.d
1312        let layers = [&layer0, &layer2];
1313        insta::assert_snapshot!(list(&layers, ""), @r#"
1314         a.b.e = "0.0"
1315         a.b.c.f = "0.1"
1316        !a.b.d = "0.2"
1317         a.b.g = "2.0"
1318         a.b.d = "2.1"
1319        "#);
1320        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1321         a.b.e = "0.0"
1322         a.b.c.f = "0.1"
1323        !a.b.d = "0.2"
1324         a.b.g = "2.0"
1325         a.b.d = "2.1"
1326        "#);
1327        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1328        insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1329        !a.b.d = "0.2"
1330         a.b.d = "2.1"
1331        "#);
1332
1333        // a.b.* is shadowed by a.b, which is shadowed by a.b.*
1334        let layers = [&layer0, &layer1, &layer2];
1335        insta::assert_snapshot!(list(&layers, ""), @r#"
1336        !a.b.e = "0.0"
1337        !a.b.c.f = "0.1"
1338        !a.b.d = "0.2"
1339        !a.b = "1.0"
1340         a.c = "1.1"
1341         a.b.g = "2.0"
1342         a.b.d = "2.1"
1343        "#);
1344        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1345        !a.b.e = "0.0"
1346        !a.b.c.f = "0.1"
1347        !a.b.d = "0.2"
1348        !a.b = "1.0"
1349         a.b.g = "2.0"
1350         a.b.d = "2.1"
1351        "#);
1352        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1353    }
1354
1355    struct TestCase {
1356        files: &'static [&'static str],
1357        env: UnresolvedConfigEnv,
1358        wants: Vec<Want>,
1359    }
1360
1361    #[derive(Debug)]
1362    enum WantState {
1363        New,
1364        Existing,
1365    }
1366    #[derive(Debug)]
1367    struct Want {
1368        path: &'static str,
1369        state: WantState,
1370    }
1371
1372    impl Want {
1373        const fn new(path: &'static str) -> Want {
1374            Want {
1375                path,
1376                state: WantState::New,
1377            }
1378        }
1379
1380        const fn existing(path: &'static str) -> Want {
1381            Want {
1382                path,
1383                state: WantState::Existing,
1384            }
1385        }
1386
1387        fn rooted_path(&self, root: &Path) -> PathBuf {
1388            root.join(self.path)
1389        }
1390
1391        fn exists(&self) -> bool {
1392            matches!(self.state, WantState::Existing)
1393        }
1394    }
1395
1396    fn config_path_home_existing() -> TestCase {
1397        TestCase {
1398            files: &["home/.jjconfig.toml"],
1399            env: UnresolvedConfigEnv {
1400                home_dir: Some("home".into()),
1401                ..Default::default()
1402            },
1403            wants: vec![Want::existing("home/.jjconfig.toml")],
1404        }
1405    }
1406
1407    fn config_path_home_new() -> TestCase {
1408        TestCase {
1409            files: &[],
1410            env: UnresolvedConfigEnv {
1411                home_dir: Some("home".into()),
1412                ..Default::default()
1413            },
1414            wants: vec![Want::new("home/.jjconfig.toml")],
1415        }
1416    }
1417
1418    fn config_path_home_existing_platform_new() -> TestCase {
1419        TestCase {
1420            files: &["home/.jjconfig.toml"],
1421            env: UnresolvedConfigEnv {
1422                home_dir: Some("home".into()),
1423                config_dir: Some("config".into()),
1424                ..Default::default()
1425            },
1426            wants: vec![
1427                Want::existing("home/.jjconfig.toml"),
1428                Want::new("config/jj/config.toml"),
1429            ],
1430        }
1431    }
1432
1433    fn config_path_platform_existing() -> TestCase {
1434        TestCase {
1435            files: &["config/jj/config.toml"],
1436            env: UnresolvedConfigEnv {
1437                home_dir: Some("home".into()),
1438                config_dir: Some("config".into()),
1439                ..Default::default()
1440            },
1441            wants: vec![Want::existing("config/jj/config.toml")],
1442        }
1443    }
1444
1445    fn config_path_platform_new() -> TestCase {
1446        TestCase {
1447            files: &[],
1448            env: UnresolvedConfigEnv {
1449                config_dir: Some("config".into()),
1450                ..Default::default()
1451            },
1452            wants: vec![Want::new("config/jj/config.toml")],
1453        }
1454    }
1455
1456    fn config_path_new_prefer_platform() -> TestCase {
1457        TestCase {
1458            files: &[],
1459            env: UnresolvedConfigEnv {
1460                home_dir: Some("home".into()),
1461                config_dir: Some("config".into()),
1462                ..Default::default()
1463            },
1464            wants: vec![Want::new("config/jj/config.toml")],
1465        }
1466    }
1467
1468    fn config_path_jj_config_existing() -> TestCase {
1469        TestCase {
1470            files: &["custom.toml"],
1471            env: UnresolvedConfigEnv {
1472                jj_config: Some("custom.toml".into()),
1473                ..Default::default()
1474            },
1475            wants: vec![Want::existing("custom.toml")],
1476        }
1477    }
1478
1479    fn config_path_jj_config_new() -> TestCase {
1480        TestCase {
1481            files: &[],
1482            env: UnresolvedConfigEnv {
1483                jj_config: Some("custom.toml".into()),
1484                ..Default::default()
1485            },
1486            wants: vec![Want::new("custom.toml")],
1487        }
1488    }
1489
1490    fn config_path_jj_config_existing_multiple() -> TestCase {
1491        TestCase {
1492            files: &["custom1.toml", "custom2.toml"],
1493            env: UnresolvedConfigEnv {
1494                jj_config: Some(
1495                    join_paths(["custom1.toml", "custom2.toml"])
1496                        .unwrap()
1497                        .into_string()
1498                        .unwrap(),
1499                ),
1500                ..Default::default()
1501            },
1502            wants: vec![
1503                Want::existing("custom1.toml"),
1504                Want::existing("custom2.toml"),
1505            ],
1506        }
1507    }
1508
1509    fn config_path_jj_config_new_multiple() -> TestCase {
1510        TestCase {
1511            files: &["custom1.toml"],
1512            env: UnresolvedConfigEnv {
1513                jj_config: Some(
1514                    join_paths(["custom1.toml", "custom2.toml"])
1515                        .unwrap()
1516                        .into_string()
1517                        .unwrap(),
1518                ),
1519                ..Default::default()
1520            },
1521            wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1522        }
1523    }
1524
1525    fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1526        TestCase {
1527            files: &["custom1.toml"],
1528            env: UnresolvedConfigEnv {
1529                jj_config: Some(
1530                    join_paths(["custom1.toml", "", "custom2.toml"])
1531                        .unwrap()
1532                        .into_string()
1533                        .unwrap(),
1534                ),
1535                ..Default::default()
1536            },
1537            wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1538        }
1539    }
1540
1541    fn config_path_jj_config_empty() -> TestCase {
1542        TestCase {
1543            files: &[],
1544            env: UnresolvedConfigEnv {
1545                jj_config: Some("".to_owned()),
1546                ..Default::default()
1547            },
1548            wants: vec![],
1549        }
1550    }
1551
1552    fn config_path_config_pick_platform() -> TestCase {
1553        TestCase {
1554            files: &["config/jj/config.toml"],
1555            env: UnresolvedConfigEnv {
1556                home_dir: Some("home".into()),
1557                config_dir: Some("config".into()),
1558                ..Default::default()
1559            },
1560            wants: vec![Want::existing("config/jj/config.toml")],
1561        }
1562    }
1563
1564    fn config_path_config_pick_home() -> TestCase {
1565        TestCase {
1566            files: &["home/.jjconfig.toml"],
1567            env: UnresolvedConfigEnv {
1568                home_dir: Some("home".into()),
1569                config_dir: Some("config".into()),
1570                ..Default::default()
1571            },
1572            wants: vec![
1573                Want::existing("home/.jjconfig.toml"),
1574                Want::new("config/jj/config.toml"),
1575            ],
1576        }
1577    }
1578
1579    fn config_path_platform_new_conf_dir_existing() -> TestCase {
1580        TestCase {
1581            files: &["config/jj/conf.d/_"],
1582            env: UnresolvedConfigEnv {
1583                home_dir: Some("home".into()),
1584                config_dir: Some("config".into()),
1585                ..Default::default()
1586            },
1587            wants: vec![
1588                Want::new("config/jj/config.toml"),
1589                Want::existing("config/jj/conf.d"),
1590            ],
1591        }
1592    }
1593
1594    fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1595        TestCase {
1596            files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1597            env: UnresolvedConfigEnv {
1598                home_dir: Some("home".into()),
1599                config_dir: Some("config".into()),
1600                ..Default::default()
1601            },
1602            wants: vec![
1603                Want::existing("config/jj/config.toml"),
1604                Want::existing("config/jj/conf.d"),
1605            ],
1606        }
1607    }
1608
1609    fn config_path_all_existing() -> TestCase {
1610        TestCase {
1611            files: &[
1612                "config/jj/conf.d/_",
1613                "config/jj/config.toml",
1614                "home/.jjconfig.toml",
1615            ],
1616            env: UnresolvedConfigEnv {
1617                home_dir: Some("home".into()),
1618                config_dir: Some("config".into()),
1619                ..Default::default()
1620            },
1621            // Precedence order is important
1622            wants: vec![
1623                Want::existing("home/.jjconfig.toml"),
1624                Want::existing("config/jj/config.toml"),
1625                Want::existing("config/jj/conf.d"),
1626            ],
1627        }
1628    }
1629
1630    fn config_path_none() -> TestCase {
1631        TestCase {
1632            files: &[],
1633            env: Default::default(),
1634            wants: vec![],
1635        }
1636    }
1637
1638    fn config_path_macos_legacy_exists() -> TestCase {
1639        TestCase {
1640            files: &["macos-legacy/jj/config.toml"],
1641            env: UnresolvedConfigEnv {
1642                home_dir: Some("home".into()),
1643                config_dir: Some("config".into()),
1644                macos_legacy_config_dir: Some("macos-legacy".into()),
1645                ..Default::default()
1646            },
1647            wants: vec![
1648                Want::new("config/jj/config.toml"),
1649                Want::existing("macos-legacy/jj/config.toml"),
1650            ],
1651        }
1652    }
1653
1654    fn config_path_macos_legacy_both_exist() -> TestCase {
1655        TestCase {
1656            files: &["macos-legacy/jj/config.toml", "config/jj/config.toml"],
1657            env: UnresolvedConfigEnv {
1658                home_dir: Some("home".into()),
1659                config_dir: Some("config".into()),
1660                macos_legacy_config_dir: Some("macos-legacy".into()),
1661                ..Default::default()
1662            },
1663            wants: vec![
1664                Want::existing("config/jj/config.toml"),
1665                Want::existing("macos-legacy/jj/config.toml"),
1666            ],
1667        }
1668    }
1669
1670    fn config_path_macos_legacy_new() -> TestCase {
1671        TestCase {
1672            files: &[],
1673            env: UnresolvedConfigEnv {
1674                home_dir: Some("home".into()),
1675                config_dir: Some("config".into()),
1676                macos_legacy_config_dir: Some("macos-legacy".into()),
1677                ..Default::default()
1678            },
1679            wants: vec![Want::new("config/jj/config.toml")],
1680        }
1681    }
1682
1683    #[test_case(config_path_home_existing())]
1684    #[test_case(config_path_home_new())]
1685    #[test_case(config_path_home_existing_platform_new())]
1686    #[test_case(config_path_platform_existing())]
1687    #[test_case(config_path_platform_new())]
1688    #[test_case(config_path_new_prefer_platform())]
1689    #[test_case(config_path_jj_config_existing())]
1690    #[test_case(config_path_jj_config_new())]
1691    #[test_case(config_path_jj_config_existing_multiple())]
1692    #[test_case(config_path_jj_config_new_multiple())]
1693    #[test_case(config_path_jj_config_empty_paths_filtered())]
1694    #[test_case(config_path_jj_config_empty())]
1695    #[test_case(config_path_config_pick_platform())]
1696    #[test_case(config_path_config_pick_home())]
1697    #[test_case(config_path_platform_new_conf_dir_existing())]
1698    #[test_case(config_path_platform_existing_conf_dir_existing())]
1699    #[test_case(config_path_all_existing())]
1700    #[test_case(config_path_none())]
1701    #[test_case(config_path_macos_legacy_exists())]
1702    #[test_case(config_path_macos_legacy_both_exist())]
1703    #[test_case(config_path_macos_legacy_new())]
1704    fn test_config_path(case: TestCase) {
1705        let tmp = setup_config_fs(case.files);
1706        let env = resolve_config_env(&case.env, tmp.path());
1707
1708        let all_expected_paths = case
1709            .wants
1710            .iter()
1711            .map(|w| w.rooted_path(tmp.path()))
1712            .collect_vec();
1713        let exists_expected_paths = case
1714            .wants
1715            .iter()
1716            .filter(|w| w.exists())
1717            .map(|w| w.rooted_path(tmp.path()))
1718            .collect_vec();
1719
1720        let all_paths = env.user_config_paths().collect_vec();
1721        let exists_paths = env.existing_user_config_paths().collect_vec();
1722
1723        assert_eq!(all_paths, all_expected_paths);
1724        assert_eq!(exists_paths, exists_expected_paths);
1725    }
1726
1727    fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1728        let tmp = testutils::new_temp_dir();
1729        for file in files {
1730            let path = tmp.path().join(file);
1731            if let Some(parent) = path.parent() {
1732                std::fs::create_dir_all(parent).unwrap();
1733            }
1734            std::fs::File::create(path).unwrap();
1735        }
1736        tmp
1737    }
1738
1739    fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1740        let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1741        let env = UnresolvedConfigEnv {
1742            config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1743            macos_legacy_config_dir: env.macos_legacy_config_dir.as_ref().map(|p| root.join(p)),
1744            home_dir: home_dir.clone(),
1745            jj_config: env.jj_config.as_ref().map(|p| {
1746                join_paths(split_paths(p).map(|p| {
1747                    if p.as_os_str().is_empty() {
1748                        return p;
1749                    }
1750                    root.join(p)
1751                }))
1752                .unwrap()
1753                .into_string()
1754                .unwrap()
1755            }),
1756        };
1757        ConfigEnv {
1758            home_dir,
1759            repo_path: None,
1760            user_config_paths: env.resolve(&Ui::null()),
1761            repo_config_path: None,
1762            command: None,
1763        }
1764    }
1765}