Skip to content

Commit 99cb0ba

Browse files
committed
Implement "config set" subcommand
Uses toml_edit to support simple config edits like: jj config set --repo user.email "somebody@example.com"
1 parent bbd6ef0 commit 99cb0ba

File tree

7 files changed

+326
-29
lines changed

7 files changed

+326
-29
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
* `jj git fetch` now supports a `--branch` argument to fetch some of the
2727
branches only.
2828

29+
* `jj config set` command allows simple config edits like
30+
`jj config set --repo user.email "somebody@example.com"`
31+
2932
### Fixed bugs
3033

3134
* Modify/delete conflicts now include context lines

Cargo.lock

+41
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ thiserror = "1.0.38"
6262
tracing = "0.1.37"
6363
tracing-subscriber = { version = "0.3.16", default-features = false, features = ["std", "ansi", "env-filter", "fmt"] }
6464
indexmap = "1.9.2"
65+
toml_edit = { version = "0.19.1", features = ["serde"] }
6566

6667
[target.'cfg(unix)'.dependencies]
6768
libc = { version = "0.2.139" }

src/cli_util.rs

+93-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::ops::Deref;
2121
use std::path::{Path, PathBuf};
2222
use std::process::ExitCode;
2323
use std::rc::Rc;
24+
use std::str::FromStr;
2425
use std::sync::Arc;
2526

2627
use clap::builder::{NonEmptyStringValueParser, TypedValueParser, ValueParserFactory};
@@ -55,10 +56,13 @@ use jujutsu_lib::working_copy::{
5556
use jujutsu_lib::workspace::{Workspace, WorkspaceInitError, WorkspaceLoadError, WorkspaceLoader};
5657
use jujutsu_lib::{dag_walk, file_util, git, revset};
5758
use thiserror::Error;
59+
use toml_edit;
5860
use tracing_subscriber::prelude::*;
5961

6062
use crate::commit_templater;
61-
use crate::config::{AnnotatedValue, CommandNameAndArgs, LayeredConfigs};
63+
use crate::config::{
64+
config_path, AnnotatedValue, CommandNameAndArgs, ConfigSource, LayeredConfigs,
65+
};
6266
use crate::formatter::{Formatter, PlainTextFormatter};
6367
use crate::merge_tools::{ConflictResolveError, DiffEditError};
6468
use crate::template_parser::{TemplateAliasesMap, TemplateParseError};
@@ -1633,6 +1637,94 @@ pub fn serialize_config_value(value: &config::Value) -> String {
16331637
}
16341638
}
16351639

