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