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