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

Implement "config set" subcommand #1312

Merged
merged 1 commit into from
Mar 1, 2023
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 @@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `jj git fetch` now supports a `--branch` argument to fetch some of the
branches only.

* `jj config set` command allows simple config edits like
`jj config set --repo user.email "[email protected]"`

### Fixed bugs

* Modify/delete conflicts now include context lines
Expand Down
41 changes: 41 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ thiserror = "1.0.38"
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", default-features = false, features = ["std", "ansi", "env-filter", "fmt"] }
indexmap = "1.9.2"
toml_edit = { version = "0.19.1", features = ["serde"] }

[target.'cfg(unix)'.dependencies]
libc = { version = "0.2.139" }
Expand Down
94 changes: 93 additions & 1 deletion src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::Arc;

use clap::builder::{NonEmptyStringValueParser, TypedValueParser, ValueParserFactory};
Expand Down Expand Up @@ -55,10 +56,13 @@ use jujutsu_lib::working_copy::{
use jujutsu_lib::workspace::{Workspace, WorkspaceInitError, WorkspaceLoadError, WorkspaceLoader};
use jujutsu_lib::{dag_walk, file_util, git, revset};
use thiserror::Error;
use toml_edit;
use tracing_subscriber::prelude::*;

use crate::commit_templater;
use crate::config::{AnnotatedValue, CommandNameAndArgs, LayeredConfigs};
use crate::config::{
config_path, AnnotatedValue, CommandNameAndArgs, ConfigSource, LayeredConfigs,
};
use crate::formatter::{Formatter, PlainTextFormatter};
use crate::merge_tools::{ConflictResolveError, DiffEditError};
use crate::template_parser::{TemplateAliasesMap, TemplateParseError};
Expand Down Expand Up @@ -1614,6 +1618,94 @@ pub fn serialize_config_value(value: &config::Value) -> String {
}
}

pub fn write_config_value_to_file(
key: &str,
value_str: &str,
path: &Path,
) -> Result<(), CommandError> {
// Read config
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.
std::io::ErrorKind::NotFound => Ok("".to_string()),
_ => Err(user_error(format!(
"Failed to read file {path}: {err:?}",
path = path.display()
))),
}
})?;
let mut doc = toml_edit::Document::from_str(&config_toml).map_err(|err| {
user_error(format!(
"Failed to parse file {path}: {err:?}",
path = path.display()
))
})?;

// Apply config value
// Iterpret value as string unless it's another simple scalar type.
// TODO(#531): Infer types based on schema (w/ --type arg to override).
let item = match toml_edit::Value::from_str(value_str) {
Ok(value @ toml_edit::Value::Boolean(..))
| Ok(value @ toml_edit::Value::Integer(..))
| Ok(value @ toml_edit::Value::Float(..))
| Ok(value @ toml_edit::Value::String(..)) => toml_edit::value(value),
_ => toml_edit::value(value_str),
};
let mut target_table = doc.as_table_mut();
let mut key_parts_iter = key.split('.');
// Note: split guarantees at least one item.
let last_key_part = key_parts_iter.next_back().unwrap();
for key_part in key_parts_iter {
target_table = target_table
.entry(key_part)
.or_insert_with(|| toml_edit::Item::Table(toml_edit::Table::new()))
.as_table_mut()
.ok_or_else(|| {
user_error(format!(
"Failed to set {key}: would overwrite non-table value with parent table"
dbarnett marked this conversation as resolved.
Show resolved Hide resolved
))
})?;
}
// Error out if overwriting non-scalar value for key (table or array).
match target_table.get(last_key_part) {
None | Some(toml_edit::Item::None) => {}
Some(toml_edit::Item::Value(val)) if !val.is_array() && !val.is_inline_table() => {}
_ => {
return Err(user_error(format!(
"Failed to set {key}: would overwrite entire non-scalar value with scalar"
)))
}
}
target_table[last_key_part] = item;
dbarnett marked this conversation as resolved.
Show resolved Hide resolved

