Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 85 additions & 3 deletions gix/src/remote/connection/fetch/update_refs/mod.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -471,5 +491,67 @@ fn worktree_branches(repo: &Repository) -> Result<BTreeMap<gix_ref::FullName, Ve
Ok(map)
}

/// A precomputed snapshot of packed-refs plus the set of loose ref names that
/// shadow them, used to answer `repo.try_find_reference()`-style queries for
/// the bulk of the `update()` loop without touching the filesystem or doing a
/// binary search per call.
struct LookupFastPath {
/// Packed-refs keyed by ref name (e.g. `refs/remotes/origin/foo`).
packed: HashMap<BString, gix_ref::Reference>,
/// Names of loose refs which shadow entries in `packed` and must take the
/// slow path so existing precedence is preserved.
loose_shadows: HashSet<BString>,
}

/// 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<LookupFastPath> {
// `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;
Loading