From 90bb4507bfad88356f5c4507249fd4fce31edffe Mon Sep 17 00:00:00 2001 From: Will Chandler Date: Sat, 10 Feb 2024 15:04:38 -0500 Subject: [PATCH] cli: Add `--show-origin` option to `config list` Add a new `--show-origin` option to `jj config list` to display the source type and file path (when present) of config values. In cases where the user config path is a directory, the individual file containing a given value will be displayed. The method that captures config file paths, `config::FileSourceFile::resolve()`, converts them to relative paths. These may be difficult to recognize when the current working directory is distant from a config file's location. We therefore attempt to canonicalize all file paths. Should this fail, we use the path as-is. --- CHANGELOG.md | 3 ++ cli/src/commands/config.rs | 29 +++++++++++++- cli/src/config.rs | 13 ++++++- cli/tests/cli-reference@.md.snap | 4 ++ cli/tests/test_config_command.rs | 66 ++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 593fb01494..6dde6f4af5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#2971](https://github.com/martinvonz/jj/issues/2971)). This may become the default depending on feedback. +* `jj config list` gained a `--show-origin` flag to display the source of + config values. + ### Fixed bugs * On Windows, symlinks in the repo are now materialized as regular files in the diff --git a/cli/src/commands/config.rs b/cli/src/commands/config.rs index 3ed0efbca5..3110ccd0ac 100644 --- a/cli/src/commands/config.rs +++ b/cli/src/commands/config.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::io::Write; +use std::path::Path; use clap::builder::NonEmptyStringValueParser; use itertools::Itertools; @@ -84,6 +85,11 @@ pub(crate) struct ConfigListArgs { /// Allow printing overridden values. #[arg(long)] pub include_overridden: bool, + /// Display the source of all listed config values with type (default, env, + /// usercfg, repocfg, command line) and the source file path for usercfg + /// and repocfg. + #[arg(long)] + pub show_origin: bool, /// Target the user-level config #[arg(long)] user: bool, @@ -204,9 +210,16 @@ pub(crate) fn cmd_config_list( if !args.include_defaults && *source == ConfigSource::Default { continue; } + + let origin = if args.show_origin { + format_origin(value.origin(), source) + } else { + String::from("") + }; + writeln!( ui.stdout(), - "{}{}={}", + "{origin}{}{}={}", if *is_overridden { "# " } else { "" }, path.join("."), serialize_config_value(value) @@ -304,3 +317,17 @@ pub(crate) fn cmd_config_path( )?; Ok(()) } + +fn format_origin(origin: Option<&str>, source: &ConfigSource) -> String { + let Some(origin) = origin else { + return format!("{}: ", source.description()); + }; + + // `config::FileSourceFile::resolve()` returns a relative path. Try to + // convert them to absolute paths for easier recognition, falling back + // to the original value on failure. + let canon = Path::new(origin).canonicalize(); + let path = canon.as_deref().unwrap_or_else(|_| Path::new(origin)); + + format!("{} {}: ", source.description(), path.display()) +} diff --git a/cli/src/config.rs b/cli/src/config.rs index 597d33944a..3f6564d0c0 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -38,12 +38,23 @@ pub enum ConfigError { pub enum ConfigSource { Default, Env, - // TODO: Track explicit file paths, especially for when user config is a dir. User, Repo, CommandArg, } +impl ConfigSource { + pub fn description(&self) -> &str { + match self { + ConfigSource::Default => "default", + ConfigSource::Env => "env", + ConfigSource::User => "usercfg", + ConfigSource::Repo => "repocfg", + ConfigSource::CommandArg => "cmdline", + } + } +} + #[derive(Clone, Debug, PartialEq)] pub struct AnnotatedValue { pub path: Vec, diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 323117b30e..d1d9517b3a 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -475,6 +475,10 @@ List variables set in config file, along with their values Possible values: `true`, `false` +* `--show-origin` — Display the source of all listed config values with type (default, env, usercfg, repocfg, command line) and the source file path for usercfg and repocfg + + Possible values: `true`, `false` + * `--user` — Target the user-level config Possible values: `true`, `false` diff --git a/cli/tests/test_config_command.rs b/cli/tests/test_config_command.rs index 71a0574d53..96bd3067ba 100644 --- a/cli/tests/test_config_command.rs +++ b/cli/tests/test_config_command.rs @@ -125,6 +125,72 @@ fn test_config_list_all() { "###); } +#[test] +fn test_config_list_show_origin() { + let mut test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + + // Create multiple user configs. + test_env.add_config( + r#" + user-key-1 = "user-val-1" + "#, + ); + + test_env.add_config( + r#" + user-key-2 = "user-val-2" + "#, + ); + + // Env + test_env.add_env_var("env-key", "env-value"); + + // Repo + test_env.jj_cmd_ok( + &repo_path, + &["config", "set", "--repo", "repo-key", "repo-val"], + ); + + let stdout = test_env.jj_cmd_success( + &repo_path, + &[ + "config", + "list", + "--config-toml", + "cmd-key='cmd-val'", + "--show-origin", + ], + ); + + // Paths starting with `$TEST_ENV` confirm that the relative path returned by + // `Value.origin()` has been converted to an absolute path. + insta::assert_snapshot!(stdout, @r###" + usercfg $TEST_ENV/config/config0001.toml: template-aliases.format_time_range(time_range)="time_range.start() ++ \" - \" ++ time_range.end()" + usercfg $TEST_ENV/config/config0002.toml: user-key-1="user-val-1" + usercfg $TEST_ENV/config/config0003.toml: user-key-2="user-val-2" + repocfg $TEST_ENV/repo/.jj/repo/config.toml: repo-key="repo-val" + env: debug.commit-timestamp="2001-02-03T04:05:09+07:00" + env: debug.operation-timestamp="2001-02-03T04:05:09+07:00" + env: debug.randomness-seed="3" + env: operation.hostname="host.example.com" + env: operation.username="test-username" + env: user.email="test.user@example.com" + env: user.name="Test User" + cmdline: cmd-key="cmd-val" + "###); + + // Run again with defaults shown. Rather than assert the full output which + // will change when any default config value is added or updated, check only + // one value to validate the formatting is correct. + let stdout = test_env.jj_cmd_success( + &repo_path, + &["config", "list", "--include-defaults", "--show-origin"], + ); + assert!(stdout.contains(r#"default: colors.diff header="yellow""#)); +} + #[test] fn test_config_list_layer() { let mut test_env = TestEnvironment::default();