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