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        // TODO: Delete in jj 0.42.0+
828        ConfigMigrationRule::custom(
829            |layer| {
830                let Ok(Some(val)) = layer.look_up_item("git.auto-local-bookmark") else {
831                    return false;
832                };
833                val.as_bool().is_some_and(|b| b)
834            },
835            |_| {
836                Ok("`git.auto-local-bookmark` is deprecated; use \
837                    `remotes.<name>.auto-track-bookmarks` instead.
838Example: jj config set --user remotes.origin.auto-track-bookmarks '*'
839For details, see: https://docs.jj-vcs.dev/latest/config/#automatic-tracking-of-bookmarks"
840                    .into())
841            },
842        ),
843        // TODO: Delete in jj 0.42.0+
844        ConfigMigrationRule::custom(
845            |layer| {
846                let Ok(Some(val)) = layer.look_up_item("git.push-new-bookmarks") else {
847                    return false;
848                };
849                val.as_bool().is_some_and(|b| b)
850            },
851            |_| {
852                Ok("`git.push-new-bookmarks` is deprecated; use \
853                    `remotes.<name>.auto-track-bookmarks` instead.
854Example: jj config set --user remotes.origin.auto-track-bookmarks '*'
855For details, see: https://docs.jj-vcs.dev/latest/config/#automatic-tracking-of-bookmarks"
856                    .into())
857            },
858        ),
859    ]
860}
861
862/// Command name and arguments specified by config.
863#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
864#[serde(untagged)]
865pub enum CommandNameAndArgs {
866    String(String),
867    Vec(NonEmptyCommandArgsVec),
868    Structured {
869        env: HashMap<String, String>,
870        command: NonEmptyCommandArgsVec,
871    },
872}
873
874impl CommandNameAndArgs {
875    /// Returns command name without arguments.
876    pub fn split_name(&self) -> Cow<'_, str> {
877        let (name, _) = self.split_name_and_args();
878        name
879    }
880
881    /// Returns command name and arguments.
882    ///
883    /// The command name may be an empty string (as well as each argument.)
884    pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
885        match self {
886            Self::String(s) => {
887                if s.contains('"') || s.contains('\'') {
888                    let mut parts = shlex::Shlex::new(s);
889                    let res = (
890                        parts.next().unwrap_or_default().into(),
891                        parts.by_ref().collect(),
892                    );
893                    if !parts.had_error {
894                        return res;
895                    }
896                }
897                let mut args = s.split(' ').map(|s| s.to_owned());
898                (args.next().unwrap().into(), args.collect())
899            }
900            Self::Vec(NonEmptyCommandArgsVec(a)) => (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])),
901            Self::Structured {
902                env: _,
903                command: cmd,
904            } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
905        }
906    }
907
908    /// Returns command string only if the underlying type is a string.
909    ///
910    /// Use this to parse enum strings such as `":builtin"`, which can be
911    /// escaped as `[":builtin"]`.
912    pub fn as_str(&self) -> Option<&str> {
913        match self {
914            Self::String(s) => Some(s),
915            Self::Vec(_) | Self::Structured { .. } => None,
916        }
917    }
918
919    /// Returns process builder configured with this.
920    pub fn to_command(&self) -> Command {
921        let empty: HashMap<&str, &str> = HashMap::new();
922        self.to_command_with_variables(&empty)
923    }
924
925    /// Returns process builder configured with this after interpolating
926    /// variables into the arguments.
927    pub fn to_command_with_variables<V: AsRef<str>>(
928        &self,
929        variables: &HashMap<&str, V>,
930    ) -> Command {
931        let (name, args) = self.split_name_and_args();
932        let mut cmd = Command::new(interpolate_variables_single(name.as_ref(), variables));
933        if let Self::Structured { env, .. } = self {
934            cmd.envs(env);
935        }
936        cmd.args(interpolate_variables(&args, variables));
937        cmd
938    }
939}
940
941impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
942    fn from(s: &T) -> Self {
943        Self::String(s.as_ref().to_owned())
944    }
945}
946
947impl fmt::Display for CommandNameAndArgs {
948    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
949        match self {
950            Self::String(s) => write!(f, "{s}"),
951            // TODO: format with shell escapes
952            Self::Vec(a) => write!(f, "{}", a.0.join(" ")),
953            Self::Structured { env, command } => {
954                for (k, v) in env {
955                    write!(f, "{k}={v} ")?;
956                }
957                write!(f, "{}", command.0.join(" "))
958            }
959        }
960    }
961}
962
963pub fn load_aliases_map<P>(
964    ui: &Ui,
965    config: &StackedConfig,
966    table_name: &ConfigNamePathBuf,
967) -> Result<AliasesMap<P, String>, CommandError>
968where
969    P: AliasDeclarationParser + Default,
970    P::Error: fmt::Display,
971{
972    let mut aliases_map = AliasesMap::new();
973    // Load from all config layers in order. 'f(x)' in default layer should be
974    // overridden by 'f(a)' in user.
975    for layer in config.layers() {
976        let table = match layer.look_up_table(table_name) {
977            Ok(Some(table)) => table,
978            Ok(None) => continue,
979            Err(item) => {
980                return Err(ConfigGetError::Type {
981                    name: table_name.to_string(),
982                    error: format!("Expected a table, but is {}", item.type_name()).into(),
983                    source_path: layer.path.clone(),
984                }
985                .into());
986            }
987        };
988        for (decl, item) in table.iter() {
989            let r = item
990                .as_str()
991                .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
992                .and_then(|v| aliases_map.insert(decl, v).map_err(|e| format!("{e}")));
993            if let Err(s) = r {
994                writeln!(
995                    ui.warning_default(),
996                    "Failed to load `{table_name}.{decl}`: {s}"
997                )?;
998            }
999        }
1000    }
1001    Ok(aliases_map)
1002}
1003
1004// Not interested in $UPPER_CASE_VARIABLES
1005static VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
1006
1007pub fn interpolate_variables<V: AsRef<str>>(
1008    args: &[String],
1009    variables: &HashMap<&str, V>,
1010) -> Vec<String> {
1011    args.iter()
1012        .map(|arg| interpolate_variables_single(arg, variables))
1013        .collect()
1014}
1015
1016fn interpolate_variables_single<V: AsRef<str>>(arg: &str, variables: &HashMap<&str, V>) -> String {
1017    VARIABLE_REGEX
1018        .replace_all(arg, |caps: &Captures| {
1019            let name = &caps[1];
1020            if let Some(subst) = variables.get(name) {
1021                subst.as_ref().to_owned()
1022            } else {
1023                caps[0].to_owned()
1024            }
1025        })
1026        .into_owned()
1027}
1028
1029/// Return all variable names found in the args, without the dollar sign
1030pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
1031    let regex = &*VARIABLE_REGEX;
1032    args.iter()
1033        .flat_map(|arg| regex.find_iter(arg))
1034        .map(|single_match| {
1035            let s = single_match.as_str();
1036            &s[1..]
1037        })
1038}
1039
1040/// Wrapper to reject an array without command name.
1041// Based on https://github.com/serde-rs/serde/issues/939
1042#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
1043#[serde(try_from = "Vec<String>")]
1044pub struct NonEmptyCommandArgsVec(Vec<String>);
1045
1046impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
1047    type Error = &'static str;
1048
1049    fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
1050        if args.is_empty() {
1051            Err("command arguments should not be empty")
1052        } else {
1053            Ok(Self(args))
1054        }
1055    }
1056}
1057
1058#[cfg(test)]
1059mod tests {
1060    use std::env::join_paths;
1061    use std::fmt::Write as _;
1062
1063    use indoc::indoc;
1064    use maplit::hashmap;
1065    use test_case::test_case;
1066    use testutils::TestResult;
1067
1068    use super::*;
1069
1070    fn insta_settings() -> insta::Settings {
1071        let mut settings = insta::Settings::clone_current();
1072        // Suppress Decor { .. } which is uninteresting
1073        settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
1074        settings
1075    }
1076
1077    #[test]
1078    fn test_parse_value_or_bare_string() -> TestResult {
1079        let parse = |s: &str| parse_value_or_bare_string(s);
1080
1081        // Value in TOML syntax
1082        assert_eq!(parse("true")?.as_bool(), Some(true));
1083        assert_eq!(parse("42")?.as_integer(), Some(42));
1084        assert_eq!(parse("-1")?.as_integer(), Some(-1));
1085        assert_eq!(parse("'a'")?.as_str(), Some("a"));
1086        assert!(parse("[]")?.is_array());
1087        assert!(parse("{ a = 'b' }")?.is_inline_table());
1088
1089        // Bare string
1090        assert_eq!(parse("")?.as_str(), Some(""));
1091        assert_eq!(parse("John Doe")?.as_str(), Some("John Doe"));
1092        assert_eq!(parse("Doe, John")?.as_str(), Some("Doe, John"));
1093        assert_eq!(parse("It's okay")?.as_str(), Some("It's okay"));
1094        assert_eq!(
1095            parse("<foo+bar@example.org>")?.as_str(),
1096            Some("<foo+bar@example.org>")
1097        );
1098        assert_eq!(parse("#ff00aa")?.as_str(), Some("#ff00aa"));
1099        assert_eq!(parse("all()")?.as_str(), Some("all()"));
1100        assert_eq!(parse("glob:*.*")?.as_str(), Some("glob:*.*"));
1101        assert_eq!(parse("柔術")?.as_str(), Some("柔術"));
1102
1103        // Error in TOML value
1104        assert!(parse("'foo").is_err());
1105        assert!(parse(r#" bar" "#).is_err());
1106        assert!(parse("[0 1]").is_err());
1107        assert!(parse("{ x = y }").is_err());
1108        assert!(parse("\n { x").is_err());
1109        assert!(parse(" x ] ").is_err());
1110        assert!(parse("[table]\nkey = 'value'").is_err());
1111        Ok(())
1112    }
1113
1114    #[test]
1115    fn test_parse_config_arg_item() {
1116        assert!(parse_config_arg_item("").is_err());
1117        assert!(parse_config_arg_item("a").is_err());
1118        assert!(parse_config_arg_item("=").is_err());
1119        // The value parser is sensitive to leading whitespaces, which seems
1120        // good because the parsing falls back to a bare string.
1121        assert!(parse_config_arg_item("a = 'b'").is_err());
1122
1123        let (name, value) = parse_config_arg_item("a=b").unwrap();
1124        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1125        assert_eq!(value.as_str(), Some("b"));
1126
1127        let (name, value) = parse_config_arg_item("a=").unwrap();
1128        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1129        assert_eq!(value.as_str(), Some(""));
1130
1131        let (name, value) = parse_config_arg_item("a= ").unwrap();
1132        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1133        assert_eq!(value.as_str(), Some(" "));
1134
1135        // This one is a bit cryptic, but b=c can be a bare string.
1136        let (name, value) = parse_config_arg_item("a=b=c").unwrap();
1137        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1138        assert_eq!(value.as_str(), Some("b=c"));
1139
1140        let (name, value) = parse_config_arg_item("a.b=true").unwrap();
1141        assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
1142        assert_eq!(value.as_bool(), Some(true));
1143
1144        let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
1145        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1146        assert_eq!(value.as_str(), Some("b=c"));
1147
1148        let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
1149        assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
1150        assert_eq!(value.as_str(), Some("c"));
1151
1152        let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
1153        assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
1154        assert!(value.is_inline_table());
1155        assert_eq!(value.to_string(), "{d = 'e=f'}");
1156    }
1157
1158    #[test]
1159    fn test_command_args() -> TestResult {
1160        let mut config = StackedConfig::empty();
1161        config.add_layer(ConfigLayer::parse(
1162            ConfigSource::User,
1163            indoc! {"
1164                    empty_array = []
1165                    empty_string = ''
1166                    array = ['emacs', '-nw']
1167                    string = 'emacs -nw'
1168                    string_quoted = '\"spaced path/to/emacs\" -nw'
1169                    structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1170                    structured.command = ['emacs', '-nw']
1171                "},
1172        )?);
1173
1174        assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1175
1176        let command_args: CommandNameAndArgs = config.get("empty_string")?;
1177        assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1178        let (name, args) = command_args.split_name_and_args();
1179        assert_eq!(name, "");
1180        assert!(args.is_empty());
1181
1182        let command_args: CommandNameAndArgs = config.get("array")?;
1183        assert_eq!(
1184            command_args,
1185            CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1186                ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1187            ))
1188        );
1189        let (name, args) = command_args.split_name_and_args();
1190        assert_eq!(name, "emacs");
1191        assert_eq!(args, ["-nw"].as_ref());
1192
1193        let command_args: CommandNameAndArgs = config.get("string")?;
1194        assert_eq!(
1195            command_args,
1196            CommandNameAndArgs::String("emacs -nw".to_owned())
1197        );
1198        let (name, args) = command_args.split_name_and_args();
1199        assert_eq!(name, "emacs");
1200        assert_eq!(args, ["-nw"].as_ref());
1201
1202        let command_args: CommandNameAndArgs = config.get("string_quoted")?;
1203        assert_eq!(
1204            command_args,
1205            CommandNameAndArgs::String("\"spaced path/to/emacs\" -nw".to_owned())
1206        );
1207        let (name, args) = command_args.split_name_and_args();
1208        assert_eq!(name, "spaced path/to/emacs");
1209        assert_eq!(args, ["-nw"].as_ref());
1210
1211        let command_args: CommandNameAndArgs = config.get("structured")?;
1212        assert_eq!(
1213            command_args,
1214            CommandNameAndArgs::Structured {
1215                env: hashmap! {
1216                    "KEY1".to_string() => "value1".to_string(),
1217                    "KEY2".to_string() => "value2".to_string(),
1218                },
1219                command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1220            }
1221        );
1222        let (name, args) = command_args.split_name_and_args();
1223        assert_eq!(name, "emacs");
1224        assert_eq!(args, ["-nw"].as_ref());
1225        Ok(())
1226    }
1227
1228    #[test]
1229    fn test_resolved_config_values_empty() {
1230        let config = StackedConfig::empty();
1231        assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
1232    }
1233
1234    #[test]
1235    fn test_resolved_config_values_single_key() -> TestResult {
1236        let settings = insta_settings();
1237        let _guard = settings.bind_to_scope();
1238        let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
1239        env_base_layer.set_value("user.name", "base-user-name")?;
1240        env_base_layer.set_value("user.email", "base@user.email")?;
1241        let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1242        repo_layer.set_value("user.email", "repo@user.email")?;
1243        let mut config = StackedConfig::empty();
1244        config.add_layer(env_base_layer);
1245        config.add_layer(repo_layer);
1246        // Note: "email" is alphabetized, before "name" from same layer.
1247        insta::assert_debug_snapshot!(
1248            resolved_config_values(&config, &ConfigNamePathBuf::root()),
1249            @r#"
1250        [
1251            AnnotatedValue {
1252                name: ConfigNamePathBuf(
1253                    [
1254                        Key {
1255                            key: "user",
1256                            repr: None,
1257                            leaf_decor: Decor { .. },
1258                            dotted_decor: Decor { .. },
1259                        },
1260                        Key {
1261                            key: "name",
1262                            repr: None,
1263                            leaf_decor: Decor { .. },
1264                            dotted_decor: Decor { .. },
1265                        },
1266                    ],
1267                ),
1268                value: String(
1269                    Formatted {
1270                        value: "base-user-name",
1271                        repr: "default",
1272                        decor: Decor { .. },
1273                    },
1274                ),
1275                source: EnvBase,
1276                path: None,
1277                is_overridden: false,
1278            },
1279            AnnotatedValue {
1280                name: ConfigNamePathBuf(
1281                    [
1282                        Key {
1283                            key: "user",
1284                            repr: None,
1285                            leaf_decor: Decor { .. },
1286                            dotted_decor: Decor { .. },
1287                        },
1288                        Key {
1289                            key: "email",
1290                            repr: None,
1291                            leaf_decor: Decor { .. },
1292                            dotted_decor: Decor { .. },
1293                        },
1294                    ],
1295                ),
1296                value: String(
1297                    Formatted {
1298                        value: "base@user.email",
1299                        repr: "default",
1300                        decor: Decor { .. },
1301                    },
1302                ),
1303                source: EnvBase,
1304                path: None,
1305                is_overridden: true,
1306            },
1307            AnnotatedValue {
1308                name: ConfigNamePathBuf(
1309                    [
1310                        Key {
1311                            key: "user",
1312                            repr: None,
1313                            leaf_decor: Decor { .. },
1314                            dotted_decor: Decor { .. },
1315                        },
1316                        Key {
1317                            key: "email",
1318                            repr: None,
1319                            leaf_decor: Decor { .. },
1320                            dotted_decor: Decor { .. },
1321                        },
1322                    ],
1323                ),
1324                value: String(
1325                    Formatted {
1326                        value: "repo@user.email",
1327                        repr: "default",
1328                        decor: Decor { .. },
1329                    },
1330                ),
1331                source: Repo,
1332                path: None,
1333                is_overridden: false,
1334            },
1335        ]
1336        "#
1337        );
1338        Ok(())
1339    }
1340
1341    #[test]
1342    fn test_resolved_config_values_filter_path() -> TestResult {
1343        let settings = insta_settings();
1344        let _guard = settings.bind_to_scope();
1345        let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1346        user_layer.set_value("test-table1.foo", "user-FOO")?;
1347        user_layer.set_value("test-table2.bar", "user-BAR")?;
1348        let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1349        repo_layer.set_value("test-table1.bar", "repo-BAR")?;
1350        let mut config = StackedConfig::empty();
1351        config.add_layer(user_layer);
1352        config.add_layer(repo_layer);
1353        insta::assert_debug_snapshot!(
1354            resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1355            @r#"
1356        [
1357            AnnotatedValue {
1358                name: ConfigNamePathBuf(
1359                    [
1360                        Key {
1361                            key: "test-table1",
1362                            repr: None,
1363                            leaf_decor: Decor { .. },
1364                            dotted_decor: Decor { .. },
1365                        },
1366                        Key {
1367                            key: "foo",
1368                            repr: None,
1369                            leaf_decor: Decor { .. },
1370                            dotted_decor: Decor { .. },
1371                        },
1372                    ],
1373                ),
1374                value: String(
1375                    Formatted {
1376                        value: "user-FOO",
1377                        repr: "default",
1378                        decor: Decor { .. },
1379                    },
1380                ),
1381                source: User,
1382                path: None,
1383                is_overridden: false,
1384            },
1385            AnnotatedValue {
1386                name: ConfigNamePathBuf(
1387                    [
1388                        Key {
1389                            key: "test-table1",
1390                            repr: None,
1391                            leaf_decor: Decor { .. },
1392                            dotted_decor: Decor { .. },
1393                        },
1394                        Key {
1395                            key: "bar",
1396                            repr: None,
1397                            leaf_decor: Decor { .. },
1398                            dotted_decor: Decor { .. },
1399                        },
1400                    ],
1401                ),
1402                value: String(
1403                    Formatted {
1404                        value: "repo-BAR",
1405                        repr: "default",
1406                        decor: Decor { .. },
1407                    },
1408                ),
1409                source: Repo,
1410                path: None,
1411                is_overridden: false,
1412            },
1413        ]
1414        "#
1415        );
1416        Ok(())
1417    }
1418
1419    #[test]
1420    fn test_resolved_config_values_overridden() -> TestResult {
1421        let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1422            let mut config = StackedConfig::empty();
1423            config.extend_layers(layers.iter().copied().cloned());
1424            let prefix = if prefix.is_empty() {
1425                ConfigNamePathBuf::root()
1426            } else {
1427                prefix.parse().unwrap()
1428            };
1429            let mut output = String::new();
1430            for annotated in resolved_config_values(&config, &prefix) {
1431                let AnnotatedValue { name, value, .. } = &annotated;
1432                let sigil = if annotated.is_overridden { '!' } else { ' ' };
1433                writeln!(output, "{sigil}{name} = {value}").unwrap();
1434            }
1435            output
1436        };
1437
1438        let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1439        layer0.set_value("a.b.e", "0.0")?;
1440        layer0.set_value("a.b.c.f", "0.1")?;
1441        layer0.set_value("a.b.d", "0.2")?;
1442        let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1443        layer1.set_value("a.b", "1.0")?;
1444        layer1.set_value("a.c", "1.1")?;
1445        let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1446        layer2.set_value("a.b.g", "2.0")?;
1447        layer2.set_value("a.b.d", "2.1")?;
1448
1449        // a.b.* is shadowed by a.b
1450        let layers = [&layer0, &layer1];
1451        insta::assert_snapshot!(list(&layers, ""), @r#"
1452        !a.b.e = "0.0"
1453        !a.b.c.f = "0.1"
1454        !a.b.d = "0.2"
1455         a.b = "1.0"
1456         a.c = "1.1"
1457        "#);
1458        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1459        !a.b.e = "0.0"
1460        !a.b.c.f = "0.1"
1461        !a.b.d = "0.2"
1462         a.b = "1.0"
1463        "#);
1464        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1465        insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1466
1467        // a.b is shadowed by a.b.*
1468        let layers = [&layer1, &layer2];
1469        insta::assert_snapshot!(list(&layers, ""), @r#"
1470        !a.b = "1.0"
1471         a.c = "1.1"
1472         a.b.g = "2.0"
1473         a.b.d = "2.1"
1474        "#);
1475        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1476        !a.b = "1.0"
1477         a.b.g = "2.0"
1478         a.b.d = "2.1"
1479        "#);
1480
1481        // a.b.d is shadowed by a.b.d
1482        let layers = [&layer0, &layer2];
1483        insta::assert_snapshot!(list(&layers, ""), @r#"
1484         a.b.e = "0.0"
1485         a.b.c.f = "0.1"
1486        !a.b.d = "0.2"
1487         a.b.g = "2.0"
1488         a.b.d = "2.1"
1489        "#);
1490        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1491         a.b.e = "0.0"
1492         a.b.c.f = "0.1"
1493        !a.b.d = "0.2"
1494         a.b.g = "2.0"
1495         a.b.d = "2.1"
1496        "#);
1497        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1498        insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1499        !a.b.d = "0.2"
1500         a.b.d = "2.1"
1501        "#);
1502
1503        // a.b.* is shadowed by a.b, which is shadowed by a.b.*
1504        let layers = [&layer0, &layer1, &layer2];
1505        insta::assert_snapshot!(list(&layers, ""), @r#"
1506        !a.b.e = "0.0"
1507        !a.b.c.f = "0.1"
1508        !a.b.d = "0.2"
1509        !a.b = "1.0"
1510         a.c = "1.1"
1511         a.b.g = "2.0"
1512         a.b.d = "2.1"
1513        "#);
1514        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1515        !a.b.e = "0.0"
1516        !a.b.c.f = "0.1"
1517        !a.b.d = "0.2"
1518        !a.b = "1.0"
1519         a.b.g = "2.0"
1520         a.b.d = "2.1"
1521        "#);
1522        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1523        Ok(())
1524    }
1525
1526    struct TestCase {
1527        files: &'static [&'static str],
1528        env: UnresolvedConfigEnv,
1529        wants: Vec<Want>,
1530    }
1531
1532    #[derive(Debug)]
1533    enum WantState {
1534        New,
1535        Existing,
1536    }
1537    #[derive(Debug)]
1538    struct Want {
1539        path: &'static str,
1540        state: WantState,
1541    }
1542
1543    impl Want {
1544        const fn new(path: &'static str) -> Self {
1545            Self {
1546                path,
1547                state: WantState::New,
1548            }
1549        }
1550
1551        const fn existing(path: &'static str) -> Self {
1552            Self {
1553                path,
1554                state: WantState::Existing,
1555            }
1556        }
1557
1558        fn rooted_path(&self, root: &Path) -> PathBuf {
1559            root.join(self.path)
1560        }
1561
1562        fn exists(&self) -> bool {
1563            matches!(self.state, WantState::Existing)
1564        }
1565    }
1566
1567    fn config_path_home_existing() -> TestCase {
1568        TestCase {
1569            files: &["home/.jjconfig.toml"],
1570            env: UnresolvedConfigEnv {
1571                home_dir: Some("home".into()),
1572                ..Default::default()
1573            },
1574            wants: vec![Want::existing("home/.jjconfig.toml")],
1575        }
1576    }
1577
1578    fn config_path_home_new() -> TestCase {
1579        TestCase {
1580            files: &[],
1581            env: UnresolvedConfigEnv {
1582                home_dir: Some("home".into()),
1583                ..Default::default()
1584            },
1585            wants: vec![Want::new("home/.jjconfig.toml")],
1586        }
1587    }
1588
1589    fn config_path_home_existing_platform_new() -> TestCase {
1590        TestCase {
1591            files: &["home/.jjconfig.toml"],
1592            env: UnresolvedConfigEnv {
1593                home_dir: Some("home".into()),
1594                config_dir: Some("config".into()),
1595                ..Default::default()
1596            },
1597            wants: vec![
1598                Want::existing("home/.jjconfig.toml"),
1599                Want::new("config/jj/config.toml"),
1600            ],
1601        }
1602    }
1603
1604    fn config_path_platform_existing() -> TestCase {
1605        TestCase {
1606            files: &["config/jj/config.toml"],
1607            env: UnresolvedConfigEnv {
1608                home_dir: Some("home".into()),
1609                config_dir: Some("config".into()),
1610                ..Default::default()
1611            },
1612            wants: vec![Want::existing("config/jj/config.toml")],
1613        }
1614    }
1615
1616    fn config_path_platform_new() -> TestCase {
1617        TestCase {
1618            files: &[],
1619            env: UnresolvedConfigEnv {
1620                config_dir: Some("config".into()),
1621                ..Default::default()
1622            },
1623            wants: vec![Want::new("config/jj/config.toml")],
1624        }
1625    }
1626
1627    fn config_path_new_prefer_platform() -> TestCase {
1628        TestCase {
1629            files: &[],
1630            env: UnresolvedConfigEnv {
1631                home_dir: Some("home".into()),
1632                config_dir: Some("config".into()),
1633                ..Default::default()
1634            },
1635            wants: vec![Want::new("config/jj/config.toml")],
1636        }
1637    }
1638
1639    fn config_path_jj_config_existing() -> TestCase {
1640        TestCase {
1641            files: &["custom.toml"],
1642            env: UnresolvedConfigEnv {
1643                jj_config: Some("custom.toml".into()),
1644                ..Default::default()
1645            },
1646            wants: vec![Want::existing("custom.toml")],
1647        }
1648    }
1649
1650    fn config_path_jj_config_new() -> TestCase {
1651        TestCase {
1652            files: &[],
1653            env: UnresolvedConfigEnv {
1654                jj_config: Some("custom.toml".into()),
1655                ..Default::default()
1656            },
1657            wants: vec![Want::new("custom.toml")],
1658        }
1659    }
1660
1661    fn config_path_jj_config_existing_multiple() -> TestCase {
1662        TestCase {
1663            files: &["custom1.toml", "custom2.toml"],
1664            env: UnresolvedConfigEnv {
1665                jj_config: Some(
1666                    join_paths(["custom1.toml", "custom2.toml"])
1667                        .unwrap()
1668                        .into_string()
1669                        .unwrap(),
1670                ),
1671                ..Default::default()
1672            },
1673            wants: vec![
1674                Want::existing("custom1.toml"),
1675                Want::existing("custom2.toml"),
1676            ],
1677        }
1678    }
1679
1680    fn config_path_jj_config_new_multiple() -> TestCase {
1681        TestCase {
1682            files: &["custom1.toml"],
1683            env: UnresolvedConfigEnv {
1684                jj_config: Some(
1685                    join_paths(["custom1.toml", "custom2.toml"])
1686                        .unwrap()
1687                        .into_string()
1688                        .unwrap(),
1689                ),
1690                ..Default::default()
1691            },
1692            wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1693        }
1694    }
1695
1696    fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1697        TestCase {
1698            files: &["custom1.toml"],
1699            env: UnresolvedConfigEnv {
1700                jj_config: Some(
1701                    join_paths(["custom1.toml", "", "custom2.toml"])
1702                        .unwrap()
1703                        .into_string()
1704                        .unwrap(),
1705                ),
1706                ..Default::default()
1707            },
1708            wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1709        }
1710    }
1711
1712    fn config_path_jj_config_empty() -> TestCase {
1713        TestCase {
1714            files: &[],
1715            env: UnresolvedConfigEnv {
1716                jj_config: Some("".to_owned()),
1717                ..Default::default()
1718            },
1719            wants: vec![],
1720        }
1721    }
1722
1723    fn config_path_config_pick_platform() -> TestCase {
1724        TestCase {
1725            files: &["config/jj/config.toml"],
1726            env: UnresolvedConfigEnv {
1727                home_dir: Some("home".into()),
1728                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_config_pick_home() -> TestCase {
1736        TestCase {
1737            files: &["home/.jjconfig.toml"],
1738            env: UnresolvedConfigEnv {
1739                home_dir: Some("home".into()),
1740                config_dir: Some("config".into()),
1741                ..Default::default()
1742            },
1743            wants: vec![
1744                Want::existing("home/.jjconfig.toml"),
1745                Want::new("config/jj/config.toml"),
1746            ],
1747        }
1748    }
1749
1750    fn config_path_platform_new_conf_dir_existing() -> TestCase {
1751        TestCase {
1752            files: &["config/jj/conf.d/_"],
1753            env: UnresolvedConfigEnv {
1754                home_dir: Some("home".into()),
1755                config_dir: Some("config".into()),
1756                ..Default::default()
1757            },
1758            wants: vec![
1759                Want::new("config/jj/config.toml"),
1760                Want::existing("config/jj/conf.d"),
1761            ],
1762        }
1763    }
1764
1765    fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1766        TestCase {
1767            files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1768            env: UnresolvedConfigEnv {
1769                home_dir: Some("home".into()),
1770                config_dir: Some("config".into()),
1771                ..Default::default()
1772            },
1773            wants: vec![
1774                Want::existing("config/jj/config.toml"),
1775                Want::existing("config/jj/conf.d"),
1776            ],
1777        }
1778    }
1779
1780    fn config_path_all_existing() -> TestCase {
1781        TestCase {
1782            files: &[
1783                "config/jj/conf.d/_",
1784                "config/jj/config.toml",
1785                "home/.jjconfig.toml",
1786            ],
1787            env: UnresolvedConfigEnv {
1788                home_dir: Some("home".into()),
1789                config_dir: Some("config".into()),
1790                ..Default::default()
1791            },
1792            // Precedence order is important
1793            wants: vec![
1794                Want::existing("home/.jjconfig.toml"),
1795                Want::existing("config/jj/config.toml"),
1796                Want::existing("config/jj/conf.d"),
1797            ],
1798        }
1799    }
1800
1801    fn config_path_none() -> TestCase {
1802        TestCase {
1803            files: &[],
1804            env: Default::default(),
1805            wants: vec![],
1806        }
1807    }
1808
1809    #[test_case(config_path_home_existing())]
1810    #[test_case(config_path_home_new())]
1811    #[test_case(config_path_home_existing_platform_new())]
1812    #[test_case(config_path_platform_existing())]
1813    #[test_case(config_path_platform_new())]
1814    #[test_case(config_path_new_prefer_platform())]
1815    #[test_case(config_path_jj_config_existing())]
1816    #[test_case(config_path_jj_config_new())]
1817    #[test_case(config_path_jj_config_existing_multiple())]
1818    #[test_case(config_path_jj_config_new_multiple())]
1819    #[test_case(config_path_jj_config_empty_paths_filtered())]
1820    #[test_case(config_path_jj_config_empty())]
1821    #[test_case(config_path_config_pick_platform())]
1822    #[test_case(config_path_config_pick_home())]
1823    #[test_case(config_path_platform_new_conf_dir_existing())]
1824    #[test_case(config_path_platform_existing_conf_dir_existing())]
1825    #[test_case(config_path_all_existing())]
1826    #[test_case(config_path_none())]
1827    fn test_config_path(case: TestCase) {
1828        let tmp = setup_config_fs(case.files);
1829        let env = resolve_config_env(&case.env, tmp.path());
1830
1831        let all_expected_paths = case
1832            .wants
1833            .iter()
1834            .map(|w| w.rooted_path(tmp.path()))
1835            .collect_vec();
1836        let exists_expected_paths = case
1837            .wants
1838            .iter()
1839            .filter(|w| w.exists())
1840            .map(|w| w.rooted_path(tmp.path()))
1841            .collect_vec();
1842
1843        let all_paths = env.user_config_paths().collect_vec();
1844        let exists_paths = env.existing_user_config_paths().collect_vec();
1845
1846        assert_eq!(all_paths, all_expected_paths);
1847        assert_eq!(exists_paths, exists_expected_paths);
1848    }
1849
1850    fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1851        let tmp = testutils::new_temp_dir();
1852        for file in files {
1853            let path = tmp.path().join(file);
1854            if let Some(parent) = path.parent() {
1855                std::fs::create_dir_all(parent).unwrap();
1856            }
1857            std::fs::File::create(path).unwrap();
1858        }
1859        tmp
1860    }
1861
1862    fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1863        let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1864        let env = UnresolvedConfigEnv {
1865            config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1866            home_dir: home_dir.clone(),
1867            jj_config: env.jj_config.as_ref().map(|p| {
1868                join_paths(split_paths(p).map(|p| {
1869                    if p.as_os_str().is_empty() {
1870                        return p;
1871                    }
1872                    root.join(p)
1873                }))
1874                .unwrap()
1875                .into_string()
1876                .unwrap()
1877            }),
1878        };
1879        ConfigEnv {
1880            home_dir,
1881            root_config_dir: None,
1882            repo_path: None,
1883            workspace_path: None,
1884            user_config_paths: env.resolve(),
1885            repo_config: None,
1886            workspace_config: None,
1887            command: None,
1888            hostname: None,
1889            environment: HashMap::new(),
1890            rng: Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(0))),
1891        }
1892    }
1893}