Skip to content

Commit

Permalink
jj-lib: refactor jj git clone|fetch and `to use new GitFetch api di…
Browse files Browse the repository at this point in the history
…rectly.

* Make the new `GitFetch` api public.
* Rewrite all tests that used `git::fetch*` to use the new API.
  One interesting artifact of the new API is that it holds onto a reference
  of the `tx.repo_mut()`, so the GitFetch object needs to fall go of scope
  completely before we can make any other mutable calls to `tx`.
* Delete the `git::fetch`
* Update `jj git clone` and `git_fetch` in `cli/src/git_utils.rs` to use
  the new api directly. Removing one redundant layer of indirection.
* This fixes #4923 as it first fetches from all remotes before `import_refs()`
  is called, so there is no race condition if the same commit is treated
  differently in different remotes specified in the same command.

Fixes: #4920
  • Loading branch information
essiene committed Dec 1, 2024
1 parent 694dfb8 commit 0f2cc4d
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 200 deletions.
44 changes: 26 additions & 18 deletions cli/src/commands/git/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use std::path::PathBuf;

use jj_lib::config::ConfigNamePathBuf;
use jj_lib::git;
use jj_lib::git::GitFetch;
use jj_lib::git::GitFetchError;
use jj_lib::git::GitFetchStats;
use jj_lib::repo::Repo;
Expand Down Expand Up @@ -221,27 +222,34 @@ fn do_git_clone(
git_repo.remote(remote_name, source).unwrap();
let mut fetch_tx = workspace_command.start_transaction();

let stats = with_remote_git_callbacks(ui, None, |cb| {
git::fetch(
let stats = with_remote_git_callbacks(ui, None, |cb| -> Result<GitFetchStats, CommandError> {
let git_settings = command.settings().git_settings();
let mut git_fetch = GitFetch::new(
fetch_tx.repo_mut(),
&git_repo,
remote_name,
&[StringPattern::everything()],
cb,
&command.settings().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::InvalidBranchPattern => {
unreachable!("we didn't provide any globs")
}
&git_settings,
git::fetch_options(cb, depth),
);

let default_branch = git_fetch
.fetch(&[StringPattern::everything()], remote_name)
.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::InvalidBranchPattern => {
unreachable!("we didn't provide any globs")
}
})?;
let import_stats = git_fetch.import_refs()?;
Ok(GitFetchStats {
default_branch,
import_stats,
})
})?;

print_git_import_stats(ui, fetch_tx.repo(), &stats.import_stats, true)?;
fetch_tx.finish(ui, "fetch from git remote into empty repo")?;
Ok((workspace_command, stats))
Expand Down
61 changes: 32 additions & 29 deletions cli/src/git_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use itertools::Itertools;
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;
Expand Down Expand Up @@ -456,47 +457,49 @@ export or their "parent" bookmarks."#,
Ok(())
}

// TODO: Move this back to cli/src/commands/git/fetch.rs
// With the new `GitFetch` api, this function is too specialized
// to the `jj git fetch` command and should not be reused.
pub fn git_fetch(
ui: &mut Ui,
tx: &mut WorkspaceCommandTransaction,
git_repo: &git2::Repository,
remotes: &[String],
branch: &[StringPattern],
) -> Result<(), CommandError> {
let git_settings = tx.settings().git_settings();

for remote in remotes {
let stats = with_remote_git_callbacks(ui, None, |cb| {
git::fetch(
let import_stats =
with_remote_git_callbacks(ui, None, |cb| -> Result<GitImportStats, CommandError> {
let git_settings = tx.settings().git_settings();
let mut git_fetch = GitFetch::new(
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)
}
git::fetch_options(cb, None),
);

for remote in remotes {
git_fetch.fetch(branch, remote).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),
})?;
}
GitFetchError::GitImportError(err) => err.into(),
GitFetchError::InternalGitError(err) => map_git_error(err),
_ => user_error(err),
Ok(git_fetch.import_refs()?)
})?;
print_git_import_stats(ui, tx.repo(), &stats.import_stats, true)?;
}
print_git_import_stats(ui, tx.repo(), &import_stats, true)?;
warn_if_branches_not_found(
ui,
tx,
Expand Down
10 changes: 2 additions & 8 deletions cli/tests/test_git_fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,7 @@ fn test_git_fetch_nonexistent_remote() {
&repo_path,
&["git", "fetch", "--remote", "rem1", "--remote", "rem2"],
);
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::assert_snapshot!(get_bookmark_output(&test_env, &repo_path), @"");
}
Expand All @@ -254,10 +251,7 @@ fn test_git_fetch_nonexistent_remote_from_config() {
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###"
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::assert_snapshot!(get_bookmark_output(&test_env, &repo_path), @"");
}
Expand Down
33 changes: 4 additions & 29 deletions lib/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1229,7 +1229,7 @@ pub enum GitFetchError {
InternalGitError(#[from] git2::Error),
}

fn fetch_options(
pub fn fetch_options(
callbacks: RemoteCallbacks<'_>,
depth: Option<NonZeroU32>,
) -> git2::FetchOptions<'_> {
Expand All @@ -1251,7 +1251,7 @@ struct FetchedBranches {
remote: String,
}

struct GitFetch<'a> {
pub struct GitFetch<'a> {
mut_repo: &'a mut MutableRepo,
git_repo: &'a git2::Repository,
git_settings: &'a GitSettings,
Expand All @@ -1260,7 +1260,7 @@ struct GitFetch<'a> {
}

impl<'a> GitFetch<'a> {
fn new(
pub fn new(
mut_repo: &'a mut MutableRepo,
git_repo: &'a git2::Repository,
git_settings: &'a GitSettings,
Expand All @@ -1280,7 +1280,7 @@ 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(
pub fn fetch(
&mut self,
branch_names: &[StringPattern],
remote_name: &str,
Expand Down Expand Up @@ -1394,31 +1394,6 @@ pub struct GitFetchStats {
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<NonZeroU32>,
) -> Result<GitFetchStats, GitFetchError> {
let mut git_fetch = GitFetch::new(
mut_repo,
git_repo,
git_settings,
fetch_options(callbacks, 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, PartialEq)]
pub enum GitPushError {
#[error("No git remote named '{0}'")]
Expand Down
Loading

0 comments on commit 0f2cc4d

Please sign in to comment.