Skip to main content

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