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;
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
47// TODO(#879): Consider generating entire schema dynamically vs. static file.
48pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");
49
50/// Parses a TOML value expression. Interprets the given value as string if it
51/// can't be parsed and doesn't look like a TOML expression.
52pub 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    // leading whitespace isn't ignored when parsing TOML value expression, but
62    // "\n[]" doesn't look like a bare string.
63    let trimmed = value_str.trim_ascii().as_bytes();
64    if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {
65        // string, array, or table constructs?
66        !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')
67    } else {
68        true // empty or whitespace only
69    }
70}
71
72/// Converts [`ConfigValue`] (or [`toml_edit::Value`]) to [`toml::Value`] which
73/// implements [`serde::Serialize`].
74pub 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/// Configuration variable with its source information.
96#[derive(Clone, Debug, serde::Serialize)]
97pub struct AnnotatedValue {
98    /// Dotted name path to the configuration variable.
99    #[serde(serialize_with = "serialize_name")]
100    pub name: ConfigNamePathBuf,
101    /// Configuration value.
102    #[serde(serialize_with = "serialize_value")]
103    pub value: ConfigValue,
104    /// Source of the configuration value.
105    #[serde(serialize_with = "serialize_source")]
106    pub source: ConfigSource,
107    /// Path to the source file, if available.
108    pub path: Option<PathBuf>,
109    /// True if this value is overridden in higher precedence layers.
110    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
134/// Collects values under the given `filter_prefix` name recursively, from all
135/// layers.
136pub fn resolved_config_values(
137    stacked_config: &StackedConfig,
138    filter_prefix: &ConfigNamePathBuf,
139) -> Vec<AnnotatedValue> {
140    // Collect annotated values in reverse order and mark each value shadowed by
141    // value or table in upper layers.
142    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, // parent is a table, but no value found
148            Err(_) => {
149                // parent is not a table, shadows lower layers
150                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            // Cannot retain inline table formatting because inner values may be
157            // overridden independently.
158            if let Some(table) = item.as_table_like() {
159                // current table and children may be shadowed by value in upper layer
160                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)); // in reverse order
165                }
166            } else {
167                // current value may be shadowed by value or table in upper layer
168                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/// Newtype for unprocessed (or unresolved) [`StackedConfig`].
195///
196/// This doesn't provide any strict guarantee about the underlying config
197/// object. It just requires an explicit cast to access to the config object.
198#[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/// A ConfigPath can be in one of two states:
220///
221/// - exists(): a config file exists at the path
222/// - !exists(): a config file doesn't exist here, but a new file _can_ be
223///   created at this path
224#[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
250/// Like std::fs::create_dir_all but creates new directories to be accessible to
251/// the user only on Unix (chmod 700).
252fn 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// The struct exists so that we can mock certain global values in unit tests.
264#[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        // This should be the default config created if there's
303        // no user config and `jj config edit` is executed.
304        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    /// Initializes configuration loader based on environment variables.
332    pub fn from_environment() -> Self {
333        let config_dir = etcetera::choose_base_strategy()
334            .ok()
335            .map(|s| s.config_dir());
336
337        // Canonicalize home as we do canonicalize cwd in CliRunner. $HOME might
338        // point to symlink.
339        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    /// Returns the paths to the user-specific config files or directories.
365    pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
366        self.user_config_paths.iter().map(ConfigPath::as_path)
367    }
368
369    /// Returns the paths to the existing user-specific config files or
370    /// directories.
371    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    /// Returns user configuration files for modification. Instantiates one if
379    /// `config` has no user configuration layers.
380    ///
381    /// The parent directory for the new file may be created by this function.
382    /// If the user configuration path is unknown, this function returns an
383    /// empty `Vec`.
384    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                // No need to propagate io::Error here. If the directory
396                // couldn't be created, file.save() would fail later.
397                if let Some(dir) = path.parent() {
398                    create_dir_all(dir).ok();
399                }
400                // The path doesn't usually exist, but we shouldn't overwrite it
401                // with an empty config if it did exist.
402                ConfigFile::load_or_empty(ConfigSource::User, path)
403            })
404            .transpose()
405    }
406
407    /// Loads user-specific config files into the given `config`. The old
408    /// user-config layers will be replaced if any.
409    #[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    /// Sets the directory where repo-specific config file is stored. The path
423    /// is usually `.jj/repo`.
424    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    /// Returns a path to the repo-specific config file.
430    pub fn repo_config_path(&self) -> Option<&Path> {
431        self.repo_config_path.as_ref().map(|p| p.as_path())
432    }
433
434    /// Returns a path to the existing repo-specific config file.
435    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    /// Returns repo configuration files for modification. Instantiates one if
443    /// `config` has no repo configuration layers.
444    ///
445    /// If the repo path is unknown, this function returns an empty `Vec`. Since
446    /// the repo config path cannot be a directory, the returned `Vec` should
447    /// have at most one config file.
448    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            // The path doesn't usually exist, but we shouldn't overwrite it
458            // with an empty config if it did exist.
459            .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
460            .transpose()
461    }
462
463    /// Loads repo-specific config file into the given `config`. The old
464    /// repo-config layer will be replaced if any.
465    #[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    /// Sets the directory for the workspace and the workspace-specific config
475    /// file.
476    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    /// Returns a path to the workspace-specific config file.
484    pub fn workspace_config_path(&self) -> Option<&Path> {
485        self.workspace_config_path.as_ref().map(|p| p.as_path())
486    }
487
488    /// Returns a path to the existing workspace-specific config file.
489    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    /// Returns workspace configuration files for modification. Instantiates one
497    /// if `config` has no workspace configuration layers.
498    ///
499    /// If the workspace path is unknown, this function returns an empty `Vec`.
500    /// Since the workspace config path cannot be a directory, the returned
501    /// `Vec` should have at most one config file.
502    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    /// Loads workspace-specific config file into the given `config`. The old
518    /// workspace-config layer will be replaced if any.
519    #[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    /// Resolves conditional scopes within the current environment. Returns new
529    /// resolved config.
530    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
559/// Initializes stacked config with the given `default_layers` and infallible
560/// sources.
561///
562/// Sources from the lowest precedence:
563/// 1. Default
564/// 2. Base environment variables
565/// 3. [User configs](https://docs.jj-vcs.dev/latest/config/)
566/// 4. Repo config `.jj/repo/config.toml`
567/// 5. Workspace config `.jj/workspace-config.toml`
568/// 6. Override environment variables
569/// 7. Command-line arguments `--config` and `--config-file`
570///
571/// This function sets up 1, 2, and 6.
572pub 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
583/// Environment variables that should be overridden by config values
584fn 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        // On Unix, $USER is set by login(1). Use it as a fallback because
597        // getpwuid() of musl libc appears not (fully?) supporting nsswitch.
598        layer.set_value(OP_USERNAME, value).unwrap();
599    }
600    if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
601        // "User-level configuration files and per-instance command-line arguments
602        // should override $NO_COLOR." https://no-color.org/
603        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    // Intentionally NOT respecting $PAGER here as it often creates a bad
611    // out-of-the-box experience for users, see http://github.com/jj-vcs/jj/issues/3502.
612    layer
613}
614
615pub fn default_config_layers() -> Vec<ConfigLayer> {
616    // Syntax error in default config isn't a user error. That's why defaults are
617    // loaded by separate builder.
618    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
636/// Environment variables that override config values
637fn 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/// Configuration source/data type provided as command-line argument.
667#[derive(Clone, Copy, Debug, Eq, PartialEq)]
668pub enum ConfigArgKind {
669    /// `--config=NAME=VALUE`
670    Item,
671    /// `--config-file=PATH`
672    File,
673}
674
675/// Parses `--config*` arguments.
676pub 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                    // Can fail depending on the argument order, but that
688                    // wouldn't matter in practice.
689                    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
705/// Parses `NAME=VALUE` string.
706fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
707    // split NAME=VALUE at the first parsable position
708    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
723/// List of rules to migrate deprecated config variables.
724pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
725    vec![
726        // TODO: Delete in jj 0.38.0+
727        ConfigMigrationRule::rename_value("core.fsmonitor", "fsmonitor.backend"),
728        // TODO: Delete in jj 0.38.0+
729        ConfigMigrationRule::rename_value(
730            "core.watchman.register-snapshot-trigger",
731            "fsmonitor.watchman.register-snapshot-trigger",
732        ),
733        // TODO: Delete in jj 0.42.0+
734        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        // TODO: Delete in jj 0.42.0+
750        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/// Command name and arguments specified by config.
769#[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    /// Returns command name without arguments.
782    pub fn split_name(&self) -> Cow<'_, str> {
783        let (name, _) = self.split_name_and_args();
784        name
785    }
786
787    /// Returns command name and arguments.
788    ///
789    /// The command name may be an empty string (as well as each argument.)
790    pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
791        match self {
792            Self::String(s) => {
793                // Handle things like `EDITOR=emacs -nw` (TODO: parse shell escapes)
794                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    /// Returns command string only if the underlying type is a string.
806    ///
807    /// Use this to parse enum strings such as `":builtin"`, which can be
808    /// escaped as `[":builtin"]`.
809    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    /// Returns process builder configured with this.
817    pub fn to_command(&self) -> Command {
818        let empty: HashMap<&str, &str> = HashMap::new();
819        self.to_command_with_variables(&empty)
820    }
821
822    /// Returns process builder configured with this after interpolating
823    /// variables into the arguments.
824    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            // TODO: format with shell escapes
849            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
860// Not interested in $UPPER_CASE_VARIABLES
861static 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
885/// Return all variable names found in the args, without the dollar sign
886pub 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/// Wrapper to reject an array without command name.
897// Based on https://github.com/serde-rs/serde/issues/939
898#[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        // Suppress Decor { .. } which is uninteresting
928        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        // Value in TOML syntax
937        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        // Bare string
945        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        // Error in TOML value
959        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        // The value parser is sensitive to leading whitespaces, which seems
974        // good because the parsing falls back to a bare string.
975        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        // This one is a bit cryptic, but b=c can be a bare string.
990        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        // Note: "email" is alphabetized, before "name" from same layer.
1099        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        // a.b.* is shadowed by a.b
1300        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        // a.b is shadowed by a.b.*
1318        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        // a.b.d is shadowed by a.b.d
1332        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        // a.b.* is shadowed by a.b, which is shadowed by a.b.*
1354        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            // Precedence order is important
1642            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}