diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba3d8fda..442bc1968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj git fetch` now supports a `--branch` argument to fetch some of the branches only. +* `jj config get` command allows retrieving config values for use in scripting. + * `jj config set` command allows simple config edits like `jj config set --repo user.email "somebody@example.com"` diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4a2fd4080..338f17a37 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -189,6 +189,8 @@ impl ConfigArgs { enum ConfigSubcommand { #[command(visible_alias("l"))] List(ConfigListArgs), + #[command(visible_alias("g"))] + Get(ConfigGetArgs), #[command(visible_alias("s"))] Set(ConfigSetArgs), #[command(visible_alias("e"))] @@ -208,6 +210,22 @@ struct ConfigListArgs { // TODO(#1047): Support ConfigArgs (--user or --repo). } +/// Get the value of a given config option. +/// +/// Unlike `jj config list`, the result of `jj config get` is printed without +/// extra formatting and therefore is usable in scripting. For example: +/// +/// $ jj config list user.name +/// user.name="Martin von Zweigbergk" +/// $ jj config get user.name +/// Martin von Zweigbergk +#[derive(clap::Args, Clone, Debug)] +#[command(verbatim_doc_comment)] +struct ConfigGetArgs { + #[arg(required = true)] + name: String, +} + /// Update config file to set the given option to a given value. #[derive(clap::Args, Clone, Debug)] struct ConfigSetArgs { @@ -1096,6 +1114,7 @@ fn cmd_config( ) -> Result<(), CommandError> { match subcommand { ConfigSubcommand::List(sub_args) => cmd_config_list(ui, command, sub_args), + ConfigSubcommand::Get(sub_args) => cmd_config_get(ui, command, sub_args), ConfigSubcommand::Set(sub_args) => cmd_config_set(ui, command, sub_args), ConfigSubcommand::Edit(sub_args) => cmd_config_edit(ui, command, sub_args), } @@ -1143,6 +1162,43 @@ fn cmd_config_list( Ok(()) } +fn cmd_config_get( + ui: &mut Ui, + command: &CommandHelper, + args: &ConfigGetArgs, +) -> Result<(), CommandError> { + let value = command + .settings() + .config() + .get_string(&args.name) + .map_err(|err| match err { + config::ConfigError::Type { + origin, + unexpected, + expected, + key, + } => { + let expected = format!("a value convertible to {expected}"); + // Copied from `impl fmt::Display for ConfigError`. We can't use + // the `Display` impl directly because `expected` is required to + // be a `'static str`. + let mut buf = String::new(); + use std::fmt::Write; + write!(buf, "invalid type: {unexpected}, expected {expected}").unwrap(); + if let Some(key) = key { + write!(buf, " for key `{key}`").unwrap(); + } + if let Some(origin) = origin { + write!(buf, " in {origin}").unwrap(); + } + CommandError::ConfigError(buf.to_string()) + } + err => err.into(), + })?; + writeln!(ui, "{value}")?; + Ok(()) +} + fn cmd_config_set( _ui: &mut Ui, command: &CommandHelper, diff --git a/tests/test_config_command.rs b/tests/test_config_command.rs index 78e7a38b5..41bd09e7e 100644 --- a/tests/test_config_command.rs +++ b/tests/test_config_command.rs @@ -438,6 +438,58 @@ fn test_config_edit_repo_outside_repo() { "###); } +#[test] +fn test_config_get() { + let test_env = TestEnvironment::default(); + test_env.add_config( + r###" + [table] + string = "some value 1" + int = 123 + list = ["list", "value"] + overridden = "foo" + "###, + ); + test_env.add_config( + r###" + [table] + overridden = "bar" + "###, + ); + + let stdout = test_env.jj_cmd_failure(test_env.env_root(), &["config", "get", "nonexistent"]); + insta::assert_snapshot!(stdout, @r###" + Config error: configuration property "nonexistent" not found + For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md. + "###); + + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "get", "table.string"]); + insta::assert_snapshot!(stdout, @r###" + some value 1 + "###); + + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "get", "table.int"]); + insta::assert_snapshot!(stdout, @r###" + 123 + "###); + + 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 + 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 + For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md. + "###); + + let stdout = + test_env.jj_cmd_success(test_env.env_root(), &["config", "get", "table.overridden"]); + insta::assert_snapshot!(stdout, @"bar"); +} + fn find_stdout_lines(keyname_pattern: &str, stdout: &str) -> String { let key_line_re = Regex::new(&format!(r"(?m)^{keyname_pattern}=.*$")).unwrap(); key_line_re