Skip to content

Commit fca92f1

Browse files
committed
cli: add --config-file=PATH argument
This would be useful for scripting purpose. Maybe we can also replace the current --config-toml=<TOML> use cases by --config-file=<PATH> and simpler --config=<KEY>=<VALUE>. #4926 (comment) If we want to add more source variants (such as fd number), it might be better to add --config-from=<type>:<path|fd|..>. In any case, we'll probably want --config=<KEY>=<VALUE>, and therefore, we'll need to merge more than one --config* arguments.
1 parent c29b5a2 commit fca92f1

File tree

8 files changed

+188
-8
lines changed

8 files changed

+188
-8
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3535
`snapshot.max-new-file-size` config option. It will print a warning and large
3636
files will be left untracked.
3737

38+
* New command option `--config-file=PATH` to load additional configuration from
39+
files.
40+
3841
* Templates now support the `>=`, `>`, `<=`, and `<` relational operators for
3942
`Integer` types.
4043

cli/src/cli_util.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ use crate::complete;
157157
use crate::config::config_from_environment;
158158
use crate::config::parse_config_args;
159159
use crate::config::CommandNameAndArgs;
160+
use crate::config::ConfigArg;
160161
use crate::config::ConfigEnv;
161162
use crate::diff_util;
162163
use crate::diff_util::DiffFormat;
@@ -3102,6 +3103,26 @@ pub struct EarlyArgs {
31023103
// cases, designed so that `--config ui.color=auto` works
31033104
#[arg(long, value_name = "TOML", global = true)]
31043105
pub config_toml: Vec<String>,
3106+
/// Additional configuration files (can be repeated)
3107+
#[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
3108+
pub config_file: Vec<String>,
3109+
}
3110+
3111+
impl EarlyArgs {
3112+
fn merged_config_args(&self, matches: &ArgMatches) -> Vec<ConfigArg> {
3113+
merge_args_with(
3114+
matches,
3115+
&[
3116+
("config_toml", &self.config_toml),
3117+
("config_file", &self.config_file),
3118+
],
3119+
|id, value| match id {
3120+
"config_toml" => ConfigArg::Toml(value.clone()),
3121+
"config_file" => ConfigArg::File(value.clone()),
3122+
_ => unreachable!("unexpected id {id:?}"),
3123+
},
3124+
)
3125+
}
31053126
}
31063127

31073128
/// Wrapper around revset expression argument.
@@ -3143,6 +3164,29 @@ impl ValueParserFactory for RevisionArg {
31433164
}
31443165
}
31453166

3167+
/// Merges multiple clap args in order of appearance.
3168+
///
3169+
/// The `id_values` is a list of `(id, values)` pairs, where `id` is the name of
3170+
/// the clap `Arg`, and `values` are the parsed values for that arg. The
3171+
/// `convert` function transforms each `(id, value)` pair to e.g. an enum.
3172+
///
3173+
/// This is a workaround for <https://github.com/clap-rs/clap/issues/3146>.
3174+
pub fn merge_args_with<'k, 'v, T, U>(
3175+
matches: &ArgMatches,
3176+
id_values: &[(&'k str, &'v [T])],
3177+
mut convert: impl FnMut(&'k str, &'v T) -> U,
3178+
) -> Vec<U> {
3179+
let mut pos_values: Vec<(usize, U)> = Vec::new();
3180+
for (id, values) in id_values {
3181+
pos_values.extend(itertools::zip_eq(
3182+
matches.indices_of(id).into_iter().flatten(),
3183+
values.iter().map(|v| convert(id, v)),
3184+
));
3185+
}
3186+
pos_values.sort_unstable_by_key(|&(pos, _)| pos);
3187+
pos_values.into_iter().map(|(_, value)| value).collect()
3188+
}
3189+
31463190
fn get_string_or_array(
31473191
config: &StackedConfig,
31483192
key: &'static str,
@@ -3279,7 +3323,7 @@ fn handle_early_args(
32793323
let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
32803324

32813325
let old_layers_len = config.layers().len();
3282-
config.extend_layers(parse_config_args(&args.config_toml)?);
3326+
config.extend_layers(parse_config_args(&args.merged_config_args(&early_matches))?);
32833327

32843328
// Command arguments overrides any other configuration including the
32853329
// variables loaded from --config* arguments.
@@ -3711,3 +3755,45 @@ fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String
37113755
);
37123756
hint
37133757
}
3758+
3759+
#[cfg(test)]
3760+
mod tests {
3761+
use clap::CommandFactory as _;
3762+
3763+
use super::*;
3764+
3765+
#[derive(clap::Parser, Clone, Debug)]
3766+
pub struct TestArgs {
3767+
#[arg(long)]
3768+
pub foo: Vec<u32>,
3769+
#[arg(long)]
3770+
pub bar: Vec<u32>,
3771+
#[arg(long)]
3772+
pub baz: bool,
3773+
}
3774+
3775+
#[test]
3776+
fn test_merge_args_with() {
3777+
let command = TestArgs::command();
3778+
let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
3779+
let matches = command.clone().try_get_matches_from(args).unwrap();
3780+
let args = TestArgs::from_arg_matches(&matches).unwrap();
3781+
merge_args_with(
3782+
&matches,
3783+
&[("foo", &args.foo), ("bar", &args.bar)],
3784+
|id, value| (id, *value),
3785+
)
3786+
};
3787+
3788+
assert_eq!(parse(&["jj"]), vec![]);
3789+
assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
3790+
assert_eq!(
3791+
parse(&["jj", "--foo=1", "--bar=2"]),
3792+
vec![("foo", 1), ("bar", 2)]
3793+
);
3794+
assert_eq!(
3795+
parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
3796+
vec![("foo", 1), ("bar", 2), ("foo", 3)]
3797+
);
3798+
}
3799+
}

