diff --git a/CHANGELOG.md b/CHANGELOG.md index 685b50e62..9e85c2753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,9 +74,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and `remote_needle` as optional arguments and matches just the branches whose name contains `branch_needle` and remote contains `remote_needle`. +* `jj git fetch` accepts repeated `--remote` arguments. + * Default remotes can be configured for the `jj git fetch` and `jj git push` operations ("origin" by default) using the `git.fetch` and `git.push` - configuration entries. + configuration entries. `git.fetch` can be a list if multiple remotes must + be fetched from. * `jj duplicate` can now duplicate multiple changes in one go. This preserves any parent-child relationships between them. For example, the entire tree of diff --git a/src/commands/config-schema.json b/src/commands/config-schema.json index 1a38c13a8..26bf7fa95 100644 --- a/src/commands/config-schema.json +++ b/src/commands/config-schema.json @@ -180,6 +180,21 @@ "type": "boolean", "description": "Whether jj creates a local branch with the same name when it imports a remote-tracking branch from git. See https://github.com/martinvonz/jj/blob/main/docs/config.md#automatic-local-branch-creation", "default": true + }, + "fetch": { + "description": "The remote(s) from which commits are fetched", + "default": "origin", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] } } }, diff --git a/src/commands/git.rs b/src/commands/git.rs index b47280411..1c33a1dad 100644 --- a/src/commands/git.rs +++ b/src/commands/git.rs @@ -85,9 +85,10 @@ pub struct GitRemoteListArgs {} /// Fetch from a Git remote #[derive(clap::Args, Clone, Debug)] pub struct GitFetchArgs { - /// The remote to fetch from (only named remotes are supported) - #[arg(long)] - remote: Option, + /// The remote to fetch from (only named remotes are supported, can be + /// repeated) + #[arg(long = "remote", value_name = "remote")] + remotes: Vec, } /// Create a new repo backed by a clone of a Git repo @@ -238,24 +239,33 @@ fn cmd_git_fetch( args: &GitFetchArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let remote = if let Some(name) = &args.remote { - name.clone() + let remotes = if args.remotes.is_empty() { + const KEY: &str = "git.fetch"; + let config = command.settings().config(); + config + .get(KEY) + .or_else(|_| config.get_string(KEY).map(|r| vec![r]))? } else { - command.settings().config().get("git.fetch")? + args.remotes.clone() }; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; - let mut tx = workspace_command.start_transaction(&format!("fetch from git remote {}", &remote)); - with_remote_callbacks(ui, |cb| { - git::fetch( - tx.mut_repo(), - &git_repo, - &remote, - cb, - &command.settings().git_settings(), - ) - }) - .map_err(|err| user_error(err.to_string()))?; + let mut tx = workspace_command.start_transaction(&format!( + "fetch from git remote(s) {}", + remotes.iter().join(",") + )); + for remote in remotes { + with_remote_callbacks(ui, |cb| { + git::fetch( + tx.mut_repo(), + &git_repo, + &remote, + cb, + &command.settings().git_settings(), + ) + }) + .map_err(|err| user_error(err.to_string()))?; + } tx.finish(ui)?; Ok(()) } diff --git a/tests/test_git_fetch.rs b/tests/test_git_fetch.rs index 04e915218..00ec54776 100644 --- a/tests/test_git_fetch.rs +++ b/tests/test_git_fetch.rs @@ -57,15 +57,53 @@ fn test_git_fetch_single_remote_from_config() { "###); } +#[test] +fn test_git_fetch_multiple_remotes() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + add_git_remote(&test_env, &repo_path, "rem1"); + add_git_remote(&test_env, &repo_path, "rem2"); + + test_env.jj_cmd_success( + &repo_path, + &["git", "fetch", "--remote", "rem1", "--remote", "rem2"], + ); + insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" + rem1: 9f01a0e04879 message + rem2: 9f01a0e04879 message + "###); +} + +#[test] +fn test_git_fetch_multiple_remotes_from_config() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + add_git_remote(&test_env, &repo_path, "rem1"); + add_git_remote(&test_env, &repo_path, "rem2"); + test_env.add_config(r#"git.fetch = ["rem1", "rem2"]"#); + + test_env.jj_cmd_success(&repo_path, &["git", "fetch"]); + insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" + rem1: 9f01a0e04879 message + rem2: 9f01a0e04879 message + "###); +} + #[test] fn test_git_fetch_nonexistent_remote() { let test_env = TestEnvironment::default(); test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); let repo_path = test_env.env_root().join("repo"); + add_git_remote(&test_env, &repo_path, "rem1"); - let stderr = &test_env.jj_cmd_failure(&repo_path, &["git", "fetch", "--remote", "rem1"]); + let stderr = &test_env.jj_cmd_failure( + &repo_path, + &["git", "fetch", "--remote", "rem1", "--remote", "rem2"], + ); insta::assert_snapshot!(stderr, @r###" - Error: No git remote named 'rem1' + Error: No git remote named 'rem2' "###); // No remote should have been fetched as part of the failing transaction insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @""); @@ -76,11 +114,12 @@ fn test_git_fetch_nonexistent_remote_from_config() { let test_env = TestEnvironment::default(); test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); let repo_path = test_env.env_root().join("repo"); - test_env.add_config(r#"git.fetch = "rem1""#); + add_git_remote(&test_env, &repo_path, "rem1"); + test_env.add_config(r#"git.fetch = ["rem1", "rem2"]"#); let stderr = &test_env.jj_cmd_failure(&repo_path, &["git", "fetch"]); insta::assert_snapshot!(stderr, @r###" - Error: No git remote named 'rem1' + Error: No git remote named 'rem2' "###); // No remote should have been fetched as part of the failing transaction insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @"");