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/Cargo.lock b/Cargo.lock index e501321056..9ebb8974ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -440,11 +440,10 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "config" -version = "0.13.4" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ - "async-trait", "lazy_static", "nom", "pathdiff", @@ -1636,7 +1635,7 @@ dependencies = [ "textwrap", "thiserror", "timeago", - "toml_edit", + "toml_edit 0.19.15", "tracing", "tracing-chrome", "tracing-subscriber", @@ -2956,11 +2955,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.11" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.4", ] [[package]] @@ -2985,6 +2987,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.40" diff --git a/Cargo.toml b/Cargo.toml index 3f7f41e483..0b2a4ccdd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ chrono = { version = "0.4.34", default-features = false, features = [ "std", "clock", ] } -config = { version = "0.13.4", default-features = false, features = ["toml"] } +config = { version = "0.14.0", default-features = false, features = ["toml"] } criterion = "0.5.1" crossterm = { version = "0.27", default-features = false } digest = "0.10.7" 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 453d7dabed..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, @@ -584,8 +595,8 @@ mod tests { command_args, CommandNameAndArgs::Structured { env: hashmap! { - "KEY1".to_string() => "value1".to_string(), - "KEY2".to_string() => "value2".to_string(), + "key1".to_string() => "value1".to_string(), + "key2".to_string() => "value2".to_string(), }, command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_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 483c70ad7a..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(); @@ -628,14 +694,16 @@ fn test_config_get() { "###); let stdout = test_env.jj_cmd_failure(test_env.env_root(), &["config", "get", "table.list"]); - insta::assert_snapshot!(stdout, @r###" - Config error: invalid type: sequence, expected a value convertible to a string + insta::with_settings!({filters => vec![(r"config\\config0002.toml", "config/config0002.toml")]}, { + insta::assert_snapshot!(stdout, @r###" + Config error: invalid type: sequence, expected a value convertible to a string for key `table.list` in config/config0002.toml For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md. - "###); + "###) + }); let stdout = test_env.jj_cmd_failure(test_env.env_root(), &["config", "get", "table"]); insta::assert_snapshot!(stdout, @r###" - Config error: invalid type: map, expected a value convertible to a string + Config error: invalid type: map, expected a value convertible to a string for key `table` For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md. "###); diff --git a/cli/tests/test_global_opts.rs b/cli/tests/test_global_opts.rs index 4d7e10fac8..bb4e678779 100644 --- a/cli/tests/test_global_opts.rs +++ b/cli/tests/test_global_opts.rs @@ -386,7 +386,13 @@ fn test_invalid_config() { test_env.add_config("[section]key = value-missing-quotes"); let stderr = test_env.jj_cmd_failure(test_env.env_root(), &["init", "repo"]); insta::assert_snapshot!(stderr.replace('\\', "/"), @r###" - Config error: expected newline, found an identifier at line 1 column 10 in config/config0002.toml + Config error: TOML parse error at line 1, column 10 + | + 1 | [section]key = value-missing-quotes + | ^ + invalid table header + expected newline, `#` + in config/config0002.toml For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md. "###); }