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;
48use crate::ui::Ui;
49
50// TODO(#879): Consider generating entire schema dynamically vs. static file.
51pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");
52
53/// Parses a TOML value expression. Interprets the given value as string if it
54/// can't be parsed and doesn't look like a TOML expression.
55pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> {
56    match value_str.parse() {
57        Ok(value) => Ok(value),
58        Err(_) if is_bare_string(value_str) => Ok(value_str.into()),
59        Err(err) => Err(err),
60    }
61}
62
63fn is_bare_string(value_str: &str) -> bool {
64    // leading whitespace isn't ignored when parsing TOML value expression, but
65    // "\n[]" doesn't look like a bare string.
66    let trimmed = value_str.trim_ascii().as_bytes();
67    if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {
68        // string, array, or table constructs?
69        !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')
70    } else {
71        true // empty or whitespace only
72    }
73}
74
75/// Converts [`ConfigValue`] (or [`toml_edit::Value`]) to [`toml::Value`] which
76/// implements [`serde::Serialize`].
77pub fn to_serializable_value(value: ConfigValue) -> toml::Value {
78    match value {
79        ConfigValue::String(v) => toml::Value::String(v.into_value()),
80        ConfigValue::Integer(v) => toml::Value::Integer(v.into_value()),
81        ConfigValue::Float(v) => toml::Value::Float(v.into_value()),
82        ConfigValue::Boolean(v) => toml::Value::Boolean(v.into_value()),
83        ConfigValue::Datetime(v) => toml::Value::Datetime(v.into_value()),
84        ConfigValue::Array(array) => {
85            let array = array.into_iter().map(to_serializable_value).collect();
86            toml::Value::Array(array)
87        }
88        ConfigValue::InlineTable(table) => {
89            let table = table
90                .into_iter()
91                .map(|(k, v)| (k, to_serializable_value(v)))
92                .collect();
93            toml::Value::Table(table)
94        }
95    }
96}
97
98/// Configuration variable with its source information.
99#[derive(Clone, Debug, serde::Serialize)]
100pub struct AnnotatedValue {
101    /// Dotted name path to the configuration variable.
102    #[serde(serialize_with = "serialize_name")]
103    pub name: ConfigNamePathBuf,
104    /// Configuration value.
105    #[serde(serialize_with = "serialize_value")]
106    pub value: ConfigValue,
107    /// Source of the configuration value.
108    #[serde(serialize_with = "serialize_source")]
109    pub source: ConfigSource,
110    /// Path to the source file, if available.
111    pub path: Option<PathBuf>,
112    /// True if this value is overridden in higher precedence layers.
113    pub is_overridden: bool,
114}
115
116fn serialize_name<S>(name: &ConfigNamePathBuf, serializer: S) -> Result<S::Ok, S::Error>
117where
118    S: serde::Serializer,
119{
120    name.to_string().serialize(serializer)
121}
122
123fn serialize_value<S>(value: &ConfigValue, serializer: S) -> Result<S::Ok, S::Error>
124where
125    S: serde::Serializer,
126{
127    to_serializable_value(value.clone()).serialize(serializer)
128}
129
130fn serialize_source<S>(source: &ConfigSource, serializer: S) -> Result<S::Ok, S::Error>
131where
132    S: serde::Serializer,
133{
134    source.to_string().serialize(serializer)
135}
136
137/// Collects values under the given `filter_prefix` name recursively, from all
138/// layers.
139pub fn resolved_config_values(
140    stacked_config: &StackedConfig,
141    filter_prefix: &ConfigNamePathBuf,
142) -> Vec<AnnotatedValue> {
143    // Collect annotated values in reverse order and mark each value shadowed by
144    // value or table in upper layers.
145    let mut config_vals = vec![];
146    let mut upper_value_names = BTreeSet::new();
147    for layer in stacked_config.layers().iter().rev() {
148        let top_item = match layer.look_up_item(filter_prefix) {
149            Ok(Some(item)) => item,
150            Ok(None) => continue, // parent is a table, but no value found
151            Err(_) => {
152                // parent is not a table, shadows lower layers
153                upper_value_names.insert(filter_prefix.clone());
154                continue;
155            }
156        };
157        let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];
158        while let Some((name, item, is_parent_overridden)) = config_stack.pop() {
159            // Cannot retain inline table formatting because inner values may be
160            // overridden independently.
161            if let Some(table) = item.as_table_like() {
162                // current table and children may be shadowed by value in upper layer
163                let is_overridden = is_parent_overridden || upper_value_names.contains(&name);
164                for (k, v) in table.iter() {
165                    let mut sub_name = name.clone();
166                    sub_name.push(k);
167                    config_stack.push((sub_name, v, is_overridden)); // in reverse order
168                }
169            } else {
170                // current value may be shadowed by value or table in upper layer
171                let maybe_child = upper_value_names
172                    .range(&name..)
173                    .next()
174                    .filter(|next| next.starts_with(&name));
175                let is_overridden = is_parent_overridden || maybe_child.is_some();
176                if maybe_child != Some(&name) {
177                    upper_value_names.insert(name.clone());
178                }
179                let value = item
180                    .clone()
181                    .into_value()
182                    .expect("Item::None should not exist in table");
183                config_vals.push(AnnotatedValue {
184                    name,
185                    value,
186                    source: layer.source,
187                    path: layer.path.clone(),
188                    is_overridden,
189                });
190            }
191        }
192    }
193    config_vals.reverse();
194    config_vals
195}
196
197/// Newtype for unprocessed (or unresolved) [`StackedConfig`].
198///
199/// This doesn't provide any strict guarantee about the underlying config
200/// object. It just requires an explicit cast to access to the config object.
201#[derive(Clone, Debug)]
202pub struct RawConfig(StackedConfig);
203
204impl AsRef<StackedConfig> for RawConfig {
205    fn as_ref(&self) -> &StackedConfig {
206        &self.0
207    }
208}
209
210impl AsMut<StackedConfig> for RawConfig {
211    fn as_mut(&mut self) -> &mut StackedConfig {
212        &mut self.0
213    }
214}
215
216#[derive(Clone, Debug)]
217enum ConfigPathState {
218    New,
219    Exists,
220}
221
222/// A ConfigPath can be in one of two states:
223///
224/// - exists(): a config file exists at the path
225/// - !exists(): a config file doesn't exist here, but a new file _can_ be
226///   created at this path
227#[derive(Clone, Debug)]
228struct ConfigPath {
229    path: PathBuf,
230    state: ConfigPathState,
231}
232
233impl ConfigPath {
234    fn new(path: PathBuf) -> Self {
235        use ConfigPathState::*;
236        Self {
237            state: if path.exists() { Exists } else { New },
238            path,
239        }
240    }
241
242    fn as_path(&self) -> &Path {
243        &self.path
244    }
245    fn exists(&self) -> bool {
246        match self.state {
247            ConfigPathState::Exists => true,
248            ConfigPathState::New => false,
249        }
250    }
251}
252
253/// Like std::fs::create_dir_all but creates new directories to be accessible to
254/// the user only on Unix (chmod 700).
255fn create_dir_all(path: &Path) -> std::io::Result<()> {
256    let mut dir = std::fs::DirBuilder::new();
257    dir.recursive(true);
258    #[cfg(unix)]
259    {
260        use std::os::unix::fs::DirBuilderExt as _;
261        dir.mode(0o700);
262    }
263    dir.create(path)
264}
265
266// The struct exists so that we can mock certain global values in unit tests.
267#[derive(Clone, Default, Debug)]
268struct UnresolvedConfigEnv {
269    config_dir: Option<PathBuf>,
270    // TODO: remove after jj 0.35
271    macos_legacy_config_dir: Option<PathBuf>,
272    home_dir: Option<PathBuf>,
273    jj_config: Option<String>,
274}
275
276impl UnresolvedConfigEnv {
277    fn resolve(self, ui: &Ui) -> Vec<ConfigPath> {
278        if let Some(paths) = self.jj_config {
279            return split_paths(&paths)
280                .filter(|path| !path.as_os_str().is_empty())
281                .map(ConfigPath::new)
282                .collect();
283        }
284
285        let mut paths = vec![];
286        let home_config_path = self.home_dir.map(|mut home_dir| {
287            home_dir.push(".jjconfig.toml");
288            ConfigPath::new(home_dir)
289        });
290        let platform_config_path = self.config_dir.clone().map(|mut config_dir| {
291            config_dir.push("jj");
292            config_dir.push("config.toml");
293            ConfigPath::new(config_dir)
294        });
295        let platform_config_dir = self.config_dir.map(|mut config_dir| {
296            config_dir.push("jj");
297            config_dir.push("conf.d");
298            ConfigPath::new(config_dir)
299        });
300        let legacy_platform_config_path =
301            self.macos_legacy_config_dir.clone().map(|mut config_dir| {
302                config_dir.push("jj");
303                config_dir.push("config.toml");
304                ConfigPath::new(config_dir)
305            });
306        let legacy_platform_config_dir = self.macos_legacy_config_dir.map(|mut config_dir| {
307            config_dir.push("jj");
308            config_dir.push("conf.d");
309            ConfigPath::new(config_dir)
310        });
311
312        if let Some(path) = home_config_path
313            && (path.exists()
314                || (platform_config_path.is_none() && legacy_platform_config_path.is_none()))
315        {
316            paths.push(path);
317        }
318
319        // This should be the default config created if there's
320        // no user config and `jj config edit` is executed.
321        if let Some(path) = platform_config_path {
322            paths.push(path);
323        }
324
325        // theoretically these should be an `if let Some(...) = ... && ..., but that
326        // isn't stable
327        if let Some(path) = platform_config_dir
328            && path.exists()
329        {
330            paths.push(path);
331        }
332
333        if let Some(path) = legacy_platform_config_path
334            && path.exists()
335        {
336            Self::warn_for_deprecated_path(
337                ui,
338                path.as_path(),
339                "~/Library/Application Support/jj",
340                "~/.config/jj",
341            );
342            paths.push(path);
343        }
344        if let Some(path) = legacy_platform_config_dir
345            && path.exists()
346        {
347            Self::warn_for_deprecated_path(
348                ui,
349                path.as_path(),
350                "~/Library/Application Support/jj",
351                "~/.config/jj",
352            );
353            paths.push(path);
354        }
355
356        paths
357    }
358
359    fn warn_for_deprecated_path(ui: &Ui, path: &Path, old: &str, new: &str) {
360        let _ = indoc::writedoc!(
361            ui.warning_default(),
362            r"
363            Deprecated configuration file `{}`.
364            Configuration files in `{old}` are deprecated, and support will be removed in a future release.
365            Instead, move your configuration files to `{new}`.
366            ",
367            path.display(),
368        );
369    }
370}
371
372#[derive(Clone, Debug)]
373pub struct ConfigEnv {
374    home_dir: Option<PathBuf>,
375    repo_path: Option<PathBuf>,
376    user_config_paths: Vec<ConfigPath>,
377    repo_config_path: Option<ConfigPath>,
378    command: Option<String>,
379}
380
381impl ConfigEnv {
382    /// Initializes configuration loader based on environment variables.
383    pub fn from_environment(ui: &Ui) -> Self {
384        let config_dir = etcetera::choose_base_strategy()
385            .ok()
386            .map(|s| s.config_dir());
387
388        // older versions of jj used a more "GUI" config option,
389        // which is not designed for user-editable configuration of CLI utilities.
390        let macos_legacy_config_dir = if cfg!(target_os = "macos") {
391            etcetera::base_strategy::choose_native_strategy()
392                .ok()
393                .map(|s| {
394                    // note that etcetera calls Library/Application Support the "data dir",
395                    // Library/Preferences is supposed to be exclusively plists
396                    s.data_dir()
397                })
398                .filter(|data_dir| {
399                    // User might've purposefully set their config dir to the deprecated one
400                    Some(data_dir) != config_dir.as_ref()
401                })
402        } else {
403            None
404        };
405
406        // Canonicalize home as we do canonicalize cwd in CliRunner. $HOME might
407        // point to symlink.
408        let home_dir = etcetera::home_dir()
409            .ok()
410            .map(|d| dunce::canonicalize(&d).unwrap_or(d));
411
412        let env = UnresolvedConfigEnv {
413            config_dir,
414            macos_legacy_config_dir,
415            home_dir: home_dir.clone(),
416            jj_config: env::var("JJ_CONFIG").ok(),
417        };
418        Self {
419            home_dir,
420            repo_path: None,
421            user_config_paths: env.resolve(ui),
422            repo_config_path: None,
423            command: None,
424        }
425    }
426
427    pub fn set_command_name(&mut self, command: String) {
428        self.command = Some(command);
429    }
430
431    /// Returns the paths to the user-specific config files or directories.
432    pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
433        self.user_config_paths.iter().map(ConfigPath::as_path)
434    }
435
436    /// Returns the paths to the existing user-specific config files or
437    /// directories.
438    pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
439        self.user_config_paths
440            .iter()
441            .filter(|p| p.exists())
442            .map(ConfigPath::as_path)
443    }
444
445    /// Returns user configuration files for modification. Instantiates one if
446    /// `config` has no user configuration layers.
447    ///
448    /// The parent directory for the new file may be created by this function.
449    /// If the user configuration path is unknown, this function returns an
450    /// empty `Vec`.
451    pub fn user_config_files(
452        &self,
453        config: &RawConfig,
454    ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
455        config_files_for(config, ConfigSource::User, || self.new_user_config_file())
456    }
457
458    fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
459        self.user_config_paths()
460            .next()
461            .map(|path| {
462                // No need to propagate io::Error here. If the directory
463                // couldn't be created, file.save() would fail later.
464                if let Some(dir) = path.parent() {
465                    create_dir_all(dir).ok();
466                }
467                // The path doesn't usually exist, but we shouldn't overwrite it
468                // with an empty config if it did exist.
469                ConfigFile::load_or_empty(ConfigSource::User, path)
470            })
471            .transpose()
472    }
473
474    /// Loads user-specific config files into the given `config`. The old
475    /// user-config layers will be replaced if any.
476    #[instrument]
477    pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
478        config.as_mut().remove_layers(ConfigSource::User);
479        for path in self.existing_user_config_paths() {
480            if path.is_dir() {
481                config.as_mut().load_dir(ConfigSource::User, path)?;
482            } else {
483                config.as_mut().load_file(ConfigSource::User, path)?;
484            }
485        }
486        Ok(())
487    }
488
489    /// Sets the directory where repo-specific config file is stored. The path
490    /// is usually `.jj/repo`.
491    pub fn reset_repo_path(&mut self, path: &Path) {
492        self.repo_path = Some(path.to_owned());
493        self.repo_config_path = Some(ConfigPath::new(path.join("config.toml")));
494    }
495
496    /// Returns a path to the repo-specific config file.
497    pub fn repo_config_path(&self) -> Option<&Path> {
498        self.repo_config_path.as_ref().map(|p| p.as_path())
499    }
500
501    /// Returns a path to the existing repo-specific config file.
502    fn existing_repo_config_path(&self) -> Option<&Path> {
503        match self.repo_config_path {
504            Some(ref path) if path.exists() => Some(path.as_path()),
505            _ => None,
506        }
507    }
508
509    /// Returns repo configuration files for modification. Instantiates one if
510    /// `config` has no repo configuration layers.
511    ///
512    /// If the repo path is unknown, this function returns an empty `Vec`. Since
513    /// the repo config path cannot be a directory, the returned `Vec` should
514    /// have at most one config file.
515    pub fn repo_config_files(
516        &self,
517        config: &RawConfig,
518    ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
519        config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file())
520    }
521
522    fn new_repo_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
523        self.repo_config_path()
524            // The path doesn't usually exist, but we shouldn't overwrite it
525            // with an empty config if it did exist.
526            .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
527            .transpose()
528    }
529
530    /// Loads repo-specific config file into the given `config`. The old
531    /// repo-config layer will be replaced if any.
532    #[instrument]
533    pub fn reload_repo_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
534        config.as_mut().remove_layers(ConfigSource::Repo);
535        if let Some(path) = self.existing_repo_config_path() {
536            config.as_mut().load_file(ConfigSource::Repo, path)?;
537        }
538        Ok(())
539    }
540
541    /// Resolves conditional scopes within the current environment. Returns new
542    /// resolved config.
543    pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
544        let context = ConfigResolutionContext {
545            home_dir: self.home_dir.as_deref(),
546            repo_path: self.repo_path.as_deref(),
547            command: self.command.as_deref(),
548        };
549        jj_lib::config::resolve(config.as_ref(), &context)
550    }
551}
552
553fn config_files_for(
554    config: &RawConfig,
555    source: ConfigSource,
556    new_file: impl FnOnce() -> Result<Option<ConfigFile>, ConfigLoadError>,
557) -> Result<Vec<ConfigFile>, ConfigLoadError> {
558    let mut files = config
559        .as_ref()
560        .layers_for(source)
561        .iter()
562        .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
563        .collect_vec();
564    if files.is_empty() {
565        files.extend(new_file()?);
566    }
567    Ok(files)
568}
569
570/// Initializes stacked config with the given `default_layers` and infallible
571/// sources.
572///
573/// Sources from the lowest precedence:
574/// 1. Default
575/// 2. Base environment variables
576/// 3. [User configs](https://jj-vcs.github.io/jj/latest/config/)
577/// 4. Repo config `.jj/repo/config.toml`
578/// 5. TODO: Workspace config `.jj/config.toml`
579/// 6. Override environment variables
580/// 7. Command-line arguments `--config` and `--config-file`
581///
582/// This function sets up 1, 2, and 6.
583pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
584    let mut config = StackedConfig::with_defaults();
585    config.extend_layers(default_layers);
586    config.add_layer(env_base_layer());
587    config.add_layer(env_overrides_layer());
588    RawConfig(config)
589}
590
591const OP_HOSTNAME: &str = "operation.hostname";
592const OP_USERNAME: &str = "operation.username";
593
594/// Environment variables that should be overridden by config values
595fn env_base_layer() -> ConfigLayer {
596    let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
597    if let Ok(value) = whoami::fallible::hostname()
598        .inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
599    {
600        layer.set_value(OP_HOSTNAME, value).unwrap();
601    }
602    if let Ok(value) = whoami::fallible::username()
603        .inspect_err(|err| tracing::warn!(?err, "failed to get username"))
604    {
605        layer.set_value(OP_USERNAME, value).unwrap();
606    } else if let Ok(value) = env::var("USER") {
607        // On Unix, $USER is set by login(1). Use it as a fallback because
608        // getpwuid() of musl libc appears not (fully?) supporting nsswitch.
609        layer.set_value(OP_USERNAME, value).unwrap();
610    }
611    if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
612        // "User-level configuration files and per-instance command-line arguments
613        // should override $NO_COLOR." https://no-color.org/
614        layer.set_value("ui.color", "never").unwrap();
615    }
616    if let Ok(value) = env::var("PAGER") {
617        layer.set_value("ui.pager", value).unwrap();
618    }
619    if let Ok(value) = env::var("VISUAL") {
620        layer.set_value("ui.editor", value).unwrap();
621    } else if let Ok(value) = env::var("EDITOR") {
622        layer.set_value("ui.editor", value).unwrap();
623    }
624    layer
625}
626
627pub fn default_config_layers() -> Vec<ConfigLayer> {
628    // Syntax error in default config isn't a user error. That's why defaults are
629    // loaded by separate builder.
630    let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
631    let mut layers = vec![
632        parse(include_str!("config/colors.toml")),
633        parse(include_str!("config/hints.toml")),
634        parse(include_str!("config/merge_tools.toml")),
635        parse(include_str!("config/misc.toml")),
636        parse(include_str!("config/revsets.toml")),
637        parse(include_str!("config/templates.toml")),
638    ];
639    if cfg!(unix) {
640        layers.push(parse(include_str!("config/unix.toml")));
641    }
642    if cfg!(windows) {
643        layers.push(parse(include_str!("config/windows.toml")));
644    }
645    layers
646}
647
648/// Environment variables that override config values
649fn env_overrides_layer() -> ConfigLayer {
650    let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
651    if let Ok(value) = env::var("JJ_USER") {
652        layer.set_value("user.name", value).unwrap();
653    }
654    if let Ok(value) = env::var("JJ_EMAIL") {
655        layer.set_value("user.email", value).unwrap();
656    }
657    if let Ok(value) = env::var("JJ_TIMESTAMP") {
658        layer.set_value("debug.commit-timestamp", value).unwrap();
659    }
660    if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
661        layer.set_value("debug.randomness-seed", value).unwrap();
662    }
663    if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
664        layer.set_value("debug.operation-timestamp", value).unwrap();
665    }
666    if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
667        layer.set_value(OP_HOSTNAME, value).unwrap();
668    }
669    if let Ok(value) = env::var("JJ_OP_USERNAME") {
670        layer.set_value(OP_USERNAME, value).unwrap();
671    }
672    if let Ok(value) = env::var("JJ_EDITOR") {
673        layer.set_value("ui.editor", value).unwrap();
674    }
675    layer
676}
677
678/// Configuration source/data type provided as command-line argument.
679#[derive(Clone, Copy, Debug, Eq, PartialEq)]
680pub enum ConfigArgKind {
681    /// `--config=NAME=VALUE`
682    Item,
683    /// `--config-file=PATH`
684    File,
685}
686
687/// Parses `--config*` arguments.
688pub fn parse_config_args(
689    toml_strs: &[(ConfigArgKind, &str)],
690) -> Result<Vec<ConfigLayer>, CommandError> {
691    let source = ConfigSource::CommandArg;
692    let mut layers = Vec::new();
693    for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
694        match kind {
695            ConfigArgKind::Item => {
696                let mut layer = ConfigLayer::empty(source);
697                for (_, item) in chunk {
698                    let (name, value) = parse_config_arg_item(item)?;
699                    // Can fail depending on the argument order, but that
700                    // wouldn't matter in practice.
701                    layer.set_value(name, value).map_err(|err| {
702                        config_error_with_message("--config argument cannot be set", err)
703                    })?;
704                }
705                layers.push(layer);
706            }
707            ConfigArgKind::File => {
708                for (_, path) in chunk {
709                    layers.push(ConfigLayer::load_from_file(source, path.into())?);
710                }
711            }
712        }
713    }
714    Ok(layers)
715}
716
717/// Parses `NAME=VALUE` string.
718fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
719    // split NAME=VALUE at the first parsable position
720    let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
721    let Some((name, value_str)) = split_candidates
722        .map(|p| (&item_str[..p], &item_str[p + 1..]))
723        .map(|(name, value)| name.parse().map(|name| (name, value)))
724        .find_or_last(Result::is_ok)
725        .transpose()
726        .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
727    else {
728        return Err(config_error("--config must be specified as NAME=VALUE"));
729    };
730    let value = parse_value_or_bare_string(value_str)
731        .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
732    Ok((name, value))
733}
734
735/// List of rules to migrate deprecated config variables.
736pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
737    vec![
738        // TODO: Delete in jj 0.33+
739        ConfigMigrationRule::rename_update_value(
740            "signing.sign-all",
741            "signing.behavior",
742            |old_value| {
743                if old_value
744                    .as_bool()
745                    .ok_or("signing.sign-all expects a boolean")?
746                {
747                    Ok("own".into())
748                } else {
749                    Ok("keep".into())
750                }
751            },
752        ),
753        // TODO: Delete in jj 0.34+
754        ConfigMigrationRule::rename_value(
755            "core.watchman.register_snapshot_trigger",
756            "fsmonitor.watchman.register-snapshot-trigger",
757        ),
758        // TODO: Delete in jj 0.34+
759        ConfigMigrationRule::rename_value("diff.format", "ui.diff.format"),
760        // TODO: Delete in jj 0.35.0+
761        ConfigMigrationRule::rename_update_value(
762            "ui.default-description",
763            "template-aliases.default_commit_description",
764            |old_value| {
765                let value = old_value.as_str().ok_or("expected a string")?;
766                // Trailing newline would be padded by templater (in jj < 0.31)
767                let value = text_util::complete_newline(value);
768                let escaped = dsl_util::escape_string(&value);
769                Ok(format!(r#""{escaped}""#).into())
770            },
771        ),
772        // TODO: Delete in jj 0.36+
773        ConfigMigrationRule::rename_value("ui.diff.tool", "ui.diff-formatter"),
774        // TODO: Delete in jj 0.36+
775        ConfigMigrationRule::rename_update_value(
776            "ui.diff.format",
777            "ui.diff-formatter",
778            |old_value| {
779                let value = old_value.as_str().ok_or("expected a string")?;
780                Ok(format!(":{value}").into())
781            },
782        ),
783        // TODO: Delete in jj 0.37+
784        ConfigMigrationRule::rename_update_value(
785            "git.push-bookmark-prefix",
786            "templates.git_push_bookmark",
787            |old_value| {
788                let value = old_value.as_str().ok_or("expected a string")?;
789                let escaped = dsl_util::escape_string(value);
790                Ok(format!(r#""{escaped}" ++ change_id.short()"#).into())
791            },
792        ),
793        // TODO: Delete in jj 0.38.0+
794        ConfigMigrationRule::rename_value("core.fsmonitor", "fsmonitor.backend"),
795        // TODO: Delete in jj 0.38.0+
796        ConfigMigrationRule::rename_value(
797            "core.watchman.register-snapshot-trigger",
798            "fsmonitor.watchman.register-snapshot-trigger",
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    fn config_path_macos_legacy_exists() -> TestCase {
1694        TestCase {
1695            files: &["macos-legacy/jj/config.toml"],
1696            env: UnresolvedConfigEnv {
1697                home_dir: Some("home".into()),
1698                config_dir: Some("config".into()),
1699                macos_legacy_config_dir: Some("macos-legacy".into()),
1700                ..Default::default()
1701            },
1702            wants: vec![
1703                Want::new("config/jj/config.toml"),
1704                Want::existing("macos-legacy/jj/config.toml"),
1705            ],
1706        }
1707    }
1708
1709    fn config_path_macos_legacy_both_exist() -> TestCase {
1710        TestCase {
1711            files: &["macos-legacy/jj/config.toml", "config/jj/config.toml"],
1712            env: UnresolvedConfigEnv {
1713                home_dir: Some("home".into()),
1714                config_dir: Some("config".into()),
1715                macos_legacy_config_dir: Some("macos-legacy".into()),
1716                ..Default::default()
1717            },
1718            wants: vec![
1719                Want::existing("config/jj/config.toml"),
1720                Want::existing("macos-legacy/jj/config.toml"),
1721            ],
1722        }
1723    }
1724
1725    fn config_path_macos_legacy_new() -> TestCase {
1726        TestCase {
1727            files: &[],
1728            env: UnresolvedConfigEnv {
1729                home_dir: Some("home".into()),
1730                config_dir: Some("config".into()),
1731                macos_legacy_config_dir: Some("macos-legacy".into()),
1732                ..Default::default()
1733            },
1734            wants: vec![Want::new("config/jj/config.toml")],
1735        }
1736    }
1737
1738    #[test_case(config_path_home_existing())]
1739    #[test_case(config_path_home_new())]
1740    #[test_case(config_path_home_existing_platform_new())]
1741    #[test_case(config_path_platform_existing())]
1742    #[test_case(config_path_platform_new())]
1743    #[test_case(config_path_new_prefer_platform())]
1744    #[test_case(config_path_jj_config_existing())]
1745    #[test_case(config_path_jj_config_new())]
1746    #[test_case(config_path_jj_config_existing_multiple())]
1747    #[test_case(config_path_jj_config_new_multiple())]
1748    #[test_case(config_path_jj_config_empty_paths_filtered())]
1749    #[test_case(config_path_jj_config_empty())]
1750    #[test_case(config_path_config_pick_platform())]
1751    #[test_case(config_path_config_pick_home())]
1752    #[test_case(config_path_platform_new_conf_dir_existing())]
1753    #[test_case(config_path_platform_existing_conf_dir_existing())]
1754    #[test_case(config_path_all_existing())]
1755    #[test_case(config_path_none())]
1756    #[test_case(config_path_macos_legacy_exists())]
1757    #[test_case(config_path_macos_legacy_both_exist())]
1758    #[test_case(config_path_macos_legacy_new())]
1759    fn test_config_path(case: TestCase) {
1760        let tmp = setup_config_fs(case.files);
1761        let env = resolve_config_env(&case.env, tmp.path());
1762
1763        let all_expected_paths = case
1764            .wants
1765            .iter()
1766            .map(|w| w.rooted_path(tmp.path()))
1767            .collect_vec();
1768        let exists_expected_paths = case
1769            .wants
1770            .iter()
1771            .filter(|w| w.exists())
1772            .map(|w| w.rooted_path(tmp.path()))
1773            .collect_vec();
1774
1775        let all_paths = env.user_config_paths().collect_vec();
1776        let exists_paths = env.existing_user_config_paths().collect_vec();
1777
1778        assert_eq!(all_paths, all_expected_paths);
1779        assert_eq!(exists_paths, exists_expected_paths);
1780    }
1781
1782    fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1783        let tmp = testutils::new_temp_dir();
1784        for file in files {
1785            let path = tmp.path().join(file);
1786            if let Some(parent) = path.parent() {
1787                std::fs::create_dir_all(parent).unwrap();
1788            }
1789            std::fs::File::create(path).unwrap();
1790        }
1791        tmp
1792    }
1793
1794    fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1795        let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1796        let env = UnresolvedConfigEnv {
1797            config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1798            macos_legacy_config_dir: env.macos_legacy_config_dir.as_ref().map(|p| root.join(p)),
1799            home_dir: home_dir.clone(),
1800            jj_config: env.jj_config.as_ref().map(|p| {
1801                join_paths(split_paths(p).map(|p| {
1802                    if p.as_os_str().is_empty() {
1803                        return p;
1804                    }
1805                    root.join(p)
1806                }))
1807                .unwrap()
1808                .into_string()
1809                .unwrap()
1810            }),
1811        };
1812        ConfigEnv {
1813            home_dir,
1814            repo_path: None,
1815            user_config_paths: env.resolve(&Ui::null()),
1816            repo_config_path: None,
1817            command: None,
1818        }
1819    }
1820}