mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-29 23:57:51 +00:00
cli: resolve conditional config scopes
Some checks are pending
binaries / Build binary artifacts (push) Waiting to run
nix / flake check (push) Waiting to run
build / build (, macos-13) (push) Waiting to run
build / build (, macos-14) (push) Waiting to run
build / build (, ubuntu-latest) (push) Waiting to run
build / build (, windows-latest) (push) Waiting to run
build / build (--all-features, ubuntu-latest) (push) Waiting to run
build / Build jj-lib without Git support (push) Waiting to run
build / Check protos (push) Waiting to run
build / Check formatting (push) Waiting to run
build / Check that MkDocs can build the docs (push) Waiting to run
build / Check that MkDocs can build the docs with latest Python and uv (push) Waiting to run
build / cargo-deny (advisories) (push) Waiting to run
build / cargo-deny (bans licenses sources) (push) Waiting to run
build / Clippy check (push) Waiting to run
Codespell / Codespell (push) Waiting to run
website / prerelease-docs-build-deploy (ubuntu-latest) (push) Waiting to run
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run
Some checks are pending
binaries / Build binary artifacts (push) Waiting to run
nix / flake check (push) Waiting to run
build / build (, macos-13) (push) Waiting to run
build / build (, macos-14) (push) Waiting to run
build / build (, ubuntu-latest) (push) Waiting to run
build / build (, windows-latest) (push) Waiting to run
build / build (--all-features, ubuntu-latest) (push) Waiting to run
build / Build jj-lib without Git support (push) Waiting to run
build / Check protos (push) Waiting to run
build / Check formatting (push) Waiting to run
build / Check that MkDocs can build the docs (push) Waiting to run
build / Check that MkDocs can build the docs with latest Python and uv (push) Waiting to run
build / cargo-deny (advisories) (push) Waiting to run
build / cargo-deny (bans licenses sources) (push) Waiting to run
build / Clippy check (push) Waiting to run
Codespell / Codespell (push) Waiting to run
website / prerelease-docs-build-deploy (ubuntu-latest) (push) Waiting to run
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run
This is an alternative way to achieve includeIf of Git without adding "include" directive. Conditional include (or include in general) is a bit trickier to implement than loading all files and filtering the contents. Closes #616
This commit is contained in:
parent
1a0d3ae3d7
commit
f5d450d7c3
7 changed files with 214 additions and 7 deletions
|
@ -48,6 +48,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
`snapshot.max-new-file-size` config option. It will print a warning and large
|
||||
files will be left untracked.
|
||||
|
||||
* Configuration files now support [conditional
|
||||
variables](docs/config.md#conditional-variables).
|
||||
|
||||
* New command options `--config=NAME=VALUE` and `--config-file=PATH` to set
|
||||
string value without quoting and to load additional configuration from files.
|
||||
|
||||
|
|
|
@ -3650,7 +3650,7 @@ impl CliRunner {
|
|||
config_env.reset_repo_path(loader.repo_path());
|
||||
config_env.reload_repo_config(&mut raw_config)?;
|
||||
}
|
||||
let mut config = raw_config.as_ref().clone(); // TODO
|
||||
let mut config = config_env.resolve_config(&raw_config)?;
|
||||
ui.reset(&config)?;
|
||||
|
||||
if env::var_os("COMPLETE").is_some() {
|
||||
|
@ -3661,7 +3661,7 @@ impl CliRunner {
|
|||
let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
|
||||
if !config_layers.is_empty() {
|
||||
raw_config.as_mut().extend_layers(config_layers);
|
||||
config = raw_config.as_ref().clone(); // TODO
|
||||
config = config_env.resolve_config(&raw_config)?;
|
||||
ui.reset(&config)?;
|
||||
}
|
||||
if !args.config_toml.is_empty() {
|
||||
|
@ -3687,7 +3687,7 @@ impl CliRunner {
|
|||
.map_err(|err| map_workspace_load_error(err, Some(path)))?;
|
||||
config_env.reset_repo_path(loader.repo_path());
|
||||
config_env.reload_repo_config(&mut raw_config)?;
|
||||
config = raw_config.as_ref().clone(); // TODO
|
||||
config = config_env.resolve_config(&raw_config)?;
|
||||
Ok(loader)
|
||||
} else {
|
||||
maybe_cwd_workspace_loader
|
||||
|
|
|
@ -693,7 +693,7 @@ fn get_jj_command() -> Result<(JjBuilder, UserSettings), CommandError> {
|
|||
config_env.reset_repo_path(loader.repo_path());
|
||||
let _ = config_env.reload_repo_config(&mut raw_config);
|
||||
}
|
||||
let mut config = raw_config.as_ref().clone(); /* TODO */
|
||||
let mut config = config_env.resolve_config(&raw_config)?;
|
||||
// skip 2 because of the clap_complete prelude: jj -- jj <actual args...>
|
||||
let args = std::env::args_os().skip(2);
|
||||
let args = expand_args(&ui, &app, args, &config)?;
|
||||
|
@ -710,7 +710,9 @@ fn get_jj_command() -> Result<(JjBuilder, UserSettings), CommandError> {
|
|||
if let Ok(loader) = DefaultWorkspaceLoaderFactory.create(&cwd.join(&repository)) {
|
||||
config_env.reset_repo_path(loader.repo_path());
|
||||
let _ = config_env.reload_repo_config(&mut raw_config);
|
||||
config = raw_config.as_ref().clone(); // TODO
|
||||
if let Ok(new_config) = config_env.resolve_config(&raw_config) {
|
||||
config = new_config;
|
||||
}
|
||||
}
|
||||
cmd_args.push("--repository".into());
|
||||
cmd_args.push(repository);
|
||||
|
|
|
@ -23,9 +23,11 @@ use std::process::Command;
|
|||
|
||||
use itertools::Itertools;
|
||||
use jj_lib::config::ConfigFile;
|
||||
use jj_lib::config::ConfigGetError;
|
||||
use jj_lib::config::ConfigLayer;
|
||||
use jj_lib::config::ConfigLoadError;
|
||||
use jj_lib::config::ConfigNamePathBuf;
|
||||
use jj_lib::config::ConfigResolutionContext;
|
||||
use jj_lib::config::ConfigSource;
|
||||
use jj_lib::config::ConfigValue;
|
||||
use jj_lib::config::StackedConfig;
|
||||
|
@ -247,6 +249,8 @@ impl UnresolvedConfigEnv {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConfigEnv {
|
||||
home_dir: Option<PathBuf>,
|
||||
repo_path: Option<PathBuf>,
|
||||
user_config_path: ConfigPath,
|
||||
repo_config_path: ConfigPath,
|
||||
}
|
||||
|
@ -254,12 +258,18 @@ pub struct ConfigEnv {
|
|||
impl ConfigEnv {
|
||||
/// Initializes configuration loader based on environment variables.
|
||||
pub fn from_environment() -> Result<Self, ConfigEnvError> {
|
||||
// Canonicalize home as we do canonicalize cwd in CliRunner.
|
||||
// TODO: Maybe better to switch to dunce::canonicalize() and remove
|
||||
// canonicalization of cwd, home, etc.?
|
||||
let home_dir = dirs::home_dir().map(|path| path.canonicalize().unwrap_or(path));
|
||||
let env = UnresolvedConfigEnv {
|
||||
config_dir: dirs::config_dir(),
|
||||
home_dir: dirs::home_dir(),
|
||||
home_dir: home_dir.clone(),
|
||||
jj_config: env::var("JJ_CONFIG").ok(),
|
||||
};
|
||||
Ok(ConfigEnv {
|
||||
home_dir,
|
||||
repo_path: None,
|
||||
user_config_path: env.resolve()?,
|
||||
repo_config_path: ConfigPath::Unavailable,
|
||||
})
|
||||
|
@ -324,6 +334,7 @@ impl ConfigEnv {
|
|||
/// Sets the directory where repo-specific config file is stored. The path
|
||||
/// is usually `.jj/repo`.
|
||||
pub fn reset_repo_path(&mut self, path: &Path) {
|
||||
self.repo_path = Some(path.to_owned());
|
||||
self.repo_config_path = ConfigPath::new(Some(path.join("config.toml")));
|
||||
}
|
||||
|
||||
|
@ -371,6 +382,16 @@ impl ConfigEnv {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolves conditional scopes within the current environment. Returns new
|
||||
/// resolved config.
|
||||
pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
|
||||
let context = ConfigResolutionContext {
|
||||
home_dir: self.home_dir.as_deref(),
|
||||
repo_path: self.repo_path.as_deref(),
|
||||
};
|
||||
jj_lib::config::resolve(config.as_ref(), &context)
|
||||
}
|
||||
}
|
||||
|
||||
fn config_files_for(
|
||||
|
@ -1188,9 +1209,10 @@ mod tests {
|
|||
|
||||
impl TestCase {
|
||||
fn resolve(&self, root: &Path) -> Result<ConfigEnv, ConfigEnvError> {
|
||||
let home_dir = self.env.home_dir.as_ref().map(|p| root.join(p));
|
||||
let env = UnresolvedConfigEnv {
|
||||
config_dir: self.env.config_dir.as_ref().map(|p| root.join(p)),
|
||||
home_dir: self.env.home_dir.as_ref().map(|p| root.join(p)),
|
||||
home_dir: home_dir.clone(),
|
||||
jj_config: self
|
||||
.env
|
||||
.jj_config
|
||||
|
@ -1198,6 +1220,8 @@ mod tests {
|
|||
.map(|p| root.join(p).to_str().unwrap().to_string()),
|
||||
};
|
||||
Ok(ConfigEnv {
|
||||
home_dir,
|
||||
repo_path: None,
|
||||
user_config_path: env.resolve()?,
|
||||
repo_config_path: ConfigPath::Unavailable,
|
||||
})
|
||||
|
|
|
@ -1047,6 +1047,107 @@ fn test_config_path_syntax() {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "dirs::home_dir() can't be overridden by $HOME")] // TODO
|
||||
fn test_config_conditional() {
|
||||
let mut test_env = TestEnvironment::default();
|
||||
test_env.jj_cmd_ok(test_env.home_dir(), &["git", "init", "repo1"]);
|
||||
test_env.jj_cmd_ok(test_env.home_dir(), &["git", "init", "repo2"]);
|
||||
let repo1_path = test_env.home_dir().join("repo1");
|
||||
let repo2_path = test_env.home_dir().join("repo2");
|
||||
// Test with fresh new config file
|
||||
let user_config_path = test_env.env_root().join("config.toml");
|
||||
test_env.set_config_path(user_config_path.clone());
|
||||
std::fs::write(
|
||||
&user_config_path,
|
||||
indoc! {"
|
||||
foo = 'global'
|
||||
[[--scope]]
|
||||
--when.repositories = ['~/repo1']
|
||||
foo = 'repo1'
|
||||
[[--scope]]
|
||||
--when.repositories = ['~/repo2']
|
||||
foo = 'repo2'
|
||||
"},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// get and list should refer to the resolved config
|
||||
let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "get", "foo"]);
|
||||
insta::assert_snapshot!(stdout, @"global");
|
||||
let stdout = test_env.jj_cmd_success(&repo1_path, &["config", "get", "foo"]);
|
||||
insta::assert_snapshot!(stdout, @"repo1");
|
||||
let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list", "--user"]);
|
||||
insta::assert_snapshot!(stdout, @"foo = 'global'");
|
||||
let stdout = test_env.jj_cmd_success(&repo1_path, &["config", "list", "--user"]);
|
||||
insta::assert_snapshot!(stdout, @"foo = 'repo1'");
|
||||
let stdout = test_env.jj_cmd_success(&repo2_path, &["config", "list", "--user"]);
|
||||
insta::assert_snapshot!(stdout, @"foo = 'repo2'");
|
||||
|
||||
// relative workspace path
|
||||
let stdout = test_env.jj_cmd_success(&repo2_path, &["config", "list", "--user", "-R../repo1"]);
|
||||
insta::assert_snapshot!(stdout, @"foo = 'repo1'");
|
||||
|
||||
// set and unset should refer to the source config
|
||||
// (there's no option to update scoped table right now.)
|
||||
let (_stdout, stderr) = test_env.jj_cmd_ok(
|
||||
test_env.env_root(),
|
||||
&["config", "set", "--user", "bar", "new value"],
|
||||
);
|
||||
insta::assert_snapshot!(stderr, @"");
|
||||
insta::assert_snapshot!(std::fs::read_to_string(&user_config_path).unwrap(), @r#"
|
||||
foo = 'global'
|
||||
bar = "new value"
|
||||
[[--scope]]
|
||||
--when.repositories = ['~/repo1']
|
||||
foo = 'repo1'
|
||||
[[--scope]]
|
||||
--when.repositories = ['~/repo2']
|
||||
foo = 'repo2'
|
||||
"#);
|
||||
let (_stdout, stderr) = test_env.jj_cmd_ok(&repo1_path, &["config", "unset", "--user", "foo"]);
|
||||
insta::assert_snapshot!(stderr, @"");
|
||||
insta::assert_snapshot!(std::fs::read_to_string(&user_config_path).unwrap(), @r#"
|
||||
bar = "new value"
|
||||
[[--scope]]
|
||||
--when.repositories = ['~/repo1']
|
||||
foo = 'repo1'
|
||||
[[--scope]]
|
||||
--when.repositories = ['~/repo2']
|
||||
foo = 'repo2'
|
||||
"#);
|
||||
}
|
||||
|
||||
// Minimal test for Windows where the home directory can't be switched.
|
||||
// (Can be removed if test_config_conditional() is enabled on Windows.)
|
||||
#[test]
|
||||
fn test_config_conditional_without_home_dir() {
|
||||
let mut 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 with fresh new config file
|
||||
let user_config_path = test_env.env_root().join("config.toml");
|
||||
test_env.set_config_path(user_config_path.clone());
|
||||
std::fs::write(
|
||||
&user_config_path,
|
||||
format!(
|
||||
indoc! {"
|
||||
foo = 'global'
|
||||
[[--scope]]
|
||||
--when.repositories = [{repo_path}]
|
||||
foo = 'repo'
|
||||
"},
|
||||
repo_path = toml_edit::Value::from(repo_path.to_str().unwrap())
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "get", "foo"]);
|
||||
insta::assert_snapshot!(stdout, @"global");
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["config", "get", "foo"]);
|
||||
insta::assert_snapshot!(stdout, @"repo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_show_paths() {
|
||||
let test_env = TestEnvironment::default();
|
||||
|
|
|
@ -721,6 +721,59 @@ fn test_invalid_config_value() {
|
|||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "dirs::home_dir() can't be overridden by $HOME")] // TODO
|
||||
fn test_conditional_config() {
|
||||
let test_env = TestEnvironment::default();
|
||||
test_env.jj_cmd_ok(test_env.home_dir(), &["git", "init", "repo1"]);
|
||||
test_env.jj_cmd_ok(test_env.home_dir(), &["git", "init", "repo2"]);
|
||||
test_env.add_config(indoc! {"
|
||||
aliases.foo = ['new', 'root()', '-mglobal']
|
||||
[[--scope]]
|
||||
--when.repositories = ['~']
|
||||
aliases.foo = ['new', 'root()', '-mhome']
|
||||
[[--scope]]
|
||||
--when.repositories = ['~/repo1']
|
||||
aliases.foo = ['new', 'root()', '-mrepo1']
|
||||
"});
|
||||
|
||||
// Sanity check
|
||||
let stdout = test_env.jj_cmd_success(
|
||||
test_env.env_root(),
|
||||
&["config", "list", "--include-overridden", "aliases"],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @"aliases.foo = ['new', 'root()', '-mglobal']");
|
||||
let stdout = test_env.jj_cmd_success(
|
||||
&test_env.home_dir().join("repo1"),
|
||||
&["config", "list", "--include-overridden", "aliases"],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @r"
|
||||
# aliases.foo = ['new', 'root()', '-mglobal']
|
||||
# aliases.foo = ['new', 'root()', '-mhome']
|
||||
aliases.foo = ['new', 'root()', '-mrepo1']
|
||||
");
|
||||
let stdout = test_env.jj_cmd_success(
|
||||
&test_env.home_dir().join("repo2"),
|
||||
&["config", "list", "--include-overridden", "aliases"],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @r"
|
||||
# aliases.foo = ['new', 'root()', '-mglobal']
|
||||
aliases.foo = ['new', 'root()', '-mhome']
|
||||
");
|
||||
|
||||
// Aliases can be expanded by using the conditional tables
|
||||
let (_stdout, stderr) = test_env.jj_cmd_ok(&test_env.home_dir().join("repo1"), &["foo"]);
|
||||
insta::assert_snapshot!(stderr, @r"
|
||||
Working copy now at: royxmykx 82899b03 (empty) repo1
|
||||
Parent commit : zzzzzzzz 00000000 (empty) (no description set)
|
||||
");
|
||||
let (_stdout, stderr) = test_env.jj_cmd_ok(&test_env.home_dir().join("repo2"), &["foo"]);
|
||||
insta::assert_snapshot!(stderr, @r"
|
||||
Working copy now at: yqosqzyt 3bd315a9 (empty) home
|
||||
Parent commit : zzzzzzzz 00000000 (empty) (no description set)
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_user_configured() {
|
||||
// Test that the user is reminded if they haven't configured their name or email
|
||||
|
|
|
@ -1233,3 +1233,27 @@ To load an entire TOML document, use `--config-file`:
|
|||
```shell
|
||||
jj --config-file=extra-config.toml log
|
||||
```
|
||||
|
||||
### Conditional variables
|
||||
|
||||
You can conditionally enable config variables by using `--when` and
|
||||
`[[--scope]]` tables. Variables defined in `[[--scope]]` tables are expanded to
|
||||
the root table. `--when` specifies the condition to enable the scope table.
|
||||
|
||||
```toml
|
||||
[user]
|
||||
name = "YOUR NAME"
|
||||
email = "YOUR_DEFAULT_EMAIL@example.com"
|
||||
|
||||
# override user.email if the repository is located under ~/oss
|
||||
[[--scope]]
|
||||
--when.repositories = ["~/oss"]
|
||||
[--scope.user]
|
||||
email = "YOUR_OSS_EMAIL@example.org"
|
||||
```
|
||||
|
||||
Condition keys:
|
||||
|
||||
* `--when.repositories`: List of paths to match the repository path prefix.
|
||||
|
||||
If no conditions are specified, table is always enabled.
|
||||
|
|
Loading…
Reference in a new issue