Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new command jj config unset #4547

Merged
merged 2 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions cli/src/commands/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod get;
mod list;
mod path;
mod set;
mod unset;

use tracing::instrument;

Expand All @@ -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;
Expand Down Expand Up @@ -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)]
Expand 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),
}
}
50 changes: 50 additions & 0 deletions cli/src/commands/config/unset.rs
pylbrecht marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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.
pylbrecht marked this conversation as resolved.
Show resolved Hide resolved
#[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)
}
64 changes: 49 additions & 15 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,12 +583,7 @@ fn read_config_path(config_path: &Path) -> Result<config::Config, config::Config
.build()
}

pub fn write_config_value_to_file(
key: &ConfigNamePathBuf,
value: toml_edit::Value,
path: &Path,
) -> Result<(), CommandError> {
// Read config
fn read_config(path: &Path) -> Result<toml_edit::Document, CommandError> {
let config_toml = std::fs::read_to_string(path).or_else(|err| {
match err.kind() {
// If config doesn't exist yet, read as empty and we'll write one.
Expand All @@ -599,12 +594,29 @@ pub fn write_config_value_to_file(
)),
}
})?;
let mut doc: toml_edit::Document = config_toml.parse().map_err(|err| {
config_toml.parse().map_err(|err| {
user_error_with_message(
format!("Failed to parse file {path}", path = path.display()),
err,
)
})?;
})
}

fn write_config(path: &Path, doc: &toml_edit::Document) -> Result<(), CommandError> {
std::fs::write(path, doc.to_string()).map_err(|err| {
user_error_with_message(
format!("Failed to write file {path}", path = path.display()),
err,
)
})
}

pub fn write_config_value_to_file(
key: &ConfigNamePathBuf,
value: toml_edit::Value,
path: &Path,
) -> Result<(), CommandError> {
let mut doc = read_config(path)?;

// Apply config value
let mut target_table = doc.as_table_mut();
Expand Down Expand Up @@ -633,13 +645,35 @@ pub fn write_config_value_to_file(
}
target_table[last_key_part] = toml_edit::Item::Value(value);

// Write config back
std::fs::write(path, doc.to_string()).map_err(|err| {
user_error_with_message(
format!("Failed to write file {path}", path = path.display()),
err,
)
})
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.
Expand Down
19 changes: 19 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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> <NAME>`
###### **Arguments:**
* `<NAME>`
###### **Options:**
* `--user` — Target the user-level config
* `--repo` — Target the repo-level config
## `jj describe`
Update the change description or other metadata
Expand Down
87 changes: 87 additions & 0 deletions cli/tests/test_config_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
pylbrecht marked this conversation as resolved.
Show resolved Hide resolved

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();
Expand Down
Loading