From 8e1b7ed32340f4607e13f96c92d9da97d61167c9 Mon Sep 17 00:00:00 2001 From: Will Chandler Date: Sat, 10 Feb 2024 15:04:38 -0500 Subject: [PATCH] cli: Add `--show-sources` option to `config list` Add a new `--show-sources` 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 003b3ccccf9..8e79d70e019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj git fetch` now accepts `-b` as a shorthand for `--branch`, making it more consistent with other commands that accept a branch +* `jj config list` gained a `--show-sources` flag to display the origin 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 3ed0efbca56..e2bd8259501 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_sources: 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_sources { + 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}:\t"); + }; + + // `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}:{}\t", path.display()) +} diff --git a/cli/src/config.rs b/cli/src/config.rs index 597d33944a5..667911e40a5 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 fmt::Display for ConfigSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfigSource::Default => write!(f, "default"), + ConfigSource::Env => write!(f, "env"), + ConfigSource::User => write!(f, "usercfg"), + ConfigSource::Repo => write!(f, "repocfg"), + ConfigSource::CommandArg => write!(f, "command line"), + } + } +} + #[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 c78cb8916e5..db78121c0a7 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -480,6 +480,10 @@ List variables set in config file, along with their values Possible values: `true`, `false` +* `--show-sources` — 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 530952aac6b..a536a2c761a 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_sources() { + 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-sources", + ], + ); + + // 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" + command line: 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-sources"], + ); + assert!(stdout.contains(r#"default: colors.diff header="yellow""#)); +} + #[test] fn test_config_list_layer() { let mut test_env = TestEnvironment::default();