diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f7bb65d66..2372a05a9c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,6 +132,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed bugs +* `jj git fetch` with multiple remotes will now fetch from all remotes before + importing refs into the jj repo. This fixes a race condition where the + treatment of a commit that is found in multiple fetch remotes depended on the + order the remotes were specified. + * Fixed diff selection by external tools with `jj split`/`commit -i FILESETS`. [#5252](https://github.com/jj-vcs/jj/issues/5252) diff --git a/cli/src/commands/git/clone.rs b/cli/src/commands/git/clone.rs index 49e2a843ce0..96cae0bae58 100644 --- a/cli/src/commands/git/clone.rs +++ b/cli/src/commands/git/clone.rs @@ -19,8 +19,8 @@ use std::num::NonZeroU32; use std::path::Path; use jj_lib::git; +use jj_lib::git::GitFetch; use jj_lib::git::GitFetchError; -use jj_lib::git::GitFetchStats; use jj_lib::repo::Repo; use jj_lib::str_util::StringPattern; use jj_lib::workspace::Workspace; @@ -118,8 +118,9 @@ pub fn cmd_git_clone( let clone_result = (|| -> Result<_, CommandError> { let mut workspace_command = init_workspace(ui, command, &canonical_wc_path, args.colocate)?; - let stats = fetch_new_remote(ui, &mut workspace_command, remote_name, &source, args.depth)?; - Ok((workspace_command, stats)) + let default_branch = + fetch_new_remote(ui, &mut workspace_command, remote_name, &source, args.depth)?; + Ok((workspace_command, default_branch)) })(); if clone_result.is_err() { let clean_up_dirs = || -> io::Result<()> { @@ -143,8 +144,8 @@ pub fn cmd_git_clone( } } - let (mut workspace_command, stats) = clone_result?; - if let Some(default_branch) = &stats.default_branch { + let (mut workspace_command, default_branch) = clone_result?; + if let Some(default_branch) = &default_branch { write_repository_level_trunk_alias( ui, workspace_command.repo_path(), @@ -194,7 +195,7 @@ fn fetch_new_remote( remote_name: &str, source: &str, depth: Option, -) -> Result { +) -> Result, CommandError> { let git_repo = get_git_repo(workspace_command.repo().store())?; git::add_remote(&git_repo, remote_name, source)?; writeln!( @@ -204,29 +205,23 @@ fn fetch_new_remote( )?; let git_settings = workspace_command.settings().git_settings()?; let mut fetch_tx = workspace_command.start_transaction(); - let stats = with_remote_git_callbacks(ui, None, &git_settings, |cb| { - git::fetch( - fetch_tx.repo_mut(), - &git_repo, - remote_name, - &[StringPattern::everything()], - cb, - &git_settings, - depth, - ) - }) - .map_err(|err| match err { - GitFetchError::NoSuchRemote(_) => { - panic!("shouldn't happen as we just created the git remote") - } - GitFetchError::GitImportError(err) => CommandError::from(err), - GitFetchError::InternalGitError(err) => map_git_error(err), - GitFetchError::Subprocess(err) => user_error(err), - GitFetchError::InvalidBranchPattern => { - unreachable!("we didn't provide any globs") - } + let mut git_fetch = GitFetch::new(fetch_tx.repo_mut(), &git_repo, &git_settings); + let default_branch = with_remote_git_callbacks(ui, None, &git_settings, |cb| { + git_fetch + .fetch(remote_name, &[StringPattern::everything()], cb, depth) + .map_err(|err| match err { + GitFetchError::NoSuchRemote(_) => { + panic!("shouldn't happen as we just created the git remote") + } + GitFetchError::InternalGitError(err) => map_git_error(err), + GitFetchError::Subprocess(err) => user_error(err), + GitFetchError::InvalidBranchPattern => { + unreachable!("we didn't provide any globs") + } + }) })?; - print_git_import_stats(ui, fetch_tx.repo(), &stats.import_stats, true)?; + let import_stats = git_fetch.import_refs()?; + print_git_import_stats(ui, fetch_tx.repo(), &import_stats, true)?; fetch_tx.finish(ui, "fetch from git remote into empty repo")?; - Ok(stats) + Ok(default_branch) } diff --git a/cli/src/git_util.rs b/cli/src/git_util.rs index 49c9e0acfc6..0ab38fd04b9 100644 --- a/cli/src/git_util.rs +++ b/cli/src/git_util.rs @@ -33,6 +33,7 @@ use jj_lib::fmt_util::binary_prefix; use jj_lib::git; use jj_lib::git::FailedRefExport; use jj_lib::git::FailedRefExportReason; +use jj_lib::git::GitFetch; use jj_lib::git::GitFetchError; use jj_lib::git::GitImportStats; use jj_lib::git::RefName; @@ -641,51 +642,47 @@ export or their "parent" bookmarks."#, Ok(()) } +// TODO: move to cli/src/commands/git/fetch +// No other aprt of the code is using this pub fn git_fetch( ui: &mut Ui, tx: &mut WorkspaceCommandTransaction, git_repo: &git2::Repository, remotes: &[String], - branch: &[StringPattern], + branch_names: &[StringPattern], ) -> Result<(), CommandError> { let git_settings = tx.settings().git_settings()?; - - for remote in remotes { - let stats = with_remote_git_callbacks(ui, None, &git_settings, |cb| { - git::fetch( - tx.repo_mut(), - git_repo, - remote, - branch, - cb, - &git_settings, - None, - ) - }) - .map_err(|err| match err { - GitFetchError::InvalidBranchPattern => { - if branch - .iter() - .any(|pattern| pattern.as_exact().is_some_and(|s| s.contains('*'))) - { - user_error_with_hint( - "Branch names may not include `*`.", - "Prefix the pattern with `glob:` to expand `*` as a glob", - ) - } else { - user_error(err) - } - } - GitFetchError::GitImportError(err) => err.into(), - GitFetchError::InternalGitError(err) => map_git_error(err), - _ => user_error(err), + let mut git_fetch = GitFetch::new(tx.repo_mut(), git_repo, &git_settings); + + for remote_name in remotes { + with_remote_git_callbacks(ui, None, &git_settings, |cb| { + git_fetch + .fetch(remote_name, branch_names, cb, None) + .map_err(|err| match err { + GitFetchError::InvalidBranchPattern => { + if branch_names + .iter() + .any(|pattern| pattern.as_exact().is_some_and(|s| s.contains('*'))) + { + user_error_with_hint( + "Branch names may not include `*`.", + "Prefix the pattern with `glob:` to expand `*` as a glob", + ) + } else { + user_error(err) + } + } + GitFetchError::InternalGitError(err) => map_git_error(err), + _ => user_error(err), + }) })?; - print_git_import_stats(ui, tx.repo(), &stats.import_stats, true)?; } + let import_stats = git_fetch.import_refs()?; + print_git_import_stats(ui, tx.repo(), &import_stats, true)?; warn_if_branches_not_found( ui, tx, - branch, + branch_names, &remotes.iter().map(StringPattern::exact).collect_vec(), ) } diff --git a/cli/tests/test_git_fetch.rs b/cli/tests/test_git_fetch.rs index 6545ad1c6a5..c4ef9289998 100644 --- a/cli/tests/test_git_fetch.rs +++ b/cli/tests/test_git_fetch.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. use std::path::Path; +use std::path::PathBuf; use test_case::test_case; @@ -51,6 +52,33 @@ fn add_git_remote(test_env: &TestEnvironment, repo_path: &Path, remote: &str) { ); } +/// Clone a git repo from a source, add a commit on top +fn clone_git_add_commit(test_env: &TestEnvironment, src_remote: &str, remote: &str) { + let src_repo_path = test_env.env_root().join(src_remote); + let git_repo_path = test_env.env_root().join(remote); + let git_repo = git2::Repository::clone(src_repo_path.to_str().unwrap(), git_repo_path).unwrap(); + let signature = + git2::Signature::new("Some One", "some.one@example.com", &git2::Time::new(0, 0)).unwrap(); + let mut tree_builder = git_repo.treebuilder(None).unwrap(); + let file_oid = git_repo.blob(remote.as_bytes()).unwrap(); + tree_builder + .insert("file", file_oid, git2::FileMode::Blob.into()) + .unwrap(); + let tree_oid = tree_builder.write().unwrap(); + let tree = git_repo.find_tree(tree_oid).unwrap(); + // our branch name is the same as the source branch name + git_repo + .commit( + Some(&format!("refs/heads/{src_remote}")), + &signature, + &signature, + "message", + &tree, + &[], + ) + .unwrap(); +} + fn get_bookmark_output(test_env: &TestEnvironment, repo_path: &Path) -> String { // --quiet to suppress deleted bookmarks hint test_env.jj_cmd_success(repo_path, &["bookmark", "list", "--all-remotes", "--quiet"]) @@ -300,10 +328,7 @@ fn test_git_fetch_nonexistent_remote(subprocess: bool) { &["git", "fetch", "--remote", "rem1", "--remote", "rem2"], ); insta::allow_duplicates! { - insta::assert_snapshot!(stderr, @r###" - bookmark: rem1@rem1 [new] untracked - Error: No git remote named 'rem2' - "###); + insta::assert_snapshot!(stderr, @"Error: No git remote named 'rem2'"); } insta::allow_duplicates! { // No remote should have been fetched as part of the failing transaction @@ -325,11 +350,10 @@ fn test_git_fetch_nonexistent_remote_from_config(subprocess: bool) { let stderr = &test_env.jj_cmd_failure(&repo_path, &["git", "fetch"]); insta::allow_duplicates! { - insta::assert_snapshot!(stderr, @r###" - bookmark: rem1@rem1 [new] untracked - Error: No git remote named 'rem2' - "###); + insta::assert_snapshot!(stderr, @"Error: No git remote named 'rem2'"); + } // No remote should have been fetched as part of the failing transaction + insta::allow_duplicates! { insta::assert_snapshot!(get_bookmark_output(&test_env, &repo_path), @""); } } @@ -1755,3 +1779,47 @@ fn test_git_fetch_remote_only_bookmark(subprocess: bool) { "###); } } + +#[test_case(false; "use git2 for remote calls")] +#[test_case(true; "spawn a git subprocess for remote calls")] +fn test_git_fetch_multiple_remotes_same_commit(subprocess: bool) { + fn setup_env(subprocess: bool) -> (TestEnvironment, PathBuf) { + let test_env = TestEnvironment::default(); + if subprocess { + test_env.set_up_git_subprocessing(); + } + test_env.add_config("git.auto-local-bookmark = true"); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + add_git_remote(&test_env, &repo_path, "rem1"); + clone_git_add_commit(&test_env, "rem1", "rem2"); + test_env.jj_cmd_ok(&repo_path, &["git", "remote", "add", "rem2", "../rem2"]); + + (test_env, repo_path.clone()) + } + + // fetch with different orderings + let (test_env1, repo_path1) = setup_env(subprocess); + test_env1.jj_cmd_ok( + &repo_path1, + &["git", "fetch", "--remote", "rem1", "--remote", "rem2"], + ); + let (test_env2, repo_path2) = setup_env(subprocess); + test_env2.jj_cmd_ok( + &repo_path2, + &["git", "fetch", "--remote", "rem2", "--remote", "rem1"], + ); + + let bookmark_output1 = get_bookmark_output(&test_env1, &repo_path1); + let bookmark_output2 = get_bookmark_output(&test_env2, &repo_path2); + assert_eq!(bookmark_output1, bookmark_output2); + insta::allow_duplicates! { + insta::assert_snapshot!(bookmark_output1, @r" + rem1 (conflicted): + + qxosxrvv 6a211027 message + + yszkquru 2497a8a0 message + @rem1 (behind by 1 commits): qxosxrvv 6a211027 message + @rem2 (behind by 1 commits): yszkquru 2497a8a0 message + "); + } +} diff --git a/lib/src/git.rs b/lib/src/git.rs index bad86045c62..71a64f82932 100644 --- a/lib/src/git.rs +++ b/lib/src/git.rs @@ -1457,8 +1457,6 @@ pub enum GitFetchError { chars = INVALID_REFSPEC_CHARS.iter().join("`, `") )] InvalidBranchPattern, - #[error("Failed to import Git refs")] - GitImportError(#[from] GitImportError), // TODO: I'm sure there are other errors possible, such as transport-level errors. #[error("Unexpected git error when fetching")] InternalGitError(#[from] git2::Error), @@ -1484,36 +1482,29 @@ fn git2_fetch_options( } struct FetchedBranches { - branches: Vec, remote: String, + branches: Vec, } -struct GitFetch<'a> { +/// Helper struct to execute multiple `git fetch` operations +pub struct GitFetch<'a> { mut_repo: &'a mut MutableRepo, git_repo: &'a git2::Repository, git_settings: &'a GitSettings, - // for git2 only - fetch_options: git2::FetchOptions<'a>, fetched: Vec, - // for subprocess only - depth: Option, } impl<'a> GitFetch<'a> { - fn new( + pub fn new( mut_repo: &'a mut MutableRepo, git_repo: &'a git2::Repository, git_settings: &'a GitSettings, - fetch_options: git2::FetchOptions<'a>, - depth: Option, ) -> Self { GitFetch { mut_repo, git_repo, git_settings, - fetch_options, fetched: vec![], - depth, } } @@ -1544,8 +1535,10 @@ impl<'a> GitFetch<'a> { fn git2_fetch( &mut self, - branch_names: &[StringPattern], + callbacks: RemoteCallbacks<'_>, + depth: Option, remote_name: &str, + branch_names: &[StringPattern], ) -> Result, GitFetchError> { let mut remote = self.git_repo.find_remote(remote_name).map_err(|err| { if is_remote_not_found_err(&err) { @@ -1567,7 +1560,7 @@ impl<'a> GitFetch<'a> { } tracing::debug!("remote.download"); - remote.download(&refspecs, Some(&mut self.fetch_options))?; + remote.download(&refspecs, Some(&mut git2_fetch_options(callbacks, depth)))?; tracing::debug!("remote.prune"); remote.prune(None)?; tracing::debug!("remote.update_tips"); @@ -1598,8 +1591,9 @@ impl<'a> GitFetch<'a> { fn subprocess_fetch( &mut self, - branch_names: &[StringPattern], + depth: Option, remote_name: &str, + branch_names: &[StringPattern], ) -> Result, GitFetchError> { let git_ctx = GitSubprocessContext::from_git2(self.git_repo, &self.git_settings.executable_path); @@ -1626,7 +1620,7 @@ impl<'a> GitFetch<'a> { // even more unfortunately, git errors out one refspec at a time, // meaning that the below cycle runs in O(#failed refspecs) while let Some(failing_refspec) = - git_ctx.spawn_fetch(remote_name, self.depth, &remaining_refspecs)? + git_ctx.spawn_fetch(remote_name, depth, &remaining_refspecs)? { remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec)); @@ -1652,20 +1646,23 @@ impl<'a> GitFetch<'a> { /// /// Keeps track of the {branch_names, remote_name} pair the refs can be /// subsequently imported into the `jj` repo by calling `import_refs()`. - fn fetch( + #[tracing::instrument(skip(self, callbacks))] + pub fn fetch( &mut self, - branch_names: &[StringPattern], remote_name: &str, + branch_names: &[StringPattern], + callbacks: RemoteCallbacks<'_>, + depth: Option, ) -> Result, GitFetchError> { let default_branch = if self.git_settings.subprocess { - self.subprocess_fetch(branch_names, remote_name) + self.subprocess_fetch(depth, remote_name, branch_names) } else { - self.git2_fetch(branch_names, remote_name) + self.git2_fetch(callbacks, depth, remote_name, branch_names) }; self.fetched.push(FetchedBranches { - branches: branch_names.to_vec(), remote: remote_name.to_string(), + branches: branch_names.to_vec(), }); default_branch @@ -1678,6 +1675,7 @@ impl<'a> GitFetch<'a> { /// Clears all yet-to-be-imported {branch_names, remote_name} pairs after /// the import. If `fetch()` has not been called since the last time /// `import_refs()` was called then this will be a no-op. + #[tracing::instrument(skip(self))] pub fn import_refs(&mut self) -> Result { tracing::debug!("import_refs"); let import_stats = @@ -1708,45 +1706,6 @@ impl<'a> GitFetch<'a> { } } -/// Describes successful `fetch()` result. -#[derive(Clone, Debug, Eq, PartialEq, Default)] -pub struct GitFetchStats { - /// Remote's default branch. - pub default_branch: Option, - /// Changes made by the import. - pub import_stats: GitImportStats, -} - -#[tracing::instrument(skip(mut_repo, git_repo, callbacks))] -pub fn fetch( - mut_repo: &mut MutableRepo, - git_repo: &git2::Repository, - remote_name: &str, - branch_names: &[StringPattern], - callbacks: RemoteCallbacks<'_>, - git_settings: &GitSettings, - depth: Option, -) -> Result { - let mut git_fetch = GitFetch::new( - mut_repo, - git_repo, - git_settings, - if git_settings.subprocess { - git2::FetchOptions::default() - } else { - git2_fetch_options(callbacks, depth) - }, - depth, - ); - let default_branch = git_fetch.fetch(branch_names, remote_name)?; - let import_stats = git_fetch.import_refs()?; - let stats = GitFetchStats { - default_branch, - import_stats, - }; - Ok(stats) -} - #[derive(Error, Debug)] pub enum GitPushError { #[error("No git remote named '{0}'")] diff --git a/lib/tests/test_git.rs b/lib/tests/test_git.rs index a3a6e72e055..8538a102db4 100644 --- a/lib/tests/test_git.rs +++ b/lib/tests/test_git.rs @@ -40,6 +40,7 @@ use jj_lib::commit_builder::CommitBuilder; use jj_lib::git; use jj_lib::git::FailedRefExportReason; use jj_lib::git::GitBranchPushTargets; +use jj_lib::git::GitFetch; use jj_lib::git::GitFetchError; use jj_lib::git::GitImportError; use jj_lib::git::GitPushError; @@ -74,6 +75,15 @@ use testutils::write_random_commit; use testutils::TestRepo; use testutils::TestRepoBackend; +/// Describes successful `fetch()` result. +#[derive(Clone, Debug, Eq, PartialEq, Default)] +struct GitFetchStats { + /// Remote's default branch. + pub default_branch: Option, + /// Changes made by the import. + pub import_stats: git::GitImportStats, +} + fn empty_git_commit<'r>( git_repo: &'r git2::Repository, ref_name: &str, @@ -127,6 +137,29 @@ fn get_git_settings(subprocess: bool) -> GitSettings { } } +fn git_fetch( + mut_repo: &mut MutableRepo, + git_repo: &git2::Repository, + remote_name: &str, + branch_names: &[StringPattern], + git_settings: &GitSettings, +) -> Result { + let mut git_fetch = GitFetch::new(mut_repo, git_repo, git_settings); + let default_branch = git_fetch.fetch( + remote_name, + branch_names, + git::RemoteCallbacks::default(), + None, + )?; + + let import_stats = git_fetch.import_refs().unwrap(); + let stats = GitFetchStats { + default_branch, + import_stats, + }; + Ok(stats) +} + #[test] fn test_import_refs() { let git_settings = GitSettings { @@ -2548,14 +2581,12 @@ fn test_fetch_empty_repo(subprocess: bool) { let git_settings = get_git_settings(subprocess); let mut tx = test_data.repo.start_transaction(); - let stats = git::fetch( + let stats = git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", &[StringPattern::everything()], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap(); // No default bookmark and no refs @@ -2576,21 +2607,19 @@ fn test_fetch_initial_commit_head_is_not_set(subprocess: bool) { let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]); let mut tx = test_data.repo.start_transaction(); - let stats = git::fetch( + let stats = git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", &[StringPattern::everything()], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap(); // No default bookmark because the origin repo's HEAD wasn't set assert_eq!(stats.default_branch, None); assert!(stats.import_stats.abandoned_commits.is_empty()); let repo = tx.commit("test").unwrap(); - // The initial commit is visible after git::fetch(). + // The initial commit is visible after git_fetch(). let view = repo.view(); assert!(view.heads().contains(&jj_id(&initial_git_commit))); let initial_commit_target = RefTarget::normal(jj_id(&initial_git_commit)); @@ -2638,14 +2667,12 @@ fn test_fetch_initial_commit_head_is_set(subprocess: bool) { .unwrap(); let mut tx = test_data.repo.start_transaction(); - let stats = git::fetch( + let stats = git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", &[StringPattern::everything()], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap(); @@ -2664,14 +2691,12 @@ fn test_fetch_success(subprocess: bool) { let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]); let mut tx = test_data.repo.start_transaction(); - git::fetch( + git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", &[StringPattern::everything()], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap(); test_data.repo = tx.commit("test").unwrap(); @@ -2688,14 +2713,12 @@ fn test_fetch_success(subprocess: bool) { .unwrap(); let mut tx = test_data.repo.start_transaction(); - let stats = git::fetch( + let stats = git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", &[StringPattern::everything()], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap(); // The default bookmark is "main" @@ -2747,14 +2770,12 @@ fn test_fetch_prune_deleted_ref(subprocess: bool) { let commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]); let mut tx = test_data.repo.start_transaction(); - git::fetch( + git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", &[StringPattern::everything()], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap(); // Test the setup @@ -2771,14 +2792,12 @@ fn test_fetch_prune_deleted_ref(subprocess: bool) { .delete() .unwrap(); // After re-fetching, the bookmark should be deleted - let stats = git::fetch( + let stats = git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", &[StringPattern::everything()], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap(); assert_eq!(stats.import_stats.abandoned_commits, vec![jj_id(&commit)]); @@ -2800,14 +2819,12 @@ fn test_fetch_no_default_branch(subprocess: bool) { let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]); let mut tx = test_data.repo.start_transaction(); - git::fetch( + git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", &[StringPattern::everything()], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap(); @@ -2824,14 +2841,12 @@ fn test_fetch_no_default_branch(subprocess: bool) { .set_head_detached(initial_git_commit.id()) .unwrap(); - let stats = git::fetch( + let stats = git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", &[StringPattern::everything()], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap(); // There is no default bookmark @@ -2847,14 +2862,12 @@ fn test_fetch_empty_refspecs(subprocess: bool) { // Base refspecs shouldn't be respected let mut tx = test_data.repo.start_transaction(); - git::fetch( + git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", &[], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap(); assert!(tx @@ -2875,14 +2888,12 @@ fn test_fetch_no_such_remote(subprocess: bool) { let test_data = GitRepoData::create(); let git_settings = get_git_settings(subprocess); let mut tx = test_data.repo.start_transaction(); - let result = git::fetch( + let result = git_fetch( tx.repo_mut(), &test_data.git_repo, "invalid-remote", &[StringPattern::everything()], - git::RemoteCallbacks::default(), &git_settings, - None, ); assert!(matches!(result, Err(GitFetchError::NoSuchRemote(_)))); } @@ -2897,7 +2908,7 @@ fn test_fetch_multiple_branches() { }; let mut tx = test_data.repo.start_transaction(); - let fetch_stats = git::fetch( + let fetch_stats = git_fetch( tx.repo_mut(), &test_data.git_repo, "origin", @@ -2906,9 +2917,7 @@ fn test_fetch_multiple_branches() { StringPattern::Exact("noexist1".to_string()), StringPattern::Exact("noexist2".to_string()), ], - git::RemoteCallbacks::default(), &git_settings, - None, ) .unwrap();