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