// Write config back
std::fs::write(path, doc.to_string()).map_err(|err| {
user_error(format!(
"Failed to write file {path}: {err:?}",
path = path.display()
))
})
}

pub fn get_config_file_path(
config_source: &ConfigSource,
workspace_loader: &WorkspaceLoader,
) -> Result<PathBuf, CommandError> {
let edit_path = match config_source {
// TODO(#531): Special-case for editors that can't handle viewing directories?
ConfigSource::User => {
config_path()?.ok_or_else(|| user_error("No repo config path found to edit"))?
}
ConfigSource::Repo => workspace_loader.repo_path().join("config.toml"),
_ => {
return Err(user_error(format!(
"Can't get path for config source {config_source:?}"
)));
}
};
Ok(edit_path)
}

pub fn run_ui_editor(settings: &UserSettings, edit_path: &PathBuf) -> Result<(), CommandError> {
let editor: CommandNameAndArgs = settings
.config()
Expand Down
78 changes: 57 additions & 21 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@ use jujutsu_lib::{conflicts, file_util, revset};
use maplit::{hashmap, hashset};

use crate::cli_util::{
self, check_stale_working_copy, print_checkout_stats, resolve_multiple_nonempty_revsets,
resolve_mutliple_nonempty_revsets_flag_guarded, run_ui_editor, serialize_config_value,
short_commit_hash, user_error, user_error_with_hint, Args, CommandError, CommandHelper,
DescriptionArg, RevisionArg, WorkspaceCommandHelper,
self, check_stale_working_copy, get_config_file_path, print_checkout_stats,
resolve_multiple_nonempty_revsets, resolve_mutliple_nonempty_revsets_flag_guarded,
run_ui_editor, serialize_config_value, short_commit_hash, user_error, user_error_with_hint,
write_config_value_to_file, Args, CommandError, CommandHelper, DescriptionArg, RevisionArg,
WorkspaceCommandHelper,
};
use crate::config::{config_path, AnnotatedValue, ConfigSource};
use crate::config::{AnnotatedValue, ConfigSource};
use crate::diff_util::{self, DiffFormat, DiffFormatArgs};
use crate::formatter::{Formatter, PlainTextFormatter};
use crate::graphlog::{get_graphlog, Edge};
Expand Down Expand Up @@ -151,6 +152,19 @@ struct ConfigArgs {
repo: bool,
}

impl ConfigArgs {
fn get_source_kind(&self) -> ConfigSource {
if self.user {
ConfigSource::User
} else if self.repo {
ConfigSource::Repo
} else {
// Shouldn't be reachable unless clap ArgGroup is broken.
panic!("No config_level provided");
}
}
}

/// Manage config options
///
/// Operates on jj configuration, which comes from the config file and
Expand All @@ -161,14 +175,12 @@ struct ConfigArgs {
///
/// 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 and editing config
/// files, but support for setting options is also planned (see
/// https://github.com/martinvonz/jj/issues/531).
#[derive(clap::Subcommand, Clone, Debug)]
enum ConfigSubcommand {
#[command(visible_alias("l"))]
List(ConfigListArgs),
#[command(visible_alias("s"))]
Set(ConfigSetArgs),
#[command(visible_alias("e"))]
Edit(ConfigEditArgs),
}
Expand All @@ -186,6 +198,18 @@ struct ConfigListArgs {
// TODO(#1047): Support ConfigArgs (--user or --repo).
}

/// Update config file to set the given option to a given value.
#[derive(clap::Args, Clone, Debug)]
struct ConfigSetArgs {
#[arg(required = true)]
name: String,
#[arg(required = true)]
value: String,
#[clap(flatten)]
config_args: ConfigArgs,
}

/// Start an editor on a jj config file.
#[derive(clap::Args, Clone, Debug)]
struct ConfigEditArgs {
#[clap(flatten)]
Expand Down Expand Up @@ -1062,6 +1086,7 @@ fn cmd_config(
) -> Result<(), CommandError> {
match subcommand {
ConfigSubcommand::List(sub_args) => cmd_config_list(ui, command, sub_args),
ConfigSubcommand::Set(sub_args) => cmd_config_set(ui, command, sub_args),
ConfigSubcommand::Edit(sub_args) => cmd_config_edit(ui, command, sub_args),
}
}
Expand Down Expand Up @@ -1108,23 +1133,34 @@ fn cmd_config_list(
Ok(())
}

fn cmd_config_set(
_ui: &mut Ui,
command: &CommandHelper,
args: &ConfigSetArgs,
) -> Result<(), CommandError> {
let config_path = get_config_file_path(
&args.config_args.get_source_kind(),
command.workspace_loader()?,
)?;
if config_path.is_dir() {
return Err(user_error(format!(
"Can't set config in path {path} (dirs not supported)",
path = config_path.display()
)));
}
write_config_value_to_file(&args.name, &args.value, &config_path)
}

fn cmd_config_edit(
_ui: &mut Ui,
command: &CommandHelper,
args: &ConfigEditArgs,
) -> Result<(), CommandError> {
let edit_path = if args.config_args.user {
// TODO(#531): Special-case for editors that can't handle viewing directories?
config_path()?.ok_or_else(|| user_error("No repo config path found to edit"))?
} else if args.config_args.repo {
let workspace_loader = command.workspace_loader()?;
workspace_loader.repo_path().join("config.toml")
} else {
// Shouldn't be reachable unless clap ArgGroup is broken.
panic!("No config_level provided");
};
run_ui_editor(command.settings(), &edit_path)?;
Ok(())
let config_path = get_config_file_path(
&args.config_args.get_source_kind(),
command.workspace_loader()?,
)?;
run_ui_editor(command.settings(), &config_path)
}

fn cmd_checkout(
Expand Down
19 changes: 13 additions & 6 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub struct TestEnvironment {
_temp_dir: TempDir,
env_root: PathBuf,
home_dir: PathBuf,
config_dir: PathBuf,
config_path: PathBuf,
env_vars: HashMap<String, String>,
config_file_number: RefCell<i64>,
command_number: RefCell<i64>,
Expand All @@ -45,7 +45,7 @@ impl Default for TestEnvironment {
_temp_dir: tmp_dir,
env_root,
home_dir,
config_dir,
config_path: config_dir,
env_vars,
config_file_number: RefCell::new(0),
command_number: RefCell::new(0),
Expand All @@ -64,7 +64,7 @@ impl TestEnvironment {
}
cmd.env("RUST_BACKTRACE", "1");
cmd.env("HOME", self.home_dir.to_str().unwrap());
cmd.env("JJ_CONFIG", self.config_dir.to_str().unwrap());
cmd.env("JJ_CONFIG", self.config_path.to_str().unwrap());
cmd.env("JJ_USER", "Test User");
cmd.env("JJ_EMAIL", "[email protected]");
cmd.env("JJ_OP_HOSTNAME", "host.example.com");
Expand Down Expand Up @@ -119,18 +119,25 @@ impl TestEnvironment {
&self.home_dir
}

pub fn config_dir(&self) -> &PathBuf {
&self.config_dir
pub fn config_path(&self) -> &PathBuf {
&self.config_path
}

pub fn set_config_path(&mut self, config_path: PathBuf) {
self.config_path = config_path;
}

pub fn add_config(&self, content: &str) {
if self.config_path.is_file() {
panic!("add_config not supported when config_path is a file");
}
// Concatenating two valid TOML files does not (generally) result in a valid
// TOML file, so we use create a new file every time instead.
let mut config_file_number = self.config_file_number.borrow_mut();
*config_file_number += 1;
let config_file_number = *config_file_number;
std::fs::write(
self.config_dir
self.config_path
.join(format!("config{config_file_number:04}.toml")),
content,
)
Expand Down
Loading