Skip to main content

jj_cli/
config.rs

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