diff --git a/CHANGELOG.md b/CHANGELOG.md index e7d3325fe9..80c6fd79f1 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 push` now accepts multiple `--branch`/`--change` arguments +* `jj config list` command prints values from config (with other subcommands + coming soon). + ### Fixed bugs * When sharing the working copy with a Git repo, we used to forget to export @@ -39,6 +42,7 @@ Thanks to the people who made this release happen! * Danny Hooper (hooper@google.com) * Yuya Nishihara (@yuja) * Ilya Grigoriev (@ilyagr) + * David Barnett (@dbarnett) ## [0.6.1] - 2022-12-05 diff --git a/src/cli_util.rs b/src/cli_util.rs index 58962d6198..d24c24c189 100644 --- a/src/cli_util.rs +++ b/src/cli_util.rs @@ -1234,6 +1234,49 @@ pub fn write_commit_summary( Ok(()) } +pub fn write_config_entry( + ui: &mut Ui, + path: &str, + value: config::Value, +) -> Result<(), CommandError> { + match value.kind { + // Handle table values specially to render each child nicely on its own line. + config::ValueKind::Table(table) => { + // TODO: Remove sorting when config crate maintains deterministic ordering. + for (key, table_val) in table.into_iter().sorted_by_key(|(k, _)| k.to_owned()) { + let key_path = match path { + "" => key, + _ => format!("{path}.{key}"), + }; + write_config_entry(ui, key_path.as_str(), table_val)?; + } + } + _ => writeln!(ui, "{path}={}", serialize_config_value(value))?, + }; + Ok(()) +} + +// TODO: Use a proper TOML library to serialize instead. +fn serialize_config_value(value: config::Value) -> String { + match value.kind { + config::ValueKind::Table(table) => format!( + "{{{}}}", + // TODO: Remove sorting when config crate maintains deterministic ordering. + table + .into_iter() + .sorted_by_key(|(k, _)| k.to_owned()) + .map(|(k, v)| format!("{k}={}", serialize_config_value(v))) + .join(", ") + ), + config::ValueKind::Array(vals) => format!( + "[{}]", + vals.into_iter().map(serialize_config_value).join(", ") + ), + config::ValueKind::String(val) => format!("{val:?}"), + _ => value.to_string(), + } +} + pub fn short_commit_description(commit: &Commit) -> String { let first_line = commit.description().split('\n').next().unwrap(); format!("{} ({})", short_commit_hash(commit.id()), first_line) diff --git a/src/commands.rs b/src/commands.rs index 3f937720e1..7ed61e5f53 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -25,6 +25,7 @@ use std::{fs, io}; use chrono::{FixedOffset, LocalResult, TimeZone, Utc}; use clap::builder::NonEmptyStringValueParser; use clap::{ArgGroup, ArgMatches, CommandFactory, FromArgMatches, Subcommand}; +use config::Source; use itertools::Itertools; use jujutsu_lib::backend::{CommitId, Timestamp, TreeValue}; use jujutsu_lib::commit::Commit; @@ -53,8 +54,8 @@ use pest::Parser; use crate::cli_util::{ self, check_stale_working_copy, print_checkout_stats, print_failed_git_export, resolve_base_revs, short_commit_description, short_commit_hash, user_error, - user_error_with_hint, write_commit_summary, Args, CommandError, CommandHelper, DescriptionArg, - RevisionArg, WorkspaceCommandHelper, + user_error_with_hint, write_commit_summary, write_config_entry, Args, CommandError, + CommandHelper, DescriptionArg, RevisionArg, WorkspaceCommandHelper, }; use crate::config::FullCommandArgs; use crate::diff_util::{self, DiffFormat, DiffFormatArgs}; @@ -69,6 +70,8 @@ use crate::ui::Ui; enum Commands { Version(VersionArgs), Init(InitArgs), + #[command(subcommand)] + Config(ConfigSubcommand), Checkout(CheckoutArgs), Untrack(UntrackArgs), Files(FilesArgs), @@ -142,6 +145,31 @@ struct InitArgs { git_repo: Option, } +/// Get config options +/// +/// Operates on jj configuration, which comes from the config file and +/// environment variables. Uses the config file at ~/.jjconfig.toml or +/// $XDG_CONFIG_HOME/jj/config.toml, unless overridden with the JJ_CONFIG +/// environment variable. +/// +/// For supported config options and more details about jj config, see +/// https://github.com/martinvonz/jj/blob/main/docs/config.md. +/// +/// Note: Currently only supports getting config options, but support for +/// setting options and editing config files is also planned (see +/// https://github.com/martinvonz/jj/issues/531). +#[derive(clap::Subcommand, Clone, Debug)] +enum ConfigSubcommand { + /// List variables set in config file, along with their values. + #[command(visible_alias("l"))] + List { + /// An optional name of a specific config option to look up. + #[arg(value_parser=NonEmptyStringValueParser::new())] + name: Option, + // TODO: Support --show-origin once mehcode/config-rs#319 is done. + }, +} + /// Create a new, empty change and edit it in the working copy /// /// For more information, see @@ -1172,6 +1200,35 @@ Set `ui.allow-init-native` to allow initializing a repo with the native backend. Ok(()) } +fn cmd_config( + ui: &mut Ui, + _command: &CommandHelper, + subcommand: &ConfigSubcommand, +) -> Result<(), CommandError> { + ui.request_pager(); + match subcommand { + ConfigSubcommand::List { name } => { + let raw_values = match name { + Some(name) => { + ui.settings() + .config() + .get::(name) + .map_err(|e| match e { + config::ConfigError::NotFound { .. } => { + user_error("key not found in config") + } + _ => e.into(), + })? + } + None => ui.settings().config().collect()?.into(), + }; + write_config_entry(ui, name.as_deref().unwrap_or(""), raw_values)?; + } + } + + Ok(()) +} + fn cmd_checkout( ui: &mut Ui, command: &CommandHelper, @@ -4274,6 +4331,7 @@ pub fn run_command( match &derived_subcommands { Commands::Version(sub_args) => cmd_version(ui, command_helper, sub_args), Commands::Init(sub_args) => cmd_init(ui, command_helper, sub_args), + Commands::Config(sub_args) => cmd_config(ui, command_helper, sub_args), Commands::Checkout(sub_args) => cmd_checkout(ui, command_helper, sub_args), Commands::Untrack(sub_args) => cmd_untrack(ui, command_helper, sub_args), Commands::Files(sub_args) => cmd_files(ui, command_helper, sub_args), diff --git a/tests/test_config_command.rs b/tests/test_config_command.rs new file mode 100644 index 0000000000..82927585a7 --- /dev/null +++ b/tests/test_config_command.rs @@ -0,0 +1,128 @@ +// Copyright 2022 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use itertools::Itertools; +use regex::Regex; + +use crate::common::TestEnvironment; + +pub mod common; + +#[test] +fn test_config_list_single() { + let test_env = TestEnvironment::default(); + test_env.add_config( + r###" + [test-table] + somekey = "some value" + "### + .as_bytes(), + ); + + let stdout = test_env.jj_cmd_success( + test_env.env_root(), + &["config", "list", "test-table.somekey"], + ); + insta::assert_snapshot!(stdout, @r###" + test-table.somekey="some value" + "###); +} + +#[test] +fn test_config_list_table() { + let test_env = TestEnvironment::default(); + test_env.add_config( + r###" + [test-table] + x = true + y.foo = "abc" + y.bar = 123 + "### + .as_bytes(), + ); + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list", "test-table"]); + insta::assert_snapshot!( + stdout, + @r###" + test-table.x=true + test-table.y.bar=123 + test-table.y.foo="abc" + "###); +} + +#[test] +fn test_config_list_array() { + let test_env = TestEnvironment::default(); + test_env.add_config( + r###" + test-array = [1, "b", 3.4] + "### + .as_bytes(), + ); + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list", "test-array"]); + insta::assert_snapshot!(stdout, @r###" + test-array=[1, "b", 3.4] + "###); +} + +#[test] +fn test_config_list_inline_table() { + let test_env = TestEnvironment::default(); + test_env.add_config( + r###" + [[test-table]] + x = 1 + [[test-table]] + y = ["z"] + "### + .as_bytes(), + ); + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list", "test-table"]); + insta::assert_snapshot!(stdout, @r###" + test-table=[{x=1}, {y=["z"]}] + "###); +} + +#[test] +fn test_config_list_all() { + let test_env = TestEnvironment::default(); + test_env.add_config( + r###" + test-val = [1, 2, 3] + [test-table] + x = true + y.foo = "abc" + y.bar = 123 + "### + .as_bytes(), + ); + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list"]); + insta::assert_snapshot!( + find_stdout_lines(r"(test-val|test-table\b[^=]*)", &stdout), + @r###" + test-table.x=true + test-table.y.bar=123 + test-table.y.foo="abc" + test-val=[1, 2, 3] + "###); +} + +fn find_stdout_lines<'a>(keyname_pattern: &str, stdout: &str) -> String { + let key_line_re = Regex::new(&format!(r"(?m)^{}=.*$", keyname_pattern)).unwrap(); + key_line_re + .find_iter(stdout) + .map(|m| m.as_str()) + .collect_vec() + .join("\n") +}