diff --git a/gix/src/remote/connection/fetch/update_refs/mod.rs b/gix/src/remote/connection/fetch/update_refs/mod.rs index 2494dbe95d2..3907e8e2b3c 100644 --- a/gix/src/remote/connection/fetch/update_refs/mod.rs +++ b/gix/src/remote/connection/fetch/update_refs/mod.rs @@ -1,7 +1,13 @@ #![allow(clippy::result_large_err)] -use std::{collections::BTreeMap, path::PathBuf}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + path::PathBuf, +}; -use gix_object::Exists; +use gix_object::{ + Exists, + bstr::{BString, ByteSlice}, +}; use gix_ref::{ Target, TargetRef, transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, @@ -77,6 +83,15 @@ pub(crate) fn update( let mut edit_indices_to_validate = Vec::new(); let mut checked_out_branches = worktree_branches(repo)?; + // For wide-refs fetches (e.g. mirror clones with 100k+ branches in + // `packed-refs`) the per-mapping `repo.try_find_reference()` call below + // dominates wall time: each call does one filesystem stat for a loose + // ref plus a binary search over `packed-refs`. The fast path here is + // purely additive — if either snapshot fails to build, fall through to + // the original lookup with unchanged semantics. Loose refs shadow + // packed entries, so the HashMap is only consulted when no loose ref + // exists for that name. + let lookup_fast_path = build_lookup_fast_path(repo); let implicit_tag_refspec = fetch_tags .to_refspec() .filter(|_| matches!(fetch_tags, crate::remote::fetch::Tags::Included)); @@ -113,7 +128,12 @@ pub(crate) fn update( } let (mode, edit_index, type_change) = match local { Some(name) => { - let (mode, reflog_message, name, previous_value) = match repo.try_find_reference(name)? { + let existing = match lookup_fast_path.as_ref().map(|fp| fp.lookup(name.as_bstr())) { + Some(LookupOutcome::Hit(r)) => Some(crate::Reference::from_ref(r, repo)), + Some(LookupOutcome::NotFound) => None, + Some(LookupOutcome::TakeSlowPath) | None => repo.try_find_reference(name)?, + }; + let (mode, reflog_message, name, previous_value) = match existing { Some(existing) => { if let Some(wt_dirs) = checked_out_branches.get_mut(existing.name()) { wt_dirs.sort(); @@ -471,5 +491,67 @@ fn worktree_branches(repo: &Repository) -> Result, + /// Names of loose refs which shadow entries in `packed` and must take the + /// slow path so existing precedence is preserved. + loose_shadows: HashSet, +} + +/// Result of a fast-path lookup. `TakeSlowPath` is distinct from `NotFound`: +/// the former means "we can't answer from the snapshot alone, defer to +/// `repo.try_find_reference()`" (e.g. a loose ref shadows this name), while +/// the latter means "we are certain no ref exists with this name". +enum LookupOutcome { + Hit(gix_ref::Reference), + NotFound, + TakeSlowPath, +} + +impl LookupFastPath { + fn lookup(&self, name: &gix_object::bstr::BStr) -> LookupOutcome { + if self.loose_shadows.contains(name) { + return LookupOutcome::TakeSlowPath; + } + match self.packed.get(name) { + Some(r) => LookupOutcome::Hit(r.clone()), + None => LookupOutcome::NotFound, + } + } +} + +/// Build [`LookupFastPath`] for the repo, returning `None` if either of the +/// inputs (packed-refs snapshot, loose refs enumeration) is unavailable. A +/// `None` return causes the caller to fall back to the unmodified slow path, +/// so the optimization is purely additive. +fn build_lookup_fast_path(repo: &Repository) -> Option { + // `cached_packed_buffer()` returns raw, namespace-prefixed names from + // `packed-refs`, while lookups here use namespace-stripped local names. + // Defer to the slow path, which applies the namespace correctly. + if repo.refs.namespace.is_some() { + return None; + } + let buf = repo.refs.cached_packed_buffer().ok().flatten()?; + let mut packed = HashMap::new(); + for r in buf.iter().ok()? { + // Bail to the slow path on parse errors so corruption surfaces via + // `repo.try_find_reference()` instead of being silently dropped. + let r = r.ok()?; + let name = r.name.as_bstr().to_owned(); + packed.insert(name, r.into()); + } + let mut loose_shadows = HashSet::new(); + for r in repo.refs.loose_iter().ok()? { + let r = r.ok()?; + loose_shadows.insert(r.name.as_bstr().to_owned()); + } + Some(LookupFastPath { packed, loose_shadows }) +} + #[cfg(test)] mod tests;