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        if let Some(path) = platform_config_dir
326            && path.exists()
327        {
328            paths.push(path);
329        }
330
331        if let Some(path) = legacy_platform_config_path
332            && path.exists()
333        {
334            Self::warn_for_deprecated_path(
335                ui,
336                path.as_path(),
337                "~/Library/Application Support/jj",
338                "~/.config/jj",
339            );
340            paths.push(path);
341        }
342        if let Some(path) = legacy_platform_config_dir
343            && path.exists()
344        {
345            Self::warn_for_deprecated_path(
346                ui,
347                path.as_path(),
348                "~/Library/Application Support/jj",
349                "~/.config/jj",
350            );
351            paths.push(path);
352        }
353
354        paths
355    }
356
357    fn warn_for_deprecated_path(ui: &Ui, path: &Path, old: &str, new: &str) {
358        let _ = indoc::writedoc!(
359            ui.warning_default(),
360            r"
361            Deprecated configuration file `{}`.
362            Configuration files in `{old}` are deprecated, and support will be removed in a future release.
363            Instead, move your configuration files to `{new}`.
364            ",
365            path.display(),
366        );
367    }
368}
369
370#[derive(Clone, Debug)]
371pub struct ConfigEnv {
372    home_dir: Option<PathBuf>,
373    repo_path: Option<PathBuf>,
374    workspace_path: Option<PathBuf>,
375    user_config_paths: Vec<ConfigPath>,
376    repo_config_path: Option<ConfigPath>,
377    workspace_config_path: Option<ConfigPath>,
378    command: Option<String>,
379    hostname: Option<String>,
380}
381
382impl ConfigEnv {
383    /// Initializes configuration loader based on environment variables.
384    pub fn from_environment(ui: &Ui) -> Self {
385        let config_dir = etcetera::choose_base_strategy()
386            .ok()
387            .map(|s| s.config_dir());
388
389        // older versions of jj used a more "GUI" config option,
390        // which is not designed for user-editable configuration of CLI utilities.
391        let macos_legacy_config_dir = if cfg!(target_os = "macos") {
392            etcetera::base_strategy::choose_native_strategy()
393                .ok()
394                .map(|s| {
395                    // note that etcetera calls Library/Application Support the "data dir",
396                    // Library/Preferences is supposed to be exclusively plists
397                    s.data_dir()
398                })
399                .filter(|data_dir| {
400                    // User might've purposefully set their config dir to the deprecated one
401                    Some(data_dir) != config_dir.as_ref()
402                })
403        } else {
404            None
405        };
406
407        // Canonicalize home as we do canonicalize cwd in CliRunner. $HOME might
408        // point to symlink.
409        let home_dir = etcetera::home_dir()
410            .ok()
411            .map(|d| dunce::canonicalize(&d).unwrap_or(d));
412
413        let env = UnresolvedConfigEnv {
414            config_dir,
415            macos_legacy_config_dir,
416            home_dir: home_dir.clone(),
417            jj_config: env::var("JJ_CONFIG").ok(),
418        };
419        Self {
420            home_dir,
421            repo_path: None,
422            workspace_path: None,
423            user_config_paths: env.resolve(ui),
424            repo_config_path: None,
425            workspace_config_path: None,
426            command: None,
427            hostname: whoami::fallible::hostname().ok(),
428        }
429    }
430
431    pub fn set_command_name(&mut self, command: String) {
432        self.command = Some(command);
433    }
434
435    /// Returns the paths to the user-specific config files or directories.
436    pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
437        self.user_config_paths.iter().map(ConfigPath::as_path)
438    }
439
440    /// Returns the paths to the existing user-specific config files or
441    /// directories.
442    pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
443        self.user_config_paths
444            .iter()
445            .filter(|p| p.exists())
446            .map(ConfigPath::as_path)
447    }
448
449    /// Returns user configuration files for modification. Instantiates one if
450    /// `config` has no user configuration layers.
451    ///
452    /// The parent directory for the new file may be created by this function.
453    /// If the user configuration path is unknown, this function returns an
454    /// empty `Vec`.
455    pub fn user_config_files(
456        &self,
457        config: &RawConfig,
458    ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
459        config_files_for(config, ConfigSource::User, || self.new_user_config_file())
460    }
461
462    fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
463        self.user_config_paths()
464            .next()
465            .map(|path| {
466                // No need to propagate io::Error here. If the directory
467                // couldn't be created, file.save() would fail later.
468                if let Some(dir) = path.parent() {
469                    create_dir_all(dir).ok();
470                }
471                // The path doesn't usually exist, but we shouldn't overwrite it
472                // with an empty config if it did exist.
473                ConfigFile::load_or_empty(ConfigSource::User, path)
474            })
475            .transpose()
476    }
477
478    /// Loads user-specific config files into the given `config`. The old
479    /// user-config layers will be replaced if any.
480    #[instrument]
481    pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
482        config.as_mut().remove_layers(ConfigSource::User);
483        for path in self.existing_user_config_paths() {
484            if path.is_dir() {
485                config.as_mut().load_dir(ConfigSource::User, path)?;
486            } else {
487                config.as_mut().load_file(ConfigSource::User, path)?;
488            }
489        }
490        Ok(())
491    }
492
493    /// Sets the directory where repo-specific config file is stored. The path
494    /// is usually `.jj/repo`.
495    pub fn reset_repo_path(&mut self, path: &Path) {
496        self.repo_path = Some(path.to_owned());
497        self.repo_config_path = Some(ConfigPath::new(path.join("config.toml")));
498    }
499
500    /// Returns a path to the repo-specific config file.
501    pub fn repo_config_path(&self) -> Option<&Path> {
502        self.repo_config_path.as_ref().map(|p| p.as_path())
503    }
504
505    /// Returns a path to the existing repo-specific config file.
506    fn existing_repo_config_path(&self) -> Option<&Path> {
507        match self.repo_config_path {
508            Some(ref path) if path.exists() => Some(path.as_path()),
509            _ => None,
510        }
511    }
512
513    /// Returns repo configuration files for modification. Instantiates one if
514    /// `config` has no repo configuration layers.
515    ///
516    /// If the repo path is unknown, this function returns an empty `Vec`. Since
517    /// the repo config path cannot be a directory, the returned `Vec` should
518    /// have at most one config file.
519    pub fn repo_config_files(
520        &self,
521        config: &RawConfig,
522    ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
523        config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file())
524    }
525
526    fn new_repo_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
527        self.repo_config_path()
528            // The path doesn't usually exist, but we shouldn't overwrite it
529            // with an empty config if it did exist.
530            .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
531            .transpose()
532    }
533
534    /// Loads repo-specific config file into the given `config`. The old
535    /// repo-config layer will be replaced if any.
536    #[instrument]
537    pub fn reload_repo_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
538        config.as_mut().remove_layers(ConfigSource::Repo);
539        if let Some(path) = self.existing_repo_config_path() {
540            config.as_mut().load_file(ConfigSource::Repo, path)?;
541        }
542        Ok(())
543    }
544
545    /// Sets the directory for the workspace and the workspace-specific config
546    /// file.
547    pub fn reset_workspace_path(&mut self, path: &Path) {
548        self.workspace_path = Some(path.to_owned());
549        self.workspace_config_path = Some(ConfigPath::new(
550            path.join(".jj").join("workspace-config.toml"),
551        ));
552    }
553
554    /// Returns a path to the workspace-specific config file.
555    pub fn workspace_config_path(&self) -> Option<&Path> {
556        self.workspace_config_path.as_ref().map(|p| p.as_path())
557    }
558
559    /// Returns a path to the existing workspace-specific config file.
560    fn existing_workspace_config_path(&self) -> Option<&Path> {
561        match self.workspace_config_path {
562            Some(ref path) if path.exists() => Some(path.as_path()),
563            _ => None,
564        }
565    }
566
567    /// Returns workspace configuration files for modification. Instantiates one
568    /// if `config` has no workspace configuration layers.
569    ///
570    /// If the workspace path is unknown, this function returns an empty `Vec`.
571    /// Since the workspace config path cannot be a directory, the returned
572    /// `Vec` should have at most one config file.
573    pub fn workspace_config_files(
574        &self,
575        config: &RawConfig,
576    ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
577        config_files_for(config, ConfigSource::Workspace, || {
578            self.new_workspace_config_file()
579        })
580    }
581
582    fn new_workspace_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
583        self.workspace_config_path()
584            .map(|path| ConfigFile::load_or_empty(ConfigSource::Workspace, path))
585            .transpose()
586    }
587
588    /// Loads workspace-specific config file into the given `config`. The old
589    /// workspace-config layer will be replaced if any.
590    #[instrument]
591    pub fn reload_workspace_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
592        config.as_mut().remove_layers(ConfigSource::Workspace);
593        if let Some(path) = self.existing_workspace_config_path() {
594            config.as_mut().load_file(ConfigSource::Workspace, path)?;
595        }
596        Ok(())
597    }
598
599    /// Resolves conditional scopes within the current environment. Returns new
600    /// resolved config.
601    pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
602        let context = ConfigResolutionContext {
603            home_dir: self.home_dir.as_deref(),
604            repo_path: self.repo_path.as_deref(),
605            workspace_path: self.workspace_path.as_deref(),
606            command: self.command.as_deref(),
607            hostname: self.hostname.as_deref().unwrap_or(""),
608        };
609        jj_lib::config::resolve(config.as_ref(), &context)
610    }
611}
612
613fn config_files_for(
614    config: &RawConfig,
615    source: ConfigSource,
616    new_file: impl FnOnce() -> Result<Option<ConfigFile>, ConfigLoadError>,
617) -> Result<Vec<ConfigFile>, ConfigLoadError> {
618    let mut files = config
619        .as_ref()
620        .layers_for(source)
621        .iter()
622        .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
623        .collect_vec();
624    if files.is_empty() {
625        files.extend(new_file()?);
626    }
627    Ok(files)
628}
629
630/// Initializes stacked config with the given `default_layers` and infallible
631/// sources.
632///
633/// Sources from the lowest precedence:
634/// 1. Default
635/// 2. Base environment variables
636/// 3. [User configs](https://jj-vcs.github.io/jj/latest/config/)
637/// 4. Repo config `.jj/repo/config.toml`
638/// 5. Workspace config `.jj/workspace-config.toml`
639/// 6. Override environment variables
640/// 7. Command-line arguments `--config` and `--config-file`
641///
642/// This function sets up 1, 2, and 6.
643pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
644    let mut config = StackedConfig::with_defaults();
645    config.extend_layers(default_layers);
646    config.add_layer(env_base_layer());
647    config.add_layer(env_overrides_layer());
648    RawConfig(config)
649}
650
651const OP_HOSTNAME: &str = "operation.hostname";
652const OP_USERNAME: &str = "operation.username";
653
654/// Environment variables that should be overridden by config values
655fn env_base_layer() -> ConfigLayer {
656    let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
657    if let Ok(value) = whoami::fallible::hostname()
658        .inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
659    {
660        layer.set_value(OP_HOSTNAME, value).unwrap();
661    }
662    if let Ok(value) = whoami::fallible::username()
663        .inspect_err(|err| tracing::warn!(?err, "failed to get username"))
664    {
665        layer.set_value(OP_USERNAME, value).unwrap();
666    } else if let Ok(value) = env::var("USER") {
667        // On Unix, $USER is set by login(1). Use it as a fallback because
668        // getpwuid() of musl libc appears not (fully?) supporting nsswitch.
669        layer.set_value(OP_USERNAME, value).unwrap();
670    }
671    if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
672        // "User-level configuration files and per-instance command-line arguments
673        // should override $NO_COLOR." https://no-color.org/
674        layer.set_value("ui.color", "never").unwrap();
675    }
676    if let Ok(value) = env::var("PAGER") {
677        layer.set_value("ui.pager", value).unwrap();
678    }
679    if let Ok(value) = env::var("VISUAL") {
680        layer.set_value("ui.editor", value).unwrap();
681    } else if let Ok(value) = env::var("EDITOR") {
682        layer.set_value("ui.editor", value).unwrap();
683    }
684    layer
685}
686
687pub fn default_config_layers() -> Vec<ConfigLayer> {
688    // Syntax error in default config isn't a user error. That's why defaults are
689    // loaded by separate builder.
690    let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
691    let mut layers = vec![
692        parse(include_str!("config/colors.toml")),
693        parse(include_str!("config/hints.toml")),
694        parse(include_str!("config/merge_tools.toml")),
695        parse(include_str!("config/misc.toml")),
696        parse(include_str!("config/revsets.toml")),
697        parse(include_str!("config/templates.toml")),
698    ];
699    if cfg!(unix) {
700        layers.push(parse(include_str!("config/unix.toml")));
701    }
702    if cfg!(windows) {
703        layers.push(parse(include_str!("config/windows.toml")));
704    }
705    layers
706}
707
708/// Environment variables that override config values
709fn env_overrides_layer() -> ConfigLayer {
710    let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
711    if let Ok(value) = env::var("JJ_USER") {
712        layer.set_value("user.name", value).unwrap();
713    }
714    if let Ok(value) = env::var("JJ_EMAIL") {
715        layer.set_value("user.email", value).unwrap();
716    }
717    if let Ok(value) = env::var("JJ_TIMESTAMP") {
718        layer.set_value("debug.commit-timestamp", value).unwrap();
719    }
720    if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
721        layer.set_value("debug.randomness-seed", value).unwrap();
722    }
723    if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
724        layer.set_value("debug.operation-timestamp", value).unwrap();
725    }
726    if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
727        layer.set_value(OP_HOSTNAME, value).unwrap();
728    }
729    if let Ok(value) = env::var("JJ_OP_USERNAME") {
730        layer.set_value(OP_USERNAME, value).unwrap();
731    }
732    if let Ok(value) = env::var("JJ_EDITOR") {
733        layer.set_value("ui.editor", value).unwrap();
734    }
735    layer
736}
737
738/// Configuration source/data type provided as command-line argument.
739#[derive(Clone, Copy, Debug, Eq, PartialEq)]
740pub enum ConfigArgKind {
741    /// `--config=NAME=VALUE`
742    Item,
743    /// `--config-file=PATH`
744    File,
745}
746
747/// Parses `--config*` arguments.
748pub fn parse_config_args(
749    toml_strs: &[(ConfigArgKind, &str)],
750) -> Result<Vec<ConfigLayer>, CommandError> {
751    let source = ConfigSource::CommandArg;
752    let mut layers = Vec::new();
753    for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
754        match kind {
755            ConfigArgKind::Item => {
756                let mut layer = ConfigLayer::empty(source);
757                for (_, item) in chunk {
758                    let (name, value) = parse_config_arg_item(item)?;
759                    // Can fail depending on the argument order, but that
760                    // wouldn't matter in practice.
761                    layer.set_value(name, value).map_err(|err| {
762                        config_error_with_message("--config argument cannot be set", err)
763                    })?;
764                }
765                layers.push(layer);
766            }
767            ConfigArgKind::File => {
768                for (_, path) in chunk {
769                    layers.push(ConfigLayer::load_from_file(source, path.into())?);
770                }
771            }
772        }
773    }
774    Ok(layers)
775}
776
777/// Parses `NAME=VALUE` string.
778fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
779    // split NAME=VALUE at the first parsable position
780    let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
781    let Some((name, value_str)) = split_candidates
782        .map(|p| (&item_str[..p], &item_str[p + 1..]))
783        .map(|(name, value)| name.parse().map(|name| (name, value)))
784        .find_or_last(Result::is_ok)
785        .transpose()
786        .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
787    else {
788        return Err(config_error("--config must be specified as NAME=VALUE"));
789    };
790    let value = parse_value_or_bare_string(value_str)
791        .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
792    Ok((name, value))
793}
794
795/// List of rules to migrate deprecated config variables.
796pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
797    vec![
798        // TODO: Delete in jj 0.35.0+
799        ConfigMigrationRule::rename_update_value(
800            "ui.default-description",
801            "template-aliases.default_commit_description",
802            |old_value| {
803                let value = old_value.as_str().ok_or("expected a string")?;
804                // Trailing newline would be padded by templater (in jj < 0.31)
805                let value = text_util::complete_newline(value);
806                let escaped = dsl_util::escape_string(&value);
807                Ok(format!(r#""{escaped}""#).into())
808            },
809        ),
810        // TODO: Delete in jj 0.36+
811        ConfigMigrationRule::rename_value("ui.diff.tool", "ui.diff-formatter"),
812        // TODO: Delete in jj 0.36+
813        ConfigMigrationRule::rename_update_value(
814            "ui.diff.format",
815            "ui.diff-formatter",
816            |old_value| {
817                let value = old_value.as_str().ok_or("expected a string")?;
818                Ok(format!(":{value}").into())
819            },
820        ),
821        // TODO: Delete in jj 0.37+
822        ConfigMigrationRule::rename_update_value(
823            "git.push-bookmark-prefix",
824            "templates.git_push_bookmark",
825            |old_value| {
826                let value = old_value.as_str().ok_or("expected a string")?;
827                let escaped = dsl_util::escape_string(value);
828                Ok(format!(r#""{escaped}" ++ change_id.short()"#).into())
829            },
830        ),
831        // TODO: Delete in jj 0.38.0+
832        ConfigMigrationRule::rename_value("core.fsmonitor", "fsmonitor.backend"),
833        // TODO: Delete in jj 0.38.0+
834        ConfigMigrationRule::rename_value(
835            "core.watchman.register-snapshot-trigger",
836            "fsmonitor.watchman.register-snapshot-trigger",
837        ),
838    ]
839}
840
841/// Command name and arguments specified by config.
842#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
843#[serde(untagged)]
844pub enum CommandNameAndArgs {
845    String(String),
846    Vec(NonEmptyCommandArgsVec),
847    Structured {
848        env: HashMap<String, String>,
849        command: NonEmptyCommandArgsVec,
850    },
851}
852
853impl CommandNameAndArgs {
854    /// Returns command name without arguments.
855    pub fn split_name(&self) -> Cow<'_, str> {
856        let (name, _) = self.split_name_and_args();
857        name
858    }
859
860    /// Returns command name and arguments.
861    ///
862    /// The command name may be an empty string (as well as each argument.)
863    pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
864        match self {
865            Self::String(s) => {
866                // Handle things like `EDITOR=emacs -nw` (TODO: parse shell escapes)
867                let mut args = s.split(' ').map(|s| s.to_owned());
868                (args.next().unwrap().into(), args.collect())
869            }
870            Self::Vec(NonEmptyCommandArgsVec(a)) => (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])),
871            Self::Structured {
872                env: _,
873                command: cmd,
874            } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
875        }
876    }
877
878    /// Returns command string only if the underlying type is a string.
879    ///
880    /// Use this to parse enum strings such as `":builtin"`, which can be
881    /// escaped as `[":builtin"]`.
882    pub fn as_str(&self) -> Option<&str> {
883        match self {
884            Self::String(s) => Some(s),
885            Self::Vec(_) | Self::Structured { .. } => None,
886        }
887    }
888
889    /// Returns process builder configured with this.
890    pub fn to_command(&self) -> Command {
891        let empty: HashMap<&str, &str> = HashMap::new();
892        self.to_command_with_variables(&empty)
893    }
894
895    /// Returns process builder configured with this after interpolating
896    /// variables into the arguments.
897    pub fn to_command_with_variables<V: AsRef<str>>(
898        &self,
899        variables: &HashMap<&str, V>,
900    ) -> Command {
901        let (name, args) = self.split_name_and_args();
902        let mut cmd = Command::new(interpolate_variables_single(name.as_ref(), variables));
903        if let Self::Structured { env, .. } = self {
904            cmd.envs(env);
905        }
906        cmd.args(interpolate_variables(&args, variables));
907        cmd
908    }
909}
910
911impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
912    fn from(s: &T) -> Self {
913        Self::String(s.as_ref().to_owned())
914    }
915}
916
917impl fmt::Display for CommandNameAndArgs {
918    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919        match self {
920            Self::String(s) => write!(f, "{s}"),
921            // TODO: format with shell escapes
922            Self::Vec(a) => write!(f, "{}", a.0.join(" ")),
923            Self::Structured { env, command } => {
924                for (k, v) in env {
925                    write!(f, "{k}={v} ")?;
926                }
927                write!(f, "{}", command.0.join(" "))
928            }
929        }
930    }
931}
932
933// Not interested in $UPPER_CASE_VARIABLES
934static VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
935
936pub fn interpolate_variables<V: AsRef<str>>(
937    args: &[String],
938    variables: &HashMap<&str, V>,
939) -> Vec<String> {
940    args.iter()
941        .map(|arg| interpolate_variables_single(arg, variables))
942        .collect()
943}
944
945fn interpolate_variables_single<V: AsRef<str>>(arg: &str, variables: &HashMap<&str, V>) -> String {
946    VARIABLE_REGEX
947        .replace_all(arg, |caps: &Captures| {
948            let name = &caps[1];
949            if let Some(subst) = variables.get(name) {
950                subst.as_ref().to_owned()
951            } else {
952                caps[0].to_owned()
953            }
954        })
955        .into_owned()
956}
957
958/// Return all variable names found in the args, without the dollar sign
959pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
960    let regex = &*VARIABLE_REGEX;
961    args.iter()
962        .flat_map(|arg| regex.find_iter(arg))
963        .map(|single_match| {
964            let s = single_match.as_str();
965            &s[1..]
966        })
967}
968
969/// Wrapper to reject an array without command name.
970// Based on https://github.com/serde-rs/serde/issues/939
971#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
972#[serde(try_from = "Vec<String>")]
973pub struct NonEmptyCommandArgsVec(Vec<String>);
974
975impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
976    type Error = &'static str;
977
978    fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
979        if args.is_empty() {
980            Err("command arguments should not be empty")
981        } else {
982            Ok(Self(args))
983        }
984    }
985}
986
987#[cfg(test)]
988mod tests {
989    use std::env::join_paths;
990    use std::fmt::Write as _;
991
992    use indoc::indoc;
993    use maplit::hashmap;
994    use test_case::test_case;
995
996    use super::*;
997
998    fn insta_settings() -> insta::Settings {
999        let mut settings = insta::Settings::clone_current();
1000        // Suppress Decor { .. } which is uninteresting
1001        settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
1002        settings
1003    }
1004
1005    #[test]
1006    fn test_parse_value_or_bare_string() {
1007        let parse = |s: &str| parse_value_or_bare_string(s);
1008
1009        // Value in TOML syntax
1010        assert_eq!(parse("true").unwrap().as_bool(), Some(true));
1011        assert_eq!(parse("42").unwrap().as_integer(), Some(42));
1012        assert_eq!(parse("-1").unwrap().as_integer(), Some(-1));
1013        assert_eq!(parse("'a'").unwrap().as_str(), Some("a"));
1014        assert!(parse("[]").unwrap().is_array());
1015        assert!(parse("{ a = 'b' }").unwrap().is_inline_table());
1016
1017        // Bare string
1018        assert_eq!(parse("").unwrap().as_str(), Some(""));
1019        assert_eq!(parse("John Doe").unwrap().as_str(), Some("John Doe"));
1020        assert_eq!(parse("Doe, John").unwrap().as_str(), Some("Doe, John"));
1021        assert_eq!(parse("It's okay").unwrap().as_str(), Some("It's okay"));
1022        assert_eq!(
1023            parse("<foo+bar@example.org>").unwrap().as_str(),
1024            Some("<foo+bar@example.org>")
1025        );
1026        assert_eq!(parse("#ff00aa").unwrap().as_str(), Some("#ff00aa"));
1027        assert_eq!(parse("all()").unwrap().as_str(), Some("all()"));
1028        assert_eq!(parse("glob:*.*").unwrap().as_str(), Some("glob:*.*"));
1029        assert_eq!(parse("柔術").unwrap().as_str(), Some("柔術"));
1030
1031        // Error in TOML value
1032        assert!(parse("'foo").is_err());
1033        assert!(parse(r#" bar" "#).is_err());
1034        assert!(parse("[0 1]").is_err());
1035        assert!(parse("{ x = }").is_err());
1036        assert!(parse("\n { x").is_err());
1037        assert!(parse(" x ] ").is_err());
1038        assert!(parse("[table]\nkey = 'value'").is_err());
1039    }
1040
1041    #[test]
1042    fn test_parse_config_arg_item() {
1043        assert!(parse_config_arg_item("").is_err());
1044        assert!(parse_config_arg_item("a").is_err());
1045        assert!(parse_config_arg_item("=").is_err());
1046        // The value parser is sensitive to leading whitespaces, which seems
1047        // good because the parsing falls back to a bare string.
1048        assert!(parse_config_arg_item("a = 'b'").is_err());
1049
1050        let (name, value) = parse_config_arg_item("a=b").unwrap();
1051        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1052        assert_eq!(value.as_str(), Some("b"));
1053
1054        let (name, value) = parse_config_arg_item("a=").unwrap();
1055        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1056        assert_eq!(value.as_str(), Some(""));
1057
1058        let (name, value) = parse_config_arg_item("a= ").unwrap();
1059        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1060        assert_eq!(value.as_str(), Some(" "));
1061
1062        // This one is a bit cryptic, but b=c can be a bare string.
1063        let (name, value) = parse_config_arg_item("a=b=c").unwrap();
1064        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1065        assert_eq!(value.as_str(), Some("b=c"));
1066
1067        let (name, value) = parse_config_arg_item("a.b=true").unwrap();
1068        assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
1069        assert_eq!(value.as_bool(), Some(true));
1070
1071        let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
1072        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1073        assert_eq!(value.as_str(), Some("b=c"));
1074
1075        let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
1076        assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
1077        assert_eq!(value.as_str(), Some("c"));
1078
1079        let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
1080        assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
1081        assert!(value.is_inline_table());
1082        assert_eq!(value.to_string(), "{d = 'e=f'}");
1083    }
1084
1085    #[test]
1086    fn test_command_args() {
1087        let mut config = StackedConfig::empty();
1088        config.add_layer(
1089            ConfigLayer::parse(
1090                ConfigSource::User,
1091                indoc! {"
1092                    empty_array = []
1093                    empty_string = ''
1094                    array = ['emacs', '-nw']
1095                    string = 'emacs -nw'
1096                    structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1097                    structured.command = ['emacs', '-nw']
1098                "},
1099            )
1100            .unwrap(),
1101        );
1102
1103        assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1104
1105        let command_args: CommandNameAndArgs = config.get("empty_string").unwrap();
1106        assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1107        let (name, args) = command_args.split_name_and_args();
1108        assert_eq!(name, "");
1109        assert!(args.is_empty());
1110
1111        let command_args: CommandNameAndArgs = config.get("array").unwrap();
1112        assert_eq!(
1113            command_args,
1114            CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1115                ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1116            ))
1117        );
1118        let (name, args) = command_args.split_name_and_args();
1119        assert_eq!(name, "emacs");
1120        assert_eq!(args, ["-nw"].as_ref());
1121
1122        let command_args: CommandNameAndArgs = config.get("string").unwrap();
1123        assert_eq!(
1124            command_args,
1125            CommandNameAndArgs::String("emacs -nw".to_owned())
1126        );
1127        let (name, args) = command_args.split_name_and_args();
1128        assert_eq!(name, "emacs");
1129        assert_eq!(args, ["-nw"].as_ref());
1130
1131        let command_args: CommandNameAndArgs = config.get("structured").unwrap();
1132        assert_eq!(
1133            command_args,
1134            CommandNameAndArgs::Structured {
1135                env: hashmap! {
1136                    "KEY1".to_string() => "value1".to_string(),
1137                    "KEY2".to_string() => "value2".to_string(),
1138                },
1139                command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1140            }
1141        );
1142        let (name, args) = command_args.split_name_and_args();
1143        assert_eq!(name, "emacs");
1144        assert_eq!(args, ["-nw"].as_ref());
1145    }
1146
1147    #[test]
1148    fn test_resolved_config_values_empty() {
1149        let config = StackedConfig::empty();
1150        assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
1151    }
1152
1153    #[test]
1154    fn test_resolved_config_values_single_key() {
1155        let settings = insta_settings();
1156        let _guard = settings.bind_to_scope();
1157        let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
1158        env_base_layer
1159            .set_value("user.name", "base-user-name")
1160            .unwrap();
1161        env_base_layer
1162            .set_value("user.email", "base@user.email")
1163            .unwrap();
1164        let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1165        repo_layer
1166            .set_value("user.email", "repo@user.email")
1167            .unwrap();
1168        let mut config = StackedConfig::empty();
1169        config.add_layer(env_base_layer);
1170        config.add_layer(repo_layer);
1171        // Note: "email" is alphabetized, before "name" from same layer.
1172        insta::assert_debug_snapshot!(
1173            resolved_config_values(&config, &ConfigNamePathBuf::root()),
1174            @r#"
1175        [
1176            AnnotatedValue {
1177                name: ConfigNamePathBuf(
1178                    [
1179                        Key {
1180                            key: "user",
1181                            repr: None,
1182                            leaf_decor: Decor { .. },
1183                            dotted_decor: Decor { .. },
1184                        },
1185                        Key {
1186                            key: "name",
1187                            repr: None,
1188                            leaf_decor: Decor { .. },
1189                            dotted_decor: Decor { .. },
1190                        },
1191                    ],
1192                ),
1193                value: String(
1194                    Formatted {
1195                        value: "base-user-name",
1196                        repr: "default",
1197                        decor: Decor { .. },
1198                    },
1199                ),
1200                source: EnvBase,
1201                path: None,
1202                is_overridden: false,
1203            },
1204            AnnotatedValue {
1205                name: ConfigNamePathBuf(
1206                    [
1207                        Key {
1208                            key: "user",
1209                            repr: None,
1210                            leaf_decor: Decor { .. },
1211                            dotted_decor: Decor { .. },
1212                        },
1213                        Key {
1214                            key: "email",
1215                            repr: None,
1216                            leaf_decor: Decor { .. },
1217                            dotted_decor: Decor { .. },
1218                        },
1219                    ],
1220                ),
1221                value: String(
1222                    Formatted {
1223                        value: "base@user.email",
1224                        repr: "default",
1225                        decor: Decor { .. },
1226                    },
1227                ),
1228                source: EnvBase,
1229                path: None,
1230                is_overridden: true,
1231            },
1232            AnnotatedValue {
1233                name: ConfigNamePathBuf(
1234                    [
1235                        Key {
1236                            key: "user",
1237                            repr: None,
1238                            leaf_decor: Decor { .. },
1239                            dotted_decor: Decor { .. },
1240                        },
1241                        Key {
1242                            key: "email",
1243                            repr: None,
1244                            leaf_decor: Decor { .. },
1245                            dotted_decor: Decor { .. },
1246                        },
1247                    ],
1248                ),
1249                value: String(
1250                    Formatted {
1251                        value: "repo@user.email",
1252                        repr: "default",
1253                        decor: Decor { .. },
1254                    },
1255                ),
1256                source: Repo,
1257                path: None,
1258                is_overridden: false,
1259            },
1260        ]
1261        "#
1262        );
1263    }
1264
1265    #[test]
1266    fn test_resolved_config_values_filter_path() {
1267        let settings = insta_settings();
1268        let _guard = settings.bind_to_scope();
1269        let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1270        user_layer.set_value("test-table1.foo", "user-FOO").unwrap();
1271        user_layer.set_value("test-table2.bar", "user-BAR").unwrap();
1272        let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1273        repo_layer.set_value("test-table1.bar", "repo-BAR").unwrap();
1274        let mut config = StackedConfig::empty();
1275        config.add_layer(user_layer);
1276        config.add_layer(repo_layer);
1277        insta::assert_debug_snapshot!(
1278            resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1279            @r#"
1280        [
1281            AnnotatedValue {
1282                name: ConfigNamePathBuf(
1283                    [
1284                        Key {
1285                            key: "test-table1",
1286                            repr: None,
1287                            leaf_decor: Decor { .. },
1288                            dotted_decor: Decor { .. },
1289                        },
1290                        Key {
1291                            key: "foo",
1292                            repr: None,
1293                            leaf_decor: Decor { .. },
1294                            dotted_decor: Decor { .. },
1295                        },
1296                    ],
1297                ),
1298                value: String(
1299                    Formatted {
1300                        value: "user-FOO",
1301                        repr: "default",
1302                        decor: Decor { .. },
1303                    },
1304                ),
1305                source: User,
1306                path: None,
1307                is_overridden: false,
1308            },
1309            AnnotatedValue {
1310                name: ConfigNamePathBuf(
1311                    [
1312                        Key {
1313                            key: "test-table1",
1314                            repr: None,
1315                            leaf_decor: Decor { .. },
1316                            dotted_decor: Decor { .. },
1317                        },
1318                        Key {
1319                            key: "bar",
1320                            repr: None,
1321                            leaf_decor: Decor { .. },
1322                            dotted_decor: Decor { .. },
1323                        },
1324                    ],
1325                ),
1326                value: String(
1327                    Formatted {
1328                        value: "repo-BAR",
1329                        repr: "default",
1330                        decor: Decor { .. },
1331                    },
1332                ),
1333                source: Repo,
1334                path: None,
1335                is_overridden: false,
1336            },
1337        ]
1338        "#
1339        );
1340    }
1341
1342    #[test]
1343    fn test_resolved_config_values_overridden() {
1344        let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1345            let mut config = StackedConfig::empty();
1346            config.extend_layers(layers.iter().copied().cloned());
1347            let prefix = if prefix.is_empty() {
1348                ConfigNamePathBuf::root()
1349            } else {
1350                prefix.parse().unwrap()
1351            };
1352            let mut output = String::new();
1353            for annotated in resolved_config_values(&config, &prefix) {
1354                let AnnotatedValue { name, value, .. } = &annotated;
1355                let sigil = if annotated.is_overridden { '!' } else { ' ' };
1356                writeln!(output, "{sigil}{name} = {value}").unwrap();
1357            }
1358            output
1359        };
1360
1361        let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1362        layer0.set_value("a.b.e", "0.0").unwrap();
1363        layer0.set_value("a.b.c.f", "0.1").unwrap();
1364        layer0.set_value("a.b.d", "0.2").unwrap();
1365        let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1366        layer1.set_value("a.b", "1.0").unwrap();
1367        layer1.set_value("a.c", "1.1").unwrap();
1368        let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1369        layer2.set_value("a.b.g", "2.0").unwrap();
1370        layer2.set_value("a.b.d", "2.1").unwrap();
1371
1372        // a.b.* is shadowed by a.b
1373        let layers = [&layer0, &layer1];
1374        insta::assert_snapshot!(list(&layers, ""), @r#"
1375        !a.b.e = "0.0"
1376        !a.b.c.f = "0.1"
1377        !a.b.d = "0.2"
1378         a.b = "1.0"
1379         a.c = "1.1"
1380        "#);
1381        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1382        !a.b.e = "0.0"
1383        !a.b.c.f = "0.1"
1384        !a.b.d = "0.2"
1385         a.b = "1.0"
1386        "#);
1387        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1388        insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1389
1390        // a.b is shadowed by a.b.*
1391        let layers = [&layer1, &layer2];
1392        insta::assert_snapshot!(list(&layers, ""), @r#"
1393        !a.b = "1.0"
1394         a.c = "1.1"
1395         a.b.g = "2.0"
1396         a.b.d = "2.1"
1397        "#);
1398        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1399        !a.b = "1.0"
1400         a.b.g = "2.0"
1401         a.b.d = "2.1"
1402        "#);
1403
1404        // a.b.d is shadowed by a.b.d
1405        let layers = [&layer0, &layer2];
1406        insta::assert_snapshot!(list(&layers, ""), @r#"
1407         a.b.e = "0.0"
1408         a.b.c.f = "0.1"
1409        !a.b.d = "0.2"
1410         a.b.g = "2.0"
1411         a.b.d = "2.1"
1412        "#);
1413        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1414         a.b.e = "0.0"
1415         a.b.c.f = "0.1"
1416        !a.b.d = "0.2"
1417         a.b.g = "2.0"
1418         a.b.d = "2.1"
1419        "#);
1420        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1421        insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1422        !a.b.d = "0.2"
1423         a.b.d = "2.1"
1424        "#);
1425
1426        // a.b.* is shadowed by a.b, which is shadowed by a.b.*
1427        let layers = [&layer0, &layer1, &layer2];
1428        insta::assert_snapshot!(list(&layers, ""), @r#"
1429        !a.b.e = "0.0"
1430        !a.b.c.f = "0.1"
1431        !a.b.d = "0.2"
1432        !a.b = "1.0"
1433         a.c = "1.1"
1434         a.b.g = "2.0"
1435         a.b.d = "2.1"
1436        "#);
1437        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1438        !a.b.e = "0.0"
1439        !a.b.c.f = "0.1"
1440        !a.b.d = "0.2"
1441        !a.b = "1.0"
1442         a.b.g = "2.0"
1443         a.b.d = "2.1"
1444        "#);
1445        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1446    }
1447
1448    struct TestCase {
1449        files: &'static [&'static str],
1450        env: UnresolvedConfigEnv,
1451        wants: Vec<Want>,
1452    }
1453
1454    #[derive(Debug)]
1455    enum WantState {
1456        New,
1457        Existing,
1458    }
1459    #[derive(Debug)]
1460    struct Want {
1461        path: &'static str,
1462        state: WantState,
1463    }
1464
1465    impl Want {
1466        const fn new(path: &'static str) -> Self {
1467            Self {
1468                path,
1469                state: WantState::New,
1470            }
1471        }
1472
1473        const fn existing(path: &'static str) -> Self {
1474            Self {
1475                path,
1476                state: WantState::Existing,
1477            }
1478        }
1479
1480        fn rooted_path(&self, root: &Path) -> PathBuf {
1481            root.join(self.path)
1482        }
1483
1484        fn exists(&self) -> bool {
1485            matches!(self.state, WantState::Existing)
1486        }
1487    }
1488
1489    fn config_path_home_existing() -> TestCase {
1490        TestCase {
1491            files: &["home/.jjconfig.toml"],
1492            env: UnresolvedConfigEnv {
1493                home_dir: Some("home".into()),
1494                ..Default::default()
1495            },
1496            wants: vec![Want::existing("home/.jjconfig.toml")],
1497        }
1498    }
1499
1500    fn config_path_home_new() -> TestCase {
1501        TestCase {
1502            files: &[],
1503            env: UnresolvedConfigEnv {
1504                home_dir: Some("home".into()),
1505                ..Default::default()
1506            },
1507            wants: vec![Want::new("home/.jjconfig.toml")],
1508        }
1509    }
1510
1511    fn config_path_home_existing_platform_new() -> TestCase {
1512        TestCase {
1513            files: &["home/.jjconfig.toml"],
1514            env: UnresolvedConfigEnv {
1515                home_dir: Some("home".into()),
1516                config_dir: Some("config".into()),
1517                ..Default::default()
1518            },
1519            wants: vec![
1520                Want::existing("home/.jjconfig.toml"),
1521                Want::new("config/jj/config.toml"),
1522            ],
1523        }
1524    }
1525
1526    fn config_path_platform_existing() -> TestCase {
1527        TestCase {
1528            files: &["config/jj/config.toml"],
1529            env: UnresolvedConfigEnv {
1530                home_dir: Some("home".into()),
1531                config_dir: Some("config".into()),
1532                ..Default::default()
1533            },
1534            wants: vec![Want::existing("config/jj/config.toml")],
1535        }
1536    }
1537
1538    fn config_path_platform_new() -> TestCase {
1539        TestCase {
1540            files: &[],
1541            env: UnresolvedConfigEnv {
1542                config_dir: Some("config".into()),
1543                ..Default::default()
1544            },
1545            wants: vec![Want::new("config/jj/config.toml")],
1546        }
1547    }
1548
1549    fn config_path_new_prefer_platform() -> TestCase {
1550        TestCase {
1551            files: &[],
1552            env: UnresolvedConfigEnv {
1553                home_dir: Some("home".into()),
1554                config_dir: Some("config".into()),
1555                ..Default::default()
1556            },
1557            wants: vec![Want::new("config/jj/config.toml")],
1558        }
1559    }
1560
1561    fn config_path_jj_config_existing() -> TestCase {
1562        TestCase {
1563            files: &["custom.toml"],
1564            env: UnresolvedConfigEnv {
1565                jj_config: Some("custom.toml".into()),
1566                ..Default::default()
1567            },
1568            wants: vec![Want::existing("custom.toml")],
1569        }
1570    }
1571
1572    fn config_path_jj_config_new() -> TestCase {
1573        TestCase {
1574            files: &[],
1575            env: UnresolvedConfigEnv {
1576                jj_config: Some("custom.toml".into()),
1577                ..Default::default()
1578            },
1579            wants: vec![Want::new("custom.toml")],
1580        }
1581    }
1582
1583    fn config_path_jj_config_existing_multiple() -> TestCase {
1584        TestCase {
1585            files: &["custom1.toml", "custom2.toml"],
1586            env: UnresolvedConfigEnv {
1587                jj_config: Some(
1588                    join_paths(["custom1.toml", "custom2.toml"])
1589                        .unwrap()
1590                        .into_string()
1591                        .unwrap(),
1592                ),
1593                ..Default::default()
1594            },
1595            wants: vec![
1596                Want::existing("custom1.toml"),
1597                Want::existing("custom2.toml"),
1598            ],
1599        }
1600    }
1601
1602    fn config_path_jj_config_new_multiple() -> TestCase {
1603        TestCase {
1604            files: &["custom1.toml"],
1605            env: UnresolvedConfigEnv {
1606                jj_config: Some(
1607                    join_paths(["custom1.toml", "custom2.toml"])
1608                        .unwrap()
1609                        .into_string()
1610                        .unwrap(),
1611                ),
1612                ..Default::default()
1613            },
1614            wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1615        }
1616    }
1617
1618    fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1619        TestCase {
1620            files: &["custom1.toml"],
1621            env: UnresolvedConfigEnv {
1622                jj_config: Some(
1623                    join_paths(["custom1.toml", "", "custom2.toml"])
1624                        .unwrap()
1625                        .into_string()
1626                        .unwrap(),
1627                ),
1628                ..Default::default()
1629            },
1630            wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1631        }
1632    }
1633
1634    fn config_path_jj_config_empty() -> TestCase {
1635        TestCase {
1636            files: &[],
1637            env: UnresolvedConfigEnv {
1638                jj_config: Some("".to_owned()),
1639                ..Default::default()
1640            },
1641            wants: vec![],
1642        }
1643    }
1644
1645    fn config_path_config_pick_platform() -> TestCase {
1646        TestCase {
1647            files: &["config/jj/config.toml"],
1648            env: UnresolvedConfigEnv {
1649                home_dir: Some("home".into()),
1650                config_dir: Some("config".into()),
1651                ..Default::default()
1652            },
1653            wants: vec![Want::existing("config/jj/config.toml")],
1654        }
1655    }
1656
1657    fn config_path_config_pick_home() -> TestCase {
1658        TestCase {
1659            files: &["home/.jjconfig.toml"],
1660            env: UnresolvedConfigEnv {
1661                home_dir: Some("home".into()),
1662                config_dir: Some("config".into()),
1663                ..Default::default()
1664            },
1665            wants: vec![
1666                Want::existing("home/.jjconfig.toml"),
1667                Want::new("config/jj/config.toml"),
1668            ],
1669        }
1670    }
1671
1672    fn config_path_platform_new_conf_dir_existing() -> TestCase {
1673        TestCase {
1674            files: &["config/jj/conf.d/_"],
1675            env: UnresolvedConfigEnv {
1676                home_dir: Some("home".into()),
1677                config_dir: Some("config".into()),
1678                ..Default::default()
1679            },
1680            wants: vec![
1681                Want::new("config/jj/config.toml"),
1682                Want::existing("config/jj/conf.d"),
1683            ],
1684        }
1685    }
1686
1687    fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1688        TestCase {
1689            files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1690            env: UnresolvedConfigEnv {
1691                home_dir: Some("home".into()),
1692                config_dir: Some("config".into()),
1693                ..Default::default()
1694            },
1695            wants: vec![
1696                Want::existing("config/jj/config.toml"),
1697                Want::existing("config/jj/conf.d"),
1698            ],
1699        }
1700    }
1701
1702    fn config_path_all_existing() -> TestCase {
1703        TestCase {
1704            files: &[
1705                "config/jj/conf.d/_",
1706                "config/jj/config.toml",
1707                "home/.jjconfig.toml",
1708            ],
1709            env: UnresolvedConfigEnv {
1710                home_dir: Some("home".into()),
1711                config_dir: Some("config".into()),
1712                ..Default::default()
1713            },
1714            // Precedence order is important
1715            wants: vec![
1716                Want::existing("home/.jjconfig.toml"),
1717                Want::existing("config/jj/config.toml"),
1718                Want::existing("config/jj/conf.d"),
1719            ],
1720        }
1721    }
1722
1723    fn config_path_none() -> TestCase {
1724        TestCase {
1725            files: &[],
1726            env: Default::default(),
1727            wants: vec![],
1728        }
1729    }
1730
1731    fn config_path_macos_legacy_exists() -> TestCase {
1732        TestCase {
1733            files: &["macos-legacy/jj/config.toml"],
1734            env: UnresolvedConfigEnv {
1735                home_dir: Some("home".into()),
1736                config_dir: Some("config".into()),
1737                macos_legacy_config_dir: Some("macos-legacy".into()),
1738                ..Default::default()
1739            },
1740            wants: vec![
1741                Want::new("config/jj/config.toml"),
1742                Want::existing("macos-legacy/jj/config.toml"),
1743            ],
1744        }
1745    }
1746
1747    fn config_path_macos_legacy_both_exist() -> TestCase {
1748        TestCase {
1749            files: &["macos-legacy/jj/config.toml", "config/jj/config.toml"],
1750            env: UnresolvedConfigEnv {
1751                home_dir: Some("home".into()),
1752                config_dir: Some("config".into()),
1753                macos_legacy_config_dir: Some("macos-legacy".into()),
1754                ..Default::default()
1755            },
1756            wants: vec![
1757                Want::existing("config/jj/config.toml"),
1758                Want::existing("macos-legacy/jj/config.toml"),
1759            ],
1760        }
1761    }
1762
1763    fn config_path_macos_legacy_new() -> TestCase {
1764        TestCase {
1765            files: &[],
1766            env: UnresolvedConfigEnv {
1767                home_dir: Some("home".into()),
1768                config_dir: Some("config".into()),
1769                macos_legacy_config_dir: Some("macos-legacy".into()),
1770                ..Default::default()
1771            },
1772            wants: vec![Want::new("config/jj/config.toml")],
1773        }
1774    }
1775
1776    #[test_case(config_path_home_existing())]
1777    #[test_case(config_path_home_new())]
1778    #[test_case(config_path_home_existing_platform_new())]
1779    #[test_case(config_path_platform_existing())]
1780    #[test_case(config_path_platform_new())]
1781    #[test_case(config_path_new_prefer_platform())]
1782    #[test_case(config_path_jj_config_existing())]
1783    #[test_case(config_path_jj_config_new())]
1784    #[test_case(config_path_jj_config_existing_multiple())]
1785    #[test_case(config_path_jj_config_new_multiple())]
1786    #[test_case(config_path_jj_config_empty_paths_filtered())]
1787    #[test_case(config_path_jj_config_empty())]
1788    #[test_case(config_path_config_pick_platform())]
1789    #[test_case(config_path_config_pick_home())]
1790    #[test_case(config_path_platform_new_conf_dir_existing())]
1791    #[test_case(config_path_platform_existing_conf_dir_existing())]
1792    #[test_case(config_path_all_existing())]
1793    #[test_case(config_path_none())]
1794    #[test_case(config_path_macos_legacy_exists())]
1795    #[test_case(config_path_macos_legacy_both_exist())]
1796    #[test_case(config_path_macos_legacy_new())]
1797    fn test_config_path(case: TestCase) {
1798        let tmp = setup_config_fs(case.files);
1799        let env = resolve_config_env(&case.env, tmp.path());
1800
1801        let all_expected_paths = case
1802            .wants
1803            .iter()
1804            .map(|w| w.rooted_path(tmp.path()))
1805            .collect_vec();
1806        let exists_expected_paths = case
1807            .wants
1808            .iter()
1809            .filter(|w| w.exists())
1810            .map(|w| w.rooted_path(tmp.path()))
1811            .collect_vec();
1812
1813        let all_paths = env.user_config_paths().collect_vec();
1814        let exists_paths = env.existing_user_config_paths().collect_vec();
1815
1816        assert_eq!(all_paths, all_expected_paths);
1817        assert_eq!(exists_paths, exists_expected_paths);
1818    }
1819
1820    fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1821        let tmp = testutils::new_temp_dir();
1822        for file in files {
1823            let path = tmp.path().join(file);
1824            if let Some(parent) = path.parent() {
1825                std::fs::create_dir_all(parent).unwrap();
1826            }
1827            std::fs::File::create(path).unwrap();
1828        }
1829        tmp
1830    }
1831
1832    fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1833        let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1834        let env = UnresolvedConfigEnv {
1835            config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1836            macos_legacy_config_dir: env.macos_legacy_config_dir.as_ref().map(|p| root.join(p)),
1837            home_dir: home_dir.clone(),
1838            jj_config: env.jj_config.as_ref().map(|p| {
1839                join_paths(split_paths(p).map(|p| {
1840                    if p.as_os_str().is_empty() {
1841                        return p;
1842                    }
1843                    root.join(p)
1844                }))
1845                .unwrap()
1846                .into_string()
1847                .unwrap()
1848            }),
1849        };
1850        ConfigEnv {
1851            home_dir,
1852            repo_path: None,
1853            workspace_path: None,
1854            user_config_paths: env.resolve(&Ui::null()),
1855            repo_config_path: None,
1856            workspace_config_path: None,
1857            command: None,
1858            hostname: None,
1859        }
1860    }
1861}