Skip to main content

jj_cli/
config.rs

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