1640+
pub fn write_config_value_to_file(
1641+
key: &str,
1642+
value_str: &str,
1643+
path: &Path,
1644+
) -> Result<(), CommandError> {
1645+
// Read config
1646+
let config_toml = std::fs::read_to_string(path).or_else(|err| {
1647+
match err.kind() {
1648+
// If config doesn't exist yet, read as empty and we'll write one.
1649+
std::io::ErrorKind::NotFound => Ok("".to_string()),
1650+
_ => Err(user_error(format!(
1651+
"Failed to read file {path}: {err:?}",
1652+
path = path.display()
1653+
))),
1654+
}
1655+
})?;
1656+
let mut doc = toml_edit::Document::from_str(&config_toml).map_err(|err| {
1657+
user_error(format!(
1658+
"Failed to parse file {path}: {err:?}",
1659+
path = path.display()
1660+
))
1661+
})?;
1662+
1663+
// Apply config value
1664+
// Iterpret value as string unless it's another simple scalar type.
1665+
// TODO(#531): Infer types based on schema (w/ --type arg to override).
1666+
let item = match toml_edit::Value::from_str(value_str) {
1667+
Ok(value @ toml_edit::Value::Boolean(..))
1668+
| Ok(value @ toml_edit::Value::Integer(..))
1669+
| Ok(value @ toml_edit::Value::Float(..))
1670+
| Ok(value @ toml_edit::Value::String(..)) => toml_edit::value(value),
1671+
_ => toml_edit::value(value_str),
1672+
};
1673+
let mut target_table = doc.as_table_mut();
1674+
let mut key_parts_iter = key.split('.');
1675+
// Note: split guarantees at least one item.
1676+
let last_key_part = key_parts_iter.next_back().unwrap();
1677+
for key_part in key_parts_iter {
1678+
target_table = target_table
1679+
.entry(key_part)
1680+
.or_insert_with(|| toml_edit::Item::Table(toml_edit::Table::new()))
1681+
.as_table_mut()
1682+
.ok_or_else(|| {
1683+
user_error(format!(
1684+
"Failed to set {key}: would overwrite non-table value with parent table"
1685+
))
1686+
})?;
1687+
}
1688+
// Error out if overwriting non-scalar value for key (table or array).
1689+
match target_table.get(last_key_part) {
1690+
None | Some(toml_edit::Item::None) => {}
1691+
Some(toml_edit::Item::Value(val)) if !val.is_array() && !val.is_inline_table() => {}
1692+
_ => {
1693+
return Err(user_error(format!(
1694+
"Failed to set {key}: would overwrite entire non-scalar value with scalar"
1695+
)))
1696+
}
1697+
}
1698+
target_table[last_key_part] = item;
1699+
1700+
// Write config back
1701+
std::fs::write(path, doc.to_string()).map_err(|err| {
1702+
user_error(format!(
1703+
"Failed to write file {path}: {err:?}",
1704+
path = path.display()
1705+
))
1706+
})
1707+
}
1708+
1709+
pub fn get_config_file_path(
1710+
config_source: &ConfigSource,
1711+
workspace_loader: &WorkspaceLoader,
1712+
) -> Result<PathBuf, CommandError> {
1713+
let edit_path = match config_source {
1714+
// TODO(#531): Special-case for editors that can't handle viewing directories?
1715+
ConfigSource::User => {
1716+
config_path()?.ok_or_else(|| user_error("No repo config path found to edit"))?
1717+
}
1718+
ConfigSource::Repo => workspace_loader.repo_path().join("config.toml"),
1719+
_ => {
1720+
return Err(user_error(format!(
1721+
"Can't get path for config source {config_source:?}"
1722+
)));
1723+
}
1724+
};
1725+
Ok(edit_path)
1726+
}
1727+
16361728
pub fn run_ui_editor(settings: &UserSettings, edit_path: &PathBuf) -> Result<(), CommandError> {
16371729
let editor: CommandNameAndArgs = settings
16381730
.config()

src/commands/mod.rs

+57-21
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,13 @@ use jujutsu_lib::{conflicts, file_util, revset};
4949
use maplit::{hashmap, hashset};
5050

5151
use crate::cli_util::{
52-
self, check_stale_working_copy, print_checkout_stats, resolve_multiple_nonempty_revsets,
53-
resolve_mutliple_nonempty_revsets_flag_guarded, run_ui_editor, serialize_config_value,
54-
short_commit_hash, user_error, user_error_with_hint, Args, CommandError, CommandHelper,
55-
DescriptionArg, RevisionArg, WorkspaceCommandHelper,
52+
self, check_stale_working_copy, get_config_file_path, print_checkout_stats,
53+
resolve_multiple_nonempty_revsets, resolve_mutliple_nonempty_revsets_flag_guarded,
54+
run_ui_editor, serialize_config_value, short_commit_hash, user_error, user_error_with_hint,
55+
write_config_value_to_file, Args, CommandError, CommandHelper, DescriptionArg, RevisionArg,
56+
WorkspaceCommandHelper,
5657
};
57-
use crate::config::{config_path, AnnotatedValue, ConfigSource};
58+
use crate::config::{AnnotatedValue, ConfigSource};
5859
use crate::diff_util::{self, DiffFormat, DiffFormatArgs};
5960
use crate::formatter::{Formatter, PlainTextFormatter};
6061
use crate::graphlog::{get_graphlog, Edge};
@@ -153,6 +154,19 @@ struct ConfigArgs {
153154
repo: bool,
154155
}
155156

157+
impl ConfigArgs {
158+
fn get_source_kind(&self) -> ConfigSource {
159+
if self.user {
160+
ConfigSource::User
161+
} else if self.repo {
162+
ConfigSource::Repo
163+
} else {
164+
// Shouldn't be reachable unless clap ArgGroup is broken.
165+
panic!("No config_level provided");
166+
}
167+
}
168+
}
169+
156170
/// Manage config options
157171
///
158172
/// Operates on jj configuration, which comes from the config file and
@@ -163,14 +177,12 @@ struct ConfigArgs {
163177
///
164178
/// For supported config options and more details about jj config, see
165179
/// https://github.com/martinvonz/jj/blob/main/docs/config.md.
166-
///
167-
/// Note: Currently only supports getting config options and editing config
168-
/// files, but support for setting options is also planned (see
169-
/// https://github.com/martinvonz/jj/issues/531).
170180
#[derive(clap::Subcommand, Clone, Debug)]
171181
enum ConfigSubcommand {
172182
#[command(visible_alias("l"))]
173183
List(ConfigListArgs),
184+
#[command(visible_alias("s"))]
185+
Set(ConfigSetArgs),
174186
#[command(visible_alias("e"))]
175187
Edit(ConfigEditArgs),
176188
}
@@ -188,6 +200,18 @@ struct ConfigListArgs {
188200
// TODO(#1047): Support ConfigArgs (--user or --repo).
189201
}
190202

203+
/// Update config file to set the given option to a given value.
204+
#[derive(clap::Args, Clone, Debug)]
205+
struct ConfigSetArgs {
206+
#[arg(required = true)]
207+
name: String,
208+
#[arg(required = true)]
209+
value: String,
210+
#[clap(flatten)]
211+
config_args: ConfigArgs,
212+
}
213+
214+
/// Start an editor on a jj config file.
191215
#[derive(clap::Args, Clone, Debug)]
192216
struct ConfigEditArgs {
193217
#[clap(flatten)]
@@ -1064,6 +1088,7 @@ fn cmd_config(
10641088
) -> Result<(), CommandError> {
10651089
match subcommand {
10661090
ConfigSubcommand::List(sub_args) => cmd_config_list(ui, command, sub_args),
1091+
ConfigSubcommand::Set(sub_args) => cmd_config_set(ui, command, sub_args),
10671092
ConfigSubcommand::Edit(sub_args) => cmd_config_edit(ui, command, sub_args),
10681093
}
10691094
}
@@ -1110,23 +1135,34 @@ fn cmd_config_list(
11101135
Ok(())
11111136
}
11121137

1138+
fn cmd_config_set(
1139+
_ui: &mut Ui,
1140+
command: &CommandHelper,
1141+
args: &ConfigSetArgs,
1142+
) -> Result<(), CommandError> {
1143+
let config_path = get_config_file_path(
1144+
&args.config_args.get_source_kind(),
1145+
command.workspace_loader()?,
1146+
)?;
1147+
if config_path.is_dir() {
1148+
return Err(user_error(format!(
1149+
"Can't set config in path {path} (dirs not supported)",
1150+
path = config_path.display()
1151+
)));
1152+
}
1153+
write_config_value_to_file(&args.name, &args.value, &config_path)
1154+
}
1155+
11131156
fn cmd_config_edit(
11141157
_ui: &mut Ui,
11151158
command: &CommandHelper,
11161159
args: &ConfigEditArgs,
11171160
) -> Result<(), CommandError> {
1118-
let edit_path = if args.config_args.user {
1119-
// TODO(#531): Special-case for editors that can't handle viewing directories?
1120-
config_path()?.ok_or_else(|| user_error("No repo config path found to edit"))?
1121-
} else if args.config_args.repo {
1122-
let workspace_loader = command.workspace_loader()?;
1123-
workspace_loader.repo_path().join("config.toml")
1124-
} else {
1125-
// Shouldn't be reachable unless clap ArgGroup is broken.
1126-
panic!("No config_level provided");
1127-
};
1128-
run_ui_editor(command.settings(), &edit_path)?;
1129-
Ok(())
1161+
let config_path = get_config_file_path(
1162+
&args.config_args.get_source_kind(),
1163+
command.workspace_loader()?,
1164+
)?;
1165+
run_ui_editor(command.settings(), &config_path)
11301166
}
11311167

11321168
fn cmd_checkout(

tests/common/mod.rs

+13-6
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub struct TestEnvironment {
2424
_temp_dir: TempDir,
2525
env_root: PathBuf,
2626
home_dir: PathBuf,
27-
config_dir: PathBuf,
27+
config_path: PathBuf,
2828
env_vars: HashMap<String, String>,
2929
config_file_number: RefCell<i64>,
3030
command_number: RefCell<i64>,
@@ -45,7 +45,7 @@ impl Default for TestEnvironment {
4545
_temp_dir: tmp_dir,
4646
env_root,
4747
home_dir,
48-
config_dir,
48+
config_path: config_dir,
4949
env_vars,
5050
config_file_number: RefCell::new(0),
5151
command_number: RefCell::new(0),
@@ -64,7 +64,7 @@ impl TestEnvironment {
6464
}
6565
cmd.env("RUST_BACKTRACE", "1");
6666
cmd.env("HOME", self.home_dir.to_str().unwrap());
67-
cmd.env("JJ_CONFIG", self.config_dir.to_str().unwrap());
67+
cmd.env("JJ_CONFIG", self.config_path.to_str().unwrap());
6868
cmd.env("JJ_USER", "Test User");
6969
cmd.env("JJ_EMAIL", "test.user@example.com");
7070
cmd.env("JJ_OP_HOSTNAME", "host.example.com");
@@ -119,18 +119,25 @@ impl TestEnvironment {
119119
&self.home_dir
120120
}
121121

122-
pub fn config_dir(&self) -> &PathBuf {
123-
&self.config_dir
122+
pub fn config_path(&self) -> &PathBuf {
123+
&self.config_path
124+
}
125+
126+
pub fn set_config_path(&mut self, config_path: PathBuf) {
127+
self.config_path = config_path;
124128
}
125129

126130
pub fn add_config(&self, content: &str) {
131+
if self.config_path.is_file() {
132+
panic!("add_config not supported when config_path is a file");
133+
}
127134
// Concatenating two valid TOML files does not (generally) result in a valid
128135
// TOML file, so we use create a new file every time instead.
129136
let mut config_file_number = self.config_file_number.borrow_mut();
130137
*config_file_number += 1;
131138
let config_file_number = *config_file_number;
132139
std::fs::write(
133-
self.config_dir
140+
self.config_path
134141
.join(format!("config{config_file_number:04}.toml")),
135142
content,
136143
)

0 commit comments

Comments
 (0)