git: migrate import_refs() to gix::Repository

Gitoxide errors are boxed since there are various error types and they tend
to exceed the clippy size limit.

Apparently, gitoxide is faster than git2:

    % hyperfine --warmup 3 --runs 10 \
        "/tmp/jj-baseline --ignore-working-copy git import -R ~/mirrors/linux" \
        "/tmp/jj-gix --ignore-working-copy git import -R ~/mirrors/linux"
    Benchmark 1: /tmp/jj-baseline --ignore-working-copy git import -R ~/mirrors/linux
      Time (mean ± σ):     205.4 ms ±  15.7 ms    [User: 59.6 ms, System: 144.6 ms]
      Range (min … max):   189.7 ms … 223.9 ms    10 runs
    Benchmark 2: /tmp/jj-gix --ignore-working-copy git import -R ~/mirrors/linux
      Time (mean ± σ):     176.2 ms ±  13.7 ms    [User: 41.2 ms, System: 134.0 ms]
      Range (min … max):   155.4 ms … 186.5 ms    10 runs
This commit is contained in:
Yuya Nishihara 2023-11-08 20:21:16 +09:00
parent 6c98dfcdcb
commit 044716ee40

View file

@ -14,11 +14,12 @@
#![allow(missing_docs)] #![allow(missing_docs)]
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::default::Default; use std::default::Default;
use std::io::Read; use std::io::Read;
use std::path::PathBuf; use std::path::PathBuf;
use std::{fmt, iter}; use std::{fmt, iter, str};
use git2::Oid; use git2::Oid;
use itertools::Itertools; use itertools::Itertools;
@ -113,39 +114,52 @@ fn get_git_backend(store: &Store) -> Option<&GitBackend> {
/// Checks if `git_ref` points to a Git commit object, and returns its id. /// Checks if `git_ref` points to a Git commit object, and returns its id.
/// ///
/// If the ref points to the previously `known_target` (i.e. unchanged), this /// If the ref points to the previously `known_target` (i.e. unchanged), this
/// should be faster than `git_ref.peel_to_commit()`. /// should be faster than `git_ref.into_fully_peeled_id()`.
fn resolve_git_ref_to_commit_id( fn resolve_git_ref_to_commit_id(
git_ref: &git2::Reference<'_>, git_ref: &gix::Reference,
known_target: &RefTarget, known_target: &RefTarget,
) -> Option<CommitId> { ) -> Option<CommitId> {
let mut peeling_ref = Cow::Borrowed(git_ref);
// Try fast path if we have a candidate id which is known to be a commit object. // Try fast path if we have a candidate id which is known to be a commit object.
if let Some(id) = known_target.as_normal() { if let Some(id) = known_target.as_normal() {
if matches!(git_ref.target(), Some(oid) if oid.as_bytes() == id.as_bytes()) { let raw_ref = &git_ref.inner;
if matches!(raw_ref.target.try_id(), Some(oid) if oid.as_bytes() == id.as_bytes()) {
return Some(id.clone()); return Some(id.clone());
} }
if matches!(git_ref.target_peel(), Some(oid) if oid.as_bytes() == id.as_bytes()) { if matches!(raw_ref.peeled, Some(oid) if oid.as_bytes() == id.as_bytes()) {
// Perhaps an annotated tag stored in packed-refs file, and pointing to the // Perhaps an annotated tag stored in packed-refs file, and pointing to the
// already known target commit. // already known target commit.
return Some(id.clone()); return Some(id.clone());
} }
// A tag (according to ref name.) Try to peel one more level. This is slightly // A tag (according to ref name.) Try to peel one more level. This is slightly
// faster than recurse into peel_to_commit(). If we recorded a tag oid, we // faster than recurse into into_fully_peeled_id(). If we recorded a tag oid, we
// could skip this at all. // could skip this at all.
if let Some(Ok(tag)) = git_ref.is_tag().then(|| git_ref.peel_to_tag()) { if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
if tag.target_id().as_bytes() == id.as_bytes() { let maybe_tag = git_ref
// An annotated tag pointing to the already known target commit. .try_id()
return Some(id.clone()); .and_then(|id| id.object().ok())
} else { .and_then(|object| object.try_into_tag().ok());
// Unknown id. Recurse from the current state as git_object_peel() of if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
// libgit2 would do. A tag may point to non-commit object. if oid.as_bytes() == id.as_bytes() {
let git_commit = tag.into_object().peel_to_commit().ok()?; // An annotated tag pointing to the already known target commit.
return Some(CommitId::from_bytes(git_commit.id().as_bytes())); return Some(id.clone());
}
// Unknown id. Recurse from the current state. A tag may point to
// non-commit object.
peeling_ref.to_mut().inner.target = gix::refs::Target::Peeled(oid.detach());
} }
} }
} }
let git_commit = git_ref.peel_to_commit().ok()?; // Alternatively, we might want to inline the first half of the peeling
Some(CommitId::from_bytes(git_commit.id().as_bytes())) // loop. into_fully_peeled_id() looks up the target object to see if it's
// a tag or not, and we need to check if it's a commit object.
let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
let is_commit = peeled_id
.object()
.map_or(false, |object| object.kind.is_commit());
is_commit.then(|| CommitId::from_bytes(peeled_id.as_bytes()))
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -168,11 +182,17 @@ pub enum GitImportError {
)] )]
RemoteReservedForLocalGitRepo, RemoteReservedForLocalGitRepo,
#[error("Unexpected git error when importing refs: {0}")] #[error("Unexpected git error when importing refs: {0}")]
InternalGitError(#[from] git2::Error), InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("The repo is not backed by a Git repo")] #[error("The repo is not backed by a Git repo")]
UnexpectedBackend, UnexpectedBackend,
} }
impl GitImportError {
fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
GitImportError::InternalGitError(source.into())
}
}
/// Describes changes made by `import_refs()` or `fetch()`. /// Describes changes made by `import_refs()` or `fetch()`.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitImportStats { pub struct GitImportStats {
@ -211,15 +231,12 @@ pub fn import_some_refs(
) -> Result<GitImportStats, GitImportError> { ) -> Result<GitImportStats, GitImportError> {
let store = mut_repo.store(); let store = mut_repo.store();
let git_backend = get_git_backend(store).ok_or(GitImportError::UnexpectedBackend)?; let git_backend = get_git_backend(store).ok_or(GitImportError::UnexpectedBackend)?;
let git_repo = git_backend.open_git_repo()?; // TODO: use gix::Repository let git_repo = git_backend.git_repo();
// TODO: Should this be a separate function? We may not always want to import // TODO: Should this be a separate function? We may not always want to import
// the Git HEAD (and add it to our set of heads). // the Git HEAD (and add it to our set of heads).
let old_git_head = mut_repo.view().git_head(); let old_git_head = mut_repo.view().git_head();
let changed_git_head = if let Ok(head_git_commit) = git_repo let changed_git_head = if let Ok(head_git_commit) = git_repo.head_commit() {
.head()
.and_then(|head_ref| head_ref.peel_to_commit())
{
// The current HEAD is not added to `hidable_git_heads` because HEAD move // The current HEAD is not added to `hidable_git_heads` because HEAD move
// doesn't automatically mean the old HEAD branch has been rewritten. // doesn't automatically mean the old HEAD branch has been rewritten.
let head_commit_id = CommitId::from_bytes(head_git_commit.id().as_bytes()); let head_commit_id = CommitId::from_bytes(head_git_commit.id().as_bytes());
@ -369,7 +386,7 @@ fn abandon_unreachable_commits(
/// Calculates diff of git refs to be imported. /// Calculates diff of git refs to be imported.
fn diff_refs_to_import( fn diff_refs_to_import(
view: &View, view: &View,
git_repo: &git2::Repository, git_repo: &gix::Repository,
git_ref_filter: impl Fn(&RefName) -> bool, git_ref_filter: impl Fn(&RefName) -> bool,
) -> Result<RefsToImport, GitImportError> { ) -> Result<RefsToImport, GitImportError> {
let mut known_git_refs: HashMap<&str, &RefTarget> = view let mut known_git_refs: HashMap<&str, &RefTarget> = view
@ -409,9 +426,10 @@ fn diff_refs_to_import(
.collect(); .collect();
let mut changed_git_refs = Vec::new(); let mut changed_git_refs = Vec::new();
let mut changed_remote_refs = BTreeMap::new(); let mut changed_remote_refs = BTreeMap::new();
for git_ref in git_repo.references()? { let git_references = git_repo.references().map_err(GitImportError::from_git)?;
let git_ref = git_ref?; for git_ref in git_references.all().map_err(GitImportError::from_git)? {
let Some(full_name) = git_ref.name() else { let git_ref = git_ref.map_err(GitImportError::from_git)?;
let Ok(full_name) = str::from_utf8(git_ref.name().as_bstr()) else {
// Skip non-utf8 refs. // Skip non-utf8 refs.
continue; continue;
}; };