From 2fe202d4f859150d395c6ac3bd3bffc7c873ba10 Mon Sep 17 00:00:00 2001 From: David Barnett Date: Tue, 13 Dec 2022 00:22:08 -0600 Subject: [PATCH] Add a "config" command with "get" and "list" subcommands Partially fixes #531. --- src/cli_util.rs | 20 ++++++++++++ src/commands.rs | 63 +++++++++++++++++++++++++++++++++++- tests/test_config_command.rs | 56 ++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 tests/test_config_command.rs diff --git a/src/cli_util.rs b/src/cli_util.rs index 94f8cb031ec..6540babb2ce 100644 --- a/src/cli_util.rs +++ b/src/cli_util.rs @@ -1221,6 +1221,26 @@ pub fn write_commit_summary( Ok(()) } +pub fn write_config_entry( + ui: &mut Ui, + path: &str, + value: config::Value, +) -> Result<(), CommandError> { + match value.kind { + config::ValueKind::Table(table) => { + for (key, table_val) in table { + let key_path = match path { + "" => key, + _ => format!("{path}.{key}"), + }; + write_config_entry(ui, key_path.as_str(), table_val)?; + } + } + _ => writeln!(ui, "{path}={value}")?, + }; + Ok(()) +} + 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 35bbc58c3dd..d66c1d7c61d 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -26,6 +26,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; @@ -54,7 +55,8 @@ use pest::Parser; use crate::cli_util::{ 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, RevisionArg, WorkspaceCommandHelper, + write_commit_summary, write_config_entry, Args, CommandError, CommandHelper, RevisionArg, + WorkspaceCommandHelper, }; use crate::config::FullCommandArgs; use crate::diff_util::{self, DiffFormat, DiffFormatArgs}; @@ -69,6 +71,8 @@ use crate::ui::Ui; enum Commands { Version(VersionArgs), Init(InitArgs), + #[command(subcommand)] + Config(ConfigSubcommand), Checkout(CheckoutArgs), Untrack(UntrackArgs), Files(FilesArgs), @@ -142,6 +146,33 @@ 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 is also planned (see +/// https://github.com/martinvonz/jj/issues/531). +#[derive(clap::Subcommand, Clone, Debug)] +enum ConfigSubcommand { + /// Get the value for a given key. + Get { + /// The config option name + #[arg(required = true, value_parser=NonEmptyStringValueParser::new())] + name: String, + }, + + /// List all variables set in config file, along with their values. + #[command(visible_alias("l"))] + List {}, +} + /// Create a new, empty change and edit it in the working copy /// /// For more information, see @@ -1156,6 +1187,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::Get { name } => { + let raw_value = ui.settings().config().get::(name); + let value = raw_value.map_err(|e| match e { + config::ConfigError::NotFound { .. } => user_error("key not found in config"), + _ => e.into(), + })?; + match value.kind { + config::ValueKind::Table(table) => { + write_config_entry(ui, &name.to_string(), table.into())? + } + _ => writeln!(ui, "{value}")?, + }; + } + ConfigSubcommand::List {} => { + let config_table = ui.settings().config().collect()?; + write_config_entry(ui, "", config_table.into())?; + } + } + + Ok(()) +} + fn cmd_checkout( ui: &mut Ui, command: &CommandHelper, @@ -4214,6 +4274,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 00000000000..e330f2b354c --- /dev/null +++ b/tests/test_config_command.rs @@ -0,0 +1,56 @@ +// 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 regex::Regex; + +use crate::common::TestEnvironment; + +pub mod common; + +#[test] +fn test_config_get() { + // TODO: Populate expected config values explicitly in this file. + let test_env = TestEnvironment::default(); + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "get", "user.name"]); + insta::assert_snapshot!(stdout, @"Test User"); +} + +#[test] +fn test_config_get_table() { + let test_env = TestEnvironment::default(); + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "get", "user"]); + insta::assert_snapshot!( + find_stdout_line("user.name", &stdout), + @"user.name=Test User"); + insta::assert_snapshot!( + find_stdout_line("user.email", &stdout), + @"user.email=test.user@example.com"); +} + +#[test] +fn test_config_list() { + let test_env = TestEnvironment::default(); + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list"]); + insta::assert_snapshot!( + find_stdout_line("user.name", &stdout), + @"user.name=Test User"); + insta::assert_snapshot!( + find_stdout_line("operation.hostname", &stdout), + @"operation.hostname=host.example.com"); +} + +fn find_stdout_line<'a>(keyname: &str, stdout: &'a str) -> &'a str { + let key_line_re = Regex::new(&format!(r"(?m)^{keyname}=.*$")).unwrap(); + key_line_re.find(stdout).unwrap().as_str() +}