Skip to content

Commit

Permalink
cli: resolve settings for newly initialized/cloned workspace
Browse files Browse the repository at this point in the history
  • Loading branch information
yuja committed Jan 5, 2025
1 parent b64cfde commit 99dcca8
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 14 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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
Expand Down
14 changes: 14 additions & 0 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,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())
Expand Down
7 changes: 4 additions & 3 deletions cli/src/commands/git/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand All @@ -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(
Expand Down
16 changes: 8 additions & 8 deletions cli/src/commands/git/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,20 +154,20 @@ pub 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)?;
Expand All @@ -186,7 +186,7 @@ pub 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(())
Expand All @@ -199,13 +199,13 @@ pub 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(),
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
2 changes: 2 additions & 0 deletions cli/src/commands/workspace/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
85 changes: 85 additions & 0 deletions cli/tests/test_git_clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
69 changes: 69 additions & 0 deletions cli/tests/test_git_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
16 changes: 14 additions & 2 deletions lib/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
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()
}
Expand Down

0 comments on commit 99dcca8

Please sign in to comment.