diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a553760b..0d97601187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * Add a new template alias `bultin_log_compact_full_description()`. +* New command `jj config unset` that unsets config values. For example, + `jj config unset --user user.name`. + ### Fixed bugs * Error on `trunk()` revset resolution is now handled gracefully. diff --git a/cli/src/commands/config/mod.rs b/cli/src/commands/config/mod.rs index e6d2ff96e3..3ce0955613 100644 --- a/cli/src/commands/config/mod.rs +++ b/cli/src/commands/config/mod.rs @@ -17,6 +17,7 @@ mod get; mod list; mod path; mod set; +mod unset; use tracing::instrument; @@ -30,6 +31,8 @@ use self::path::cmd_config_path; use self::path::ConfigPathArgs; use self::set::cmd_config_set; use self::set::ConfigSetArgs; +use self::unset::cmd_config_unset; +use self::unset::ConfigUnsetArgs; use crate::cli_util::CommandHelper; use crate::command_error::CommandError; use crate::config::ConfigSource; @@ -82,6 +85,8 @@ pub(crate) enum ConfigCommand { Path(ConfigPathArgs), #[command(visible_alias("s"))] Set(ConfigSetArgs), + #[command(visible_alias("u"))] + Unset(ConfigUnsetArgs), } #[instrument(skip_all)] @@ -96,5 +101,6 @@ pub(crate) fn cmd_config( ConfigCommand::List(args) => cmd_config_list(ui, command, args), ConfigCommand::Path(args) => cmd_config_path(ui, command, args), ConfigCommand::Set(args) => cmd_config_set(ui, command, args), + ConfigCommand::Unset(args) => cmd_config_unset(ui, command, args), } } diff --git a/cli/src/commands/config/unset.rs b/cli/src/commands/config/unset.rs new file mode 100644 index 0000000000..46a8446f39 --- /dev/null +++ b/cli/src/commands/config/unset.rs @@ -0,0 +1,50 @@ +// Copyright 2024 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 tracing::instrument; + +use super::ConfigLevelArgs; +use crate::cli_util::get_new_config_file_path; +use crate::cli_util::CommandHelper; +use crate::command_error::user_error; +use crate::command_error::CommandError; +use crate::config::remove_config_value_from_file; +use crate::config::ConfigNamePathBuf; +use crate::ui::Ui; + +/// Update config file to unset the given option. +#[derive(clap::Args, Clone, Debug)] +pub struct ConfigUnsetArgs { + #[arg(required = true)] + name: ConfigNamePathBuf, + #[command(flatten)] + level: ConfigLevelArgs, +} + +#[instrument(skip_all)] +pub fn cmd_config_unset( + _ui: &mut Ui, + command: &CommandHelper, + args: &ConfigUnsetArgs, +) -> Result<(), CommandError> { + let config_path = get_new_config_file_path(&args.level.expect_source_kind(), command)?; + if config_path.is_dir() { + return Err(user_error(format!( + "Can't set config in path {path} (dirs not supported)", + path = config_path.display() + ))); + } + + remove_config_value_from_file(&args.name, &config_path) +} diff --git a/cli/src/config.rs b/cli/src/config.rs index 8391600004..5bdda27c5d 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -648,6 +648,34 @@ pub fn write_config_value_to_file( write_config(path, &doc) } +pub fn remove_config_value_from_file( + key: &ConfigNamePathBuf, + path: &Path, +) -> Result<(), CommandError> { + let mut doc = read_config(path)?; + + // Find target table + let mut key_iter = key.components(); + let last_key = key_iter.next_back().expect("key must not be empty"); + let target_table = key_iter.try_fold(doc.as_table_mut(), |table, key| { + table + .get_mut(key) + .ok_or_else(|| config::ConfigError::NotFound(key.to_string())) + .and_then(|table| { + table.as_table_mut().ok_or_else(|| { + config::ConfigError::Message(format!(r#""{key}" is not a table"#)) + }) + }) + })?; + + // Remove config value + target_table + .remove(last_key) + .ok_or_else(|| config::ConfigError::NotFound(key.to_string()))?; + + write_config(path, &doc) +} + /// Command name and arguments specified by config. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)] #[serde(untagged)] diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 0589b9a41d..3a3f4f788e 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -30,6 +30,7 @@ This document contains the help content for the `jj` command-line program. * [`jj config list`↴](#jj-config-list) * [`jj config path`↴](#jj-config-path) * [`jj config set`↴](#jj-config-set) +* [`jj config unset`↴](#jj-config-unset) * [`jj describe`↴](#jj-describe) * [`jj diff`↴](#jj-diff) * [`jj diffedit`↴](#jj-diffedit) @@ -479,6 +480,7 @@ For file locations, supported config options, and other details about jj config, * `list` — List variables set in config file, along with their values * `path` — Print the path to the config file * `set` — Update config file to set the given option to a given value +* `unset` — Update config file to unset the given option @@ -580,6 +582,23 @@ Update config file to set the given option to a given value +## `jj config unset` + +Update config file to unset the given option + +**Usage:** `jj config unset <--user|--repo> ` + +###### **Arguments:** + +* `` + +###### **Options:** + +* `--user` — Target the user-level config +* `--repo` — Target the repo-level config + + + ## `jj describe` Update the change description or other metadata diff --git a/cli/tests/test_config_command.rs b/cli/tests/test_config_command.rs index 4740baa5aa..f5d389b087 100644 --- a/cli/tests/test_config_command.rs +++ b/cli/tests/test_config_command.rs @@ -602,6 +602,93 @@ fn test_config_set_nontable_parent() { "###); } +#[test] +fn test_config_unset_non_existent_key() { + let mut test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + // Point to a config file since `config set` can't handle directories. + let user_config_path = test_env.config_path().join("config.toml"); + test_env.set_config_path(user_config_path); + let repo_path = test_env.env_root().join("repo"); + + let stderr = test_env.jj_cmd_failure(&repo_path, &["config", "unset", "--user", "nonexistent"]); + insta::assert_snapshot!(stderr, @r###" + Config error: configuration property "nonexistent" not found + For help, see https://martinvonz.github.io/jj/latest/config/. + "###); +} + +#[test] +fn test_config_unset_inline_table_key() { + let mut test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + // Point to a config file since `config set` can't handle directories. + let user_config_path = test_env.config_path().join("config.toml"); + test_env.set_config_path(user_config_path.clone()); + let repo_path = test_env.env_root().join("repo"); + + test_env.jj_cmd_ok( + &repo_path, + &["config", "set", "--user", "inline-table", "{ foo = true }"], + ); + let stderr = test_env.jj_cmd_failure( + &repo_path, + &["config", "unset", "--user", "inline-table.foo"], + ); + + insta::assert_snapshot!(stderr, @r###" + Config error: "inline-table" is not a table + For help, see https://martinvonz.github.io/jj/latest/config/. + "###); +} + +#[test] +fn test_config_unset_for_user() { + let mut test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + // Point to a config file since `config set` can't handle directories. + let user_config_path = test_env.config_path().join("config.toml"); + test_env.set_config_path(user_config_path.clone()); + let repo_path = test_env.env_root().join("repo"); + + test_env.jj_cmd_ok(&repo_path, &["config", "set", "--user", "foo", "true"]); + test_env.jj_cmd_ok(&repo_path, &["config", "unset", "--user", "foo"]); + + test_env.jj_cmd_ok( + &repo_path, + &["config", "set", "--user", "table.foo", "true"], + ); + test_env.jj_cmd_ok(&repo_path, &["config", "unset", "--user", "table.foo"]); + + test_env.jj_cmd_ok( + &repo_path, + &["config", "set", "--user", "table.inline", "{ foo = true }"], + ); + test_env.jj_cmd_ok(&repo_path, &["config", "unset", "--user", "table.inline"]); + + let user_config_toml = std::fs::read_to_string(&user_config_path).unwrap(); + insta::assert_snapshot!(user_config_toml, @r#" + [table] + "#); +} + +#[test] +fn test_config_unset_for_repo() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + test_env.jj_cmd_ok( + &repo_path, + &["config", "set", "--repo", "test-key", "test-val"], + ); + test_env.jj_cmd_ok(&repo_path, &["config", "unset", "--repo", "test-key"]); + + let repo_config_path = repo_path.join(".jj/repo/config.toml"); + let repo_config_toml = std::fs::read_to_string(&repo_config_path).unwrap(); + insta::assert_snapshot!(repo_config_toml, @""); +} + #[test] fn test_config_edit_missing_opt() { let test_env = TestEnvironment::default();