cli: resolve settings for newly initialized/cloned workspace
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 / Run doctests (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

Fixes #5144
This commit is contained in:
Yuya Nishihara 2024-12-29 15:46:55 +09:00
parent 56bd4765f5
commit 6c14ccd89d
9 changed files with 200 additions and 14 deletions

View file

@ -43,6 +43,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* Fixed diff selection by external tools with `jj split`/`commit -i FILESETS`.
[#5252](https://github.com/jj-vcs/jj/issues/5252)
* Conditional configuration now applies when initializing new repository.
[#5144](https://github.com/jj-vcs/jj/issues/5144)
## [0.25.0] - 2025-01-01
### Release highlights

View file

@ -346,6 +346,20 @@ impl CommandHelper {
&self.data.settings
}
/// Resolves configuration for new workspace located at the specified path.
pub fn settings_for_new_workspace(
&self,
workspace_root: &Path,
) -> Result<UserSettings, CommandError> {
let mut config_env = self.data.config_env.clone();
let mut raw_config = self.data.raw_config.clone();
let repo_path = workspace_root.join(".jj").join("repo");
config_env.reset_repo_path(&repo_path);
config_env.reload_repo_config(&mut raw_config)?;
let config = config_env.resolve_config(&raw_config)?;
Ok(self.data.settings.with_new_config(config)?)
}
/// Loads text editor from the settings.
pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
TextEditor::from_settings(self.settings())

View file

@ -197,10 +197,11 @@ fn do_git_clone(
source: &str,
wc_path: &Path,
) -> Result<(WorkspaceCommandHelper, GitFetchStats), CommandError> {
let settings = command.settings_for_new_workspace(wc_path)?;
let (workspace, repo) = if colocate {
Workspace::init_colocated_git(command.settings(), wc_path)?
Workspace::init_colocated_git(&settings, wc_path)?
} else {
Workspace::init_internal_git(command.settings(), wc_path)?
Workspace::init_internal_git(&settings, wc_path)?
};
let git_repo = get_git_repo(repo.store())?;
writeln!(
@ -211,8 +212,8 @@ fn do_git_clone(
let mut workspace_command = command.for_workable_repo(ui, workspace, repo)?;
maybe_add_gitignore(&workspace_command)?;
git_repo.remote(remote_name, source).unwrap();
let git_settings = workspace_command.settings().git_settings()?;
let mut fetch_tx = workspace_command.start_transaction();
let git_settings = command.settings().git_settings()?;
let stats = with_remote_git_callbacks(ui, None, |cb| {
git::fetch(

View file

@ -154,20 +154,20 @@ fn do_init(
GitInitMode::Internal
};
let settings = command.settings_for_new_workspace(workspace_root)?;
match &init_mode {
GitInitMode::Colocate => {
let (workspace, repo) =
Workspace::init_colocated_git(command.settings(), workspace_root)?;
let (workspace, repo) = Workspace::init_colocated_git(&settings, workspace_root)?;
let workspace_command = command.for_workable_repo(ui, workspace, repo)?;
maybe_add_gitignore(&workspace_command)?;
}
GitInitMode::External(git_repo_path) => {
let (workspace, repo) =
Workspace::init_external_git(command.settings(), workspace_root, git_repo_path)?;
Workspace::init_external_git(&settings, workspace_root, git_repo_path)?;
// Import refs first so all the reachable commits are indexed in
// chronological order.
let colocated = is_colocated_git_workspace(&workspace, &repo);
let repo = init_git_refs(ui, command, repo, colocated)?;
let repo = init_git_refs(ui, repo, command.string_args(), colocated)?;
let mut workspace_command = command.for_workable_repo(ui, workspace, repo)?;
maybe_add_gitignore(&workspace_command)?;
workspace_command.maybe_snapshot(ui)?;
@ -186,7 +186,7 @@ fn do_init(
print_trackable_remote_bookmarks(ui, workspace_command.repo().view())?;
}
GitInitMode::Internal => {
Workspace::init_internal_git(command.settings(), workspace_root)?;
Workspace::init_internal_git(&settings, workspace_root)?;
}
}
Ok(())
@ -199,13 +199,13 @@ fn do_init(
/// moves the Git HEAD to the working copy parent.
fn init_git_refs(
ui: &mut Ui,
command: &CommandHelper,
repo: Arc<ReadonlyRepo>,
string_args: &[String],
colocated: bool,
) -> Result<Arc<ReadonlyRepo>, CommandError> {
let mut tx = start_repo_transaction(&repo, command.string_args());
let mut git_settings = repo.settings().git_settings()?;
let mut tx = start_repo_transaction(&repo, string_args);
// There should be no old refs to abandon, but enforce it.
let mut git_settings = command.settings().git_settings()?;
git_settings.abandon_unreachable_commits = false;
let stats = git::import_some_refs(
tx.repo_mut(),

View file

@ -61,7 +61,7 @@ pub(crate) fn cmd_init(
Set `ui.allow-init-native` to allow initializing a repo with the native backend.",
));
}
Workspace::init_local(command.settings(), &wc_path)?;
Workspace::init_local(&command.settings_for_new_workspace(&wc_path)?, &wc_path)?;
let relative_wc_path = file_util::relative_path(cwd, &wc_path);
writeln!(

View file

@ -108,6 +108,8 @@ pub fn cmd_workspace_add(
let working_copy_factory = command.get_working_copy_factory()?;
let repo_path = old_workspace_command.repo_path();
// If we add per-workspace configuration, we'll need to reload settings for
// the new workspace.
let (new_workspace, repo) = Workspace::init_workspace_with_existing_repo(
&destination_path,
repo_path,

View file

@ -16,8 +16,11 @@ use std::path;
use std::path::Path;
use std::path::PathBuf;
use indoc::formatdoc;
use crate::common::get_stderr_string;
use crate::common::get_stdout_string;
use crate::common::to_toml_value;
use crate::common::TestEnvironment;
fn set_up_non_empty_git_repo(git_repo: &git2::Repository) {
@ -586,6 +589,88 @@ fn test_git_clone_trunk_deleted() {
"#);
}
#[test]
fn test_git_clone_conditional_config() {
let test_env = TestEnvironment::default();
let source_repo_path = test_env.env_root().join("source");
let old_workspace_root = test_env.env_root().join("old");
let new_workspace_root = test_env.env_root().join("new");
let source_git_repo = git2::Repository::init(source_repo_path).unwrap();
set_up_non_empty_git_repo(&source_git_repo);
let jj_cmd_ok = |current_dir: &Path, args: &[&str]| {
let mut cmd = test_env.jj_cmd(current_dir, args);
cmd.env_remove("JJ_EMAIL");
cmd.env_remove("JJ_OP_HOSTNAME");
cmd.env_remove("JJ_OP_USERNAME");
let assert = cmd.assert().success();
let stdout = test_env.normalize_output(&get_stdout_string(&assert));
let stderr = test_env.normalize_output(&get_stderr_string(&assert));
(stdout, stderr)
};
let log_template = r#"separate(' ', author.email(), description.first_line()) ++ "\n""#;
let op_log_template = r#"separate(' ', user, description.first_line()) ++ "\n""#;
// Override user.email and operation.username conditionally
test_env.add_config(formatdoc! {"
user.email = 'base@example.org'
operation.hostname = 'base'
operation.username = 'base'
[[--scope]]
--when.repositories = [{new_workspace_root}]
user.email = 'new-repo@example.org'
operation.username = 'new-repo'
",
new_workspace_root = to_toml_value(new_workspace_root.to_str().unwrap()),
});
// Override operation.hostname by repo config, which should be loaded into
// the command settings, but shouldn't be copied to the new repo.
jj_cmd_ok(test_env.env_root(), &["git", "init", "old"]);
jj_cmd_ok(
&old_workspace_root,
&["config", "set", "--repo", "operation.hostname", "old-repo"],
);
jj_cmd_ok(&old_workspace_root, &["new"]);
let (stdout, _stderr) = jj_cmd_ok(&old_workspace_root, &["op", "log", "-T", op_log_template]);
insta::assert_snapshot!(stdout, @r"
@ base@old-repo new empty commit
base@base add workspace 'default'
@
");
// Clone repo at the old workspace directory.
let (_stdout, stderr) = jj_cmd_ok(
&old_workspace_root,
&["git", "clone", "../source", "../new"],
);
insta::assert_snapshot!(stderr, @r#"
Fetching into new repo in "$TEST_ENV/new"
bookmark: main@origin [new] untracked
Setting the revset alias "trunk()" to "main@origin"
Working copy now at: zxsnswpr 5695b5e5 (empty) (no description set)
Parent commit : mzyxwzks 9f01a0e0 main | message
Added 1 files, modified 0 files, removed 0 files
"#);
jj_cmd_ok(&new_workspace_root, &["new"]);
let (stdout, _stderr) = jj_cmd_ok(&new_workspace_root, &["log", "-T", log_template]);
insta::assert_snapshot!(stdout, @r"
@ new-repo@example.org
new-repo@example.org
some.one@example.com message
~
");
let (stdout, _stderr) = jj_cmd_ok(&new_workspace_root, &["op", "log", "-T", op_log_template]);
insta::assert_snapshot!(stdout, @r"
@ new-repo@base new empty commit
new-repo@base check out git remote's default branch
new-repo@base fetch from git remote into empty repo
new-repo@base add workspace 'default'
@
");
}
#[test]
fn test_git_clone_with_depth() {
let test_env = TestEnvironment::default();

View file

@ -16,9 +16,13 @@ use std::fmt::Write as _;
use std::path::Path;
use std::path::PathBuf;
use indoc::formatdoc;
use test_case::test_case;
use crate::common::get_stderr_string;
use crate::common::get_stdout_string;
use crate::common::strip_last_line;
use crate::common::to_toml_value;
use crate::common::TestEnvironment;
fn init_git_repo(git_repo_path: &Path, bare: bool) -> git2::Repository {
@ -766,6 +770,71 @@ fn test_git_init_colocated_via_flag_git_dir_not_exists() {
"###);
}
#[test]
fn test_git_init_conditional_config() {
let test_env = TestEnvironment::default();
let old_workspace_root = test_env.env_root().join("old");
let new_workspace_root = test_env.env_root().join("new");
let jj_cmd_ok = |current_dir: &Path, args: &[&str]| {
let mut cmd = test_env.jj_cmd(current_dir, args);
cmd.env_remove("JJ_EMAIL");
cmd.env_remove("JJ_OP_HOSTNAME");
cmd.env_remove("JJ_OP_USERNAME");
let assert = cmd.assert().success();
let stdout = test_env.normalize_output(&get_stdout_string(&assert));
let stderr = test_env.normalize_output(&get_stderr_string(&assert));
(stdout, stderr)
};
let log_template = r#"separate(' ', author.email(), description.first_line()) ++ "\n""#;
let op_log_template = r#"separate(' ', user, description.first_line()) ++ "\n""#;
// Override user.email and operation.username conditionally
test_env.add_config(formatdoc! {"
user.email = 'base@example.org'
operation.hostname = 'base'
operation.username = 'base'
[[--scope]]
--when.repositories = [{new_workspace_root}]
user.email = 'new-repo@example.org'
operation.username = 'new-repo'
",
new_workspace_root = to_toml_value(new_workspace_root.to_str().unwrap()),
});
// Override operation.hostname by repo config, which should be loaded into
// the command settings, but shouldn't be copied to the new repo.
jj_cmd_ok(test_env.env_root(), &["git", "init", "old"]);
jj_cmd_ok(
&old_workspace_root,
&["config", "set", "--repo", "operation.hostname", "old-repo"],
);
jj_cmd_ok(&old_workspace_root, &["new"]);
let (stdout, _stderr) = jj_cmd_ok(&old_workspace_root, &["op", "log", "-T", op_log_template]);
insta::assert_snapshot!(stdout, @r"
@ base@old-repo new empty commit
base@base add workspace 'default'
@
");
// Create new repo at the old workspace directory.
let (_stdout, stderr) = jj_cmd_ok(&old_workspace_root, &["git", "init", "../new"]);
insta::assert_snapshot!(stderr.replace('\\', "/"), @r#"Initialized repo in "../new""#);
jj_cmd_ok(&new_workspace_root, &["new"]);
let (stdout, _stderr) = jj_cmd_ok(&new_workspace_root, &["log", "-T", log_template]);
insta::assert_snapshot!(stdout, @r"
@ new-repo@example.org
new-repo@example.org
");
let (stdout, _stderr) = jj_cmd_ok(&new_workspace_root, &["op", "log", "-T", op_log_template]);
insta::assert_snapshot!(stdout, @r"
@ new-repo@base new empty commit
new-repo@base add workspace 'default'
@
");
}
#[test]
fn test_git_init_bad_wc_path() {
let test_env = TestEnvironment::default();

View file

@ -144,6 +144,11 @@ fn to_timestamp(value: ConfigValue) -> Result<Timestamp, Box<dyn std::error::Err
impl UserSettings {
pub fn from_config(config: StackedConfig) -> Result<Self, ConfigGetError> {
let rng_seed = config.get::<u64>("debug.randomness-seed").optional()?;
Self::from_config_and_rng(config, Arc::new(JJRng::new(rng_seed)))
}
fn from_config_and_rng(config: StackedConfig, rng: Arc<JJRng>) -> Result<Self, ConfigGetError> {
let user_name = config.get("user.name")?;
let user_email = config.get("user.email")?;
let commit_timestamp = config
@ -162,14 +167,21 @@ impl UserSettings {
operation_hostname,
operation_username,
};
let rng_seed = config.get::<u64>("debug.randomness-seed").optional()?;
Ok(UserSettings {
config: Arc::new(config),
data: Arc::new(data),
rng: Arc::new(JJRng::new(rng_seed)),
rng,
})
}
/// Like [`UserSettings::from_config()`], but retains the internal state.
///
/// This ensures that no duplicated change IDs are generated within the
/// current process. New `debug.randomness-seed` value is ignored.
pub fn with_new_config(&self, config: StackedConfig) -> Result<Self, ConfigGetError> {
Self::from_config_and_rng(config, self.rng.clone())
}
pub fn get_rng(&self) -> Arc<JJRng> {
self.rng.clone()
}