cli/src/config.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ impl ConfigEnv {
319319
/// 4. Repo config `.jj/repo/config.toml`
320320
/// 5. TODO: Workspace config `.jj/config.toml`
321321
/// 6. Override environment variables
322-
/// 7. Command-line arguments `--config-toml`
322+
/// 7. Command-line arguments `--config-toml`, `--config-file`
323323
///
324324
/// This function sets up 1, 2, and 6.
325325
pub fn config_from_environment(
@@ -401,15 +401,28 @@ fn env_overrides_layer() -> ConfigLayer {
401401
layer
402402
}
403403

404+
/// Configuration source/data provided as command-line argument.
405+
#[derive(Clone, Debug, Eq, PartialEq)]
406+
pub enum ConfigArg {
407+
/// `--config-toml=TOML`
408+
Toml(String),
409+
/// `--config-file=PATH`
410+
File(String),
411+
}
412+
404413
/// Parses `--config-toml` arguments.
405-
pub fn parse_config_args(toml_strs: &[String]) -> Result<Vec<ConfigLayer>, ConfigLoadError> {
414+
pub fn parse_config_args(toml_strs: &[ConfigArg]) -> Result<Vec<ConfigLayer>, ConfigLoadError> {
406415
// It might look silly that a layer is constructed per argument, but
407416
// --config-toml argument can contain a full TOML document, and it makes
408417
// sense to preserve line numbers within the doc. If we add
409418
// --config=KEY=VALUE, multiple values might be loaded into one layer.
419+
let source = ConfigSource::CommandArg;
410420
toml_strs
411421
.iter()
412-
.map(|text| ConfigLayer::parse(ConfigSource::CommandArg, text))
422+
.map(|arg| match arg {
423+
ConfigArg::Toml(text) => ConfigLayer::parse(source, text),
424+
ConfigArg::File(path) => ConfigLayer::load_from_file(source, path.into()),
425+
})
413426
.try_collect()
414427
}
415428

cli/tests/[email protected]

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ To get started, see the tutorial at https://martinvonz.github.io/jj/latest/tutor
196196
Warnings and errors will still be printed.
197197
* `--no-pager` — Disable the pager
198198
* `--config-toml <TOML>` — Additional configuration options (can be repeated)
199+
* `--config-file <PATH>` — Additional configuration files (can be repeated)
199200
200201
201202

cli/tests/test_completion.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ fn test_bookmark_names() {
8383
--quiet Silence non-primary command output
8484
--no-pager Disable the pager
8585
--config-toml Additional configuration options (can be repeated)
86+
--config-file Additional configuration files (can be repeated)
8687
--help Print help (see more with '--help')
8788
");
8889

cli/tests/test_global_opts.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
use std::ffi::OsString;
1616

17+
use indoc::indoc;
18+
1719
use crate::common::get_stderr_string;
1820
use crate::common::strip_last_line;
1921
use crate::common::TestEnvironment;
@@ -595,6 +597,72 @@ fn test_early_args() {
595597
);
596598
}
597599

600+
#[test]
601+
fn test_config_args() {
602+
let test_env = TestEnvironment::default();
603+
let list_config = |args: &[&str]| {
604+
test_env.jj_cmd_success(
605+
test_env.env_root(),
606+
&[&["config", "list", "--include-overridden", "test"], args].concat(),
607+
)
608+
};
609+
610+
std::fs::write(
611+
test_env.env_root().join("file1.toml"),
612+
indoc! {"
613+
test.key1 = 'file1'
614+
test.key2 = 'file1'
615+
"},
616+
)
617+
.unwrap();
618+
std::fs::write(
619+
test_env.env_root().join("file2.toml"),
620+
indoc! {"
621+
test.key3 = 'file2'
622+
"},
623+
)
624+
.unwrap();
625+
626+
let stdout = list_config(&["--config-toml=test.key1='arg1'"]);
627+
insta::assert_snapshot!(stdout, @"test.key1 = 'arg1'");
628+
let stdout = list_config(&["--config-file=file1.toml"]);
629+
insta::assert_snapshot!(stdout, @r"
630+
test.key1 = 'file1'
631+
test.key2 = 'file1'
632+
");
633+
634+
// --config* arguments are processed in order of appearance
635+
let stdout = list_config(&[
636+
"--config-toml=test.key1='arg1'",
637+
"--config-file=file1.toml",
638+
"--config-toml=test.key2='arg3'",
639+
"--config-file=file2.toml",
640+
]);
641+
insta::assert_snapshot!(stdout, @r"
642+
# test.key1 = 'arg1'
643+
test.key1 = 'file1'
644+
# test.key2 = 'file1'
645+
test.key2 = 'arg3'
646+
test.key3 = 'file2'
647+
");
648+
649+
let stderr = test_env.jj_cmd_failure(
650+
test_env.env_root(),
651+
&["config", "list", "--config-file=unknown.toml"],
652+
);
653+
insta::with_settings!({
654+
filters => [("(?m)^([2-9]): .*", "$1: <redacted>")],
655+
}, {
656+
insta::assert_snapshot!(stderr, @r"
657+
Config error: Failed to read configuration file
658+
Caused by:
659+
1: Cannot access unknown.toml
660+
2: <redacted>
661+
For help, see https://martinvonz.github.io/jj/latest/config/.
662+
");
663+
});
664+
}
665+
598666
#[test]
599667
fn test_invalid_config() {
600668
// Test that we get a reasonable error if the config is invalid (#55)
@@ -708,6 +776,7 @@ fn test_help() {
708776
--quiet Silence non-primary command output
709777
--no-pager Disable the pager
710778
--config-toml <TOML> Additional configuration options (can be repeated)
779+
--config-file <PATH> Additional configuration files (can be repeated)
711780
");
712781
}
713782

docs/config.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,9 +1203,9 @@ env JJ_CONFIG=/dev/null jj log # Ignores any settings specified in the con
12031203

12041204
### Specifying config on the command-line
12051205

1206-
You can use one or more `--config-toml` options on the command line to specify
1207-
additional configuration settings. This overrides settings defined in config
1208-
files or environment variables. For example,
1206+
You can use one or more `--config-toml`/`--config-file` options on the command
1207+
line to specify additional configuration settings. This overrides settings
1208+
defined in config files or environment variables. For example,
12091209

12101210
```shell
12111211
jj --config-toml='ui.color="always"' --config-toml='ui.diff-editor="kdiff3"' split
@@ -1221,3 +1221,9 @@ files with the config specified in `.jjconfig.toml`:
12211221
```shell
12221222
jj --config-toml="$(cat extra-config.toml)" log
12231223
```
1224+
1225+
This is equivalent to
1226+
1227+
```shell
1228+
jj --config-file=extra-config.toml log
1229+
```

lib/src/config.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,8 @@ impl ConfigLayer {
292292
Ok(Self::with_data(source, data.into_mut()))
293293
}
294294

295-
fn load_from_file(source: ConfigSource, path: PathBuf) -> Result<Self, ConfigLoadError> {
295+
/// Loads TOML file from the specified `path`.
296+
pub fn load_from_file(source: ConfigSource, path: PathBuf) -> Result<Self, ConfigLoadError> {
296297
let text = fs::read_to_string(&path)
297298
.context(&path)
298299
.map_err(ConfigLoadError::Read)?;

0 commit comments

Comments
 (0)