Skip to main content

grit_lib/
pack_object_select.rs

1//! Object selection for pack-objects (walk or pack bitmap reachability).
2
3use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
4use std::fmt;
5use std::sync::Arc;
6
7use crate::alternate_boundary::AlternateSources;
8use crate::bitmap_walk::{BitmapWalkError, ReachabilityQuery, ReachableSet};
9use crate::commit_graph_file::{CommitGraphChain, UNRANKED_GENERATION};
10use crate::environment::RepositoryOptions;
11use crate::error::{Error, Result};
12use crate::objects::{
13    oid_hash_map, oid_hash_set, oid_hash_set_with_capacity, parse_tag, tag_object_line_oid,
14    CommitRef, Object, ObjectId, ObjectKind, OidHashMap, OidHashSet, TreeRefIter,
15};
16use crate::odb::store::{CompositeStore, ObjectStore};
17use crate::odb::Odb;
18use crate::pack_bitmap::BitmapIndex;
19use crate::pack_name_hash::pack_name_hash;
20use crate::progress::CancelSignal;
21use crate::repo::Repository;
22use crate::rev_list::{MissingAction, ObjectFilter};
23
24/// Objects to emit in a negotiated pack plus peer-held closure for thin deltas.
25#[derive(Clone)]
26#[allow(clippy::type_complexity)]
27pub struct PackObjectSelection {
28    /// Objects reachable from `wants` but not from `haves`, in valid pack order.
29    pub send: Vec<ObjectId>,
30    /// Peer-held commits whose trees seed thin-pack delta bases, not the full peer
31    /// object closure: the edge commits where the object walk reached the peer's
32    /// history, or the negotiation `haves` when bitmaps (or a path filter) selected
33    /// the objects instead.
34    pub have_closure: HashSet<ObjectId>,
35    /// Peer-held objects when pack reachability bitmaps are available (thin-pack bases).
36    pub peer_held: Option<Arc<ReachableSet>>,
37    /// Pack name hash of the path each object was found at, used to pair delta
38    /// candidates: blobs from the tree walk, every object the bitmap name-hash
39    /// cache covers when bitmaps are used.
40    pub name_hashes: HashMap<ObjectId, u32>,
41}
42
43impl fmt::Debug for PackObjectSelection {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.debug_struct("PackObjectSelection")
46            .field("send", &self.send)
47            .field("have_closure", &self.have_closure)
48            .field(
49                "peer_held",
50                &self
51                    .peer_held
52                    .as_ref()
53                    .map(|held| held.count())
54                    .unwrap_or(0),
55            )
56            .field("name_hashes", &self.name_hashes)
57            .finish()
58    }
59}
60
61/// How client `haves` bound the want-side object walk.
62#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
63pub enum HaveBoundary {
64    /// Pre-walk all commits reachable from every `have` before the want walk.
65    #[default]
66    CommitClosure,
67    /// Git's `--revs` / `^have` walk, used by send-pack and upload-pack: peel have
68    /// tips, mark them uninteresting, and interleave a commit-date walk with wants
69    /// until the queue is only uninteresting commits (no full have closure pre-walk).
70    TipsOnly,
71}
72
73/// Parameters for [`enumerate_pack_objects`].
74#[derive(Clone, Copy, Debug)]
75pub struct PackEnumerateOptions<'a> {
76    /// Partial-clone filter applied to the want side (when supported).
77    pub filter: Option<&'a ObjectFilter>,
78    /// Shallow graft boundaries on the want side: do not walk past these commits
79    /// when enumerating objects to send. When non-empty, bitmap enumeration is
80    /// skipped and the object walk is used.
81    pub shallow_grafts: &'a HashSet<ObjectId>,
82    /// Shallow graft boundaries when walking client `haves` to build the peer
83    /// closure. Defaults to [`Self::shallow_grafts`] when unset. When non-empty,
84    /// bitmaps are not used: a bitmap closure of a `have` runs past the client's
85    /// shallow boundary into history the client does not hold.
86    pub have_shallow_grafts: Option<&'a HashSet<ObjectId>>,
87    /// When false, always use the object walk even if bitmaps exist.
88    pub use_bitmaps: bool,
89    /// Extra negotiation boundaries merged into the walk (legacy local-fetch hints).
90    pub exclude_objects: Option<&'a HashSet<ObjectId>>,
91    /// When `wants` are read from another ODB, objects already stored here are boundaries.
92    pub peer_odb: Option<&'a Odb>,
93    /// Whether `haves` are expanded to a full commit closure before the want walk.
94    pub have_boundary: HaveBoundary,
95}
96
97impl PackEnumerateOptions<'_> {
98    /// Whether any shallow graft bounds either side of the walk, which rules out
99    /// bitmap reachability (bitmaps record full, ungrafted closures).
100    fn has_shallow_grafts(&self) -> bool {
101        !self.shallow_grafts.is_empty() || self.have_shallow_grafts.is_some_and(|g| !g.is_empty())
102    }
103}
104
105/// Select pack objects using bitmap reachability when possible, otherwise the object walk.
106///
107/// # Errors
108///
109/// Propagates repository read failures. Missing `want` objects error; missing `have` objects are skipped.
110pub fn enumerate_pack_objects(
111    odb: &Odb,
112    wants: &[ObjectId],
113    haves: &[ObjectId],
114    opts: &PackEnumerateOptions<'_>,
115) -> Result<PackObjectSelection> {
116    enumerate_pack_objects_naming(
117        odb,
118        wants,
119        haves,
120        opts,
121        NameTrees::No,
122        false,
123        &CancelSignal::never(),
124    )
125}
126
127/// Whether the object walk records name hashes for trees as well as blobs.
128#[derive(Clone, Copy, PartialEq, Eq)]
129pub(crate) enum NameTrees {
130    /// Blobs only: the packers that pair only blobs skip the extra entries.
131    No,
132    /// Blobs and trees, for a delta search that pairs trees too.
133    Yes,
134}
135
136impl NameTrees {
137    /// [`Self::Yes`] when a pack built with `opts` runs a new delta search, or is thin
138    /// (receiver-held bases are matched to sent trees by path).
139    pub(crate) fn for_delta_search(opts: &crate::pack_objects::PackBuildOptions) -> Self {
140        if opts.delta && (opts.thin || (opts.window > 0 && opts.max_depth > 0)) {
141            Self::Yes
142        } else {
143            Self::No
144        }
145    }
146}
147
148/// Like [`enumerate_pack_objects`], choosing whether the walk names trees and
149/// stopping with [`crate::error::Error::Cancelled`] once `stop` fires (polled for
150/// every commit and object the walk reads).
151pub(crate) fn enumerate_pack_objects_naming(
152    odb: &Odb,
153    wants: &[ObjectId],
154    haves: &[ObjectId],
155    opts: &PackEnumerateOptions<'_>,
156    name_trees: NameTrees,
157    local_only: bool,
158    stop: &CancelSignal<'_>,
159) -> Result<PackObjectSelection> {
160    stop.poll()?;
161    if !local_only && opts.use_bitmaps {
162        if let Some(sel) = try_bitmap_enumeration(odb, wants, haves, opts)? {
163            stop.poll()?;
164            return Ok(sel);
165        }
166    }
167    let alternate_sources = if local_only {
168        AlternateSources::load(odb)?.composite()
169    } else {
170        None
171    };
172    walk_enumeration(
173        odb,
174        wants,
175        haves,
176        opts,
177        name_trees,
178        alternate_sources,
179        MissingBlobMode::Fail,
180        stop,
181    )
182    .map(|(selection, _)| selection)
183}
184
185/// What the object walk does with a blob the repository does not store.
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub(crate) enum MissingBlobMode {
188    /// List it anyway; the pack writer fails when it reads it (the plain walk).
189    Fail,
190    /// Check that every blob it would list is stored, and set the missing ones
191    /// aside as [`MissingBlob`]s instead of listing them.
192    Probe,
193}
194
195/// A blob the walk would have sent but the repository does not store.
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub(crate) struct MissingBlob {
198    /// The missing blob.
199    pub(crate) oid: ObjectId,
200    /// The tree the walk found it in, when known.
201    pub(crate) referrer: Option<ObjectId>,
202}
203
204/// Like [`enumerate_pack_objects_naming`] with [`MissingBlobMode::Probe`]: the object
205/// walk always runs (bitmaps cannot see missing objects), and blobs the repository
206/// lacks come back separately instead of being listed.
207///
208/// Missing commits, trees and tags still fail the walk.
209pub(crate) fn enumerate_pack_objects_probing(
210    odb: &Odb,
211    wants: &[ObjectId],
212    haves: &[ObjectId],
213    opts: &PackEnumerateOptions<'_>,
214    name_trees: NameTrees,
215    stop: &CancelSignal<'_>,
216) -> Result<(PackObjectSelection, Vec<MissingBlob>)> {
217    stop.poll()?;
218    let opts = PackEnumerateOptions {
219        use_bitmaps: false,
220        ..*opts
221    };
222    walk_enumeration(
223        odb,
224        wants,
225        haves,
226        &opts,
227        name_trees,
228        None,
229        MissingBlobMode::Probe,
230        stop,
231    )
232}
233
234/// Bitmap reachability for upload-pack when shallow grafts and filters allow it.
235///
236/// Returns the send-side [`ReachableSet`] and the peer-held [`ReachableSet`].
237pub fn try_bitmap_reachable_for_upload(
238    odb: &Odb,
239    wants: &[ObjectId],
240    haves: &[ObjectId],
241    filter: Option<&ObjectFilter>,
242    shallow_grafts: &HashSet<ObjectId>,
243) -> Result<Option<(ReachableSet, ReachableSet)>> {
244    if !shallow_grafts.is_empty() || filter.is_some_and(selects_object_type) {
245        return Ok(None);
246    }
247    let Some(git_dir) = odb.config_git_dir() else {
248        return Ok(None);
249    };
250    let repo = match Repository::open(git_dir, &RepositoryOptions::empty()) {
251        Ok(r) => r,
252        Err(_) => return Ok(None),
253    };
254    let Some(index) = BitmapIndex::open(&repo).ok().flatten() else {
255        return Ok(None);
256    };
257    let index = Arc::new(index);
258    match index.reachability_and_haves(
259        &repo,
260        ReachabilityQuery {
261            wants,
262            haves,
263            filter,
264        },
265        MissingAction::Error,
266        MissingAction::Allow,
267    ) {
268        Ok(sets) => Ok(Some(sets)),
269        Err(BitmapWalkError::Unsupported(_)) => Ok(None),
270        Err(BitmapWalkError::Bitmap(_)) => Ok(None),
271        Err(BitmapWalkError::Repository(e)) => Err(e),
272    }
273}
274
275/// Bitmap `object:type` selection also drops the wanted commits and tags, which a
276/// pack must always carry, so those filters use the object walk.
277fn selects_object_type(filter: &ObjectFilter) -> bool {
278    match filter {
279        ObjectFilter::ObjectType(_) => true,
280        ObjectFilter::Combine(parts) => parts.iter().any(selects_object_type),
281        _ => false,
282    }
283}
284
285fn try_bitmap_enumeration(
286    odb: &Odb,
287    wants: &[ObjectId],
288    haves: &[ObjectId],
289    opts: &PackEnumerateOptions<'_>,
290) -> Result<Option<PackObjectSelection>> {
291    if opts.has_shallow_grafts() {
292        return Ok(None);
293    }
294    let Some((send_set, peer_held)) =
295        try_bitmap_reachable_for_upload(odb, wants, haves, opts.filter, opts.shallow_grafts)?
296    else {
297        return Ok(None);
298    };
299    let index = send_set.bitmap_index();
300    let send: Vec<ObjectId> = send_set.iter_grouped_by_kind().collect();
301    let have_closure: HashSet<ObjectId> = haves.iter().copied().collect();
302    let name_hashes = name_hashes_from_bitmap(index, &send, &HashSet::new());
303    Ok(Some(PackObjectSelection {
304        send,
305        have_closure,
306        peer_held: Some(Arc::new(peer_held)),
307        name_hashes,
308    }))
309}
310
311pub(crate) fn name_hashes_for_send(
312    index: &BitmapIndex,
313    send: &[ObjectId],
314    have_closure: &HashSet<ObjectId>,
315) -> HashMap<ObjectId, u32> {
316    name_hashes_from_bitmap(index, send, have_closure)
317}
318
319fn name_hashes_from_bitmap(
320    index: &BitmapIndex,
321    send: &[ObjectId],
322    have_closure: &HashSet<ObjectId>,
323) -> HashMap<ObjectId, u32> {
324    let mut out = HashMap::new();
325    for oid in send.iter().chain(have_closure.iter()) {
326        let Some(pos) = index.position_of(oid) else {
327            continue;
328        };
329        if let Some(h) = index.name_hash(pos) {
330            out.insert(*oid, h);
331        }
332    }
333    out
334}
335
336#[allow(clippy::too_many_arguments)]
337fn walk_enumeration(
338    odb: &Odb,
339    wants: &[ObjectId],
340    haves: &[ObjectId],
341    opts: &PackEnumerateOptions<'_>,
342    name_trees: NameTrees,
343    alternate_sources: Option<Arc<CompositeStore>>,
344    missing_mode: MissingBlobMode,
345    stop: &CancelSignal<'_>,
346) -> Result<(PackObjectSelection, Vec<MissingBlob>)> {
347    let have_shallow = opts.have_shallow_grafts.unwrap_or(opts.shallow_grafts);
348    let have_commits = match opts.have_boundary {
349        HaveBoundary::CommitClosure => {
350            commits_reachable_from_haves(odb, haves, have_shallow, true, stop)?
351        }
352        HaveBoundary::TipsOnly => {
353            send_pack_uninteresting_commits(odb, wants, haves, have_shallow, stop)?
354        }
355    };
356    let mut boundary: HashSet<ObjectId> = HashSet::new();
357    if let Some(extra) = opts.exclude_objects {
358        boundary.extend(extra.iter().copied());
359    }
360    let peer_held = try_peer_held_bitmap(odb, haves, opts)?;
361    stop.poll()?;
362    let mut missing = Vec::new();
363    let (send, name_hashes, edges) = match opts.filter {
364        Some(filter) if filter.uses_paths() => {
365            let (mut send, name_hashes) = rev_list_filtered_objects(
366                odb,
367                wants,
368                haves,
369                filter,
370                &have_commits,
371                missing_mode,
372                stop,
373            )?;
374            if missing_mode == MissingBlobMode::Probe {
375                missing = take_unstored_blobs(odb, &mut send)?;
376            }
377            (send, name_hashes, None)
378        }
379        filter => {
380            let walked = collect_reachable_excluding_with_name_hashes(
381                odb,
382                wants,
383                &HashSet::new(),
384                &mut boundary,
385                &have_commits,
386                opts.peer_odb,
387                alternate_sources,
388                false,
389                opts.shallow_grafts,
390                filter,
391                name_trees,
392                missing_mode,
393                stop,
394            )?;
395            missing = walked.missing;
396            (walked.send, walked.name_hashes, Some(walked.edges))
397        }
398    };
399    // Thin bases come only from the edge commits where the walk met the peer's
400    // history, as Git's `--thin` does, not from every advertised tip.
401    let have_closure = match edges {
402        Some(edges) => edges,
403        None => haves.iter().copied().collect(),
404    };
405    Ok((
406        PackObjectSelection {
407            send,
408            have_closure,
409            peer_held,
410            name_hashes,
411        },
412        missing,
413    ))
414}
415
416/// Remove the objects `send` lists that the repository does not store and return
417/// them as missing blobs. Path-filtered walks (`sparse:oid`) only ever list
418/// commits and trees the walk has read, so anything absent is a blob.
419fn take_unstored_blobs(odb: &Odb, send: &mut Vec<ObjectId>) -> Result<Vec<MissingBlob>> {
420    let present = odb.contains_many(send)?;
421    let mut missing = Vec::new();
422    let mut present = present.into_iter();
423    send.retain(|oid| {
424        let keep = present.next().unwrap_or(true);
425        if !keep {
426            missing.push(MissingBlob {
427                oid: *oid,
428                referrer: None,
429            });
430        }
431        keep
432    });
433    Ok(missing)
434}
435
436/// An annotated tag ref that `include-tag` may add to a fetch pack.
437#[derive(Clone, Copy, Debug, PartialEq, Eq)]
438pub(crate) struct TagCandidate {
439    /// The tag object the ref names.
440    pub(crate) tag: ObjectId,
441    /// The non-tag object the tag chain ends at (the advertised `^{}` value).
442    pub(crate) peeled: ObjectId,
443}
444
445/// Append to `send` every candidate tag whose peeled object `send` already holds,
446/// with each tag object of its chain, the way `pack-objects --include-tag` does.
447///
448/// Cost follows the size of `send` and the number of candidates, never the history
449/// behind them: `send` is scanned once (a second time only when a chain passes
450/// through a tag no candidate names) and only the added tags are read.
451///
452/// # Parameters
453///
454/// - `odb`: the database the added tag objects are read from.
455/// - `send`: the objects the pack holds; added tags are appended after them.
456/// - `candidates`: the annotated tag refs the client may follow.
457///
458/// # Errors
459///
460/// [`Error::ObjectNotFound`] or [`Error::CorruptObject`] for an unreadable tag in a
461/// chain that is added.
462pub(crate) fn append_included_tags(
463    odb: &Odb,
464    send: &mut Vec<ObjectId>,
465    candidates: &[TagCandidate],
466) -> Result<()> {
467    if candidates.is_empty() {
468        return Ok(());
469    }
470    let mut named = oid_hash_set_with_capacity(candidates.len() * 2);
471    for c in candidates {
472        named.insert(c.tag);
473        named.insert(c.peeled);
474    }
475    let present: OidHashSet = send
476        .iter()
477        .filter(|o| named.contains(*o))
478        .copied()
479        .collect();
480    let mut added = included_tag_chains(odb, candidates, |oid| present.contains(oid))?;
481    let unnamed: OidHashSet = added
482        .iter()
483        .filter(|o| !named.contains(*o))
484        .copied()
485        .collect();
486    if !unnamed.is_empty() {
487        let already_sent: OidHashSet = send
488            .iter()
489            .filter(|o| unnamed.contains(*o))
490            .copied()
491            .collect();
492        added.retain(|oid| !already_sent.contains(oid));
493    }
494    send.extend(added);
495    Ok(())
496}
497
498/// The tag objects `include-tag` adds for `candidates` to a pack whose membership
499/// `in_pack` answers.
500///
501/// A candidate qualifies when its peeled object is in the pack and its tag is not;
502/// its whole chain (tag of a tag) is then returned, minus objects already packed.
503/// `in_pack` must be exact for candidate tags and peeled ids; for unnamed chain
504/// tags a `false` only means the caller has to deduplicate them.
505///
506/// # Errors
507///
508/// [`Error::ObjectNotFound`] or [`Error::CorruptObject`] for an unreadable tag in a
509/// qualifying chain.
510pub(crate) fn included_tag_chains(
511    odb: &Odb,
512    candidates: &[TagCandidate],
513    in_pack: impl Fn(&ObjectId) -> bool,
514) -> Result<Vec<ObjectId>> {
515    let mut seen = oid_hash_set();
516    let mut out = Vec::new();
517    for c in candidates {
518        if !in_pack(&c.peeled) || in_pack(&c.tag) {
519            continue;
520        }
521        let mut current = c.tag;
522        while current != c.peeled && seen.insert(current) {
523            if !in_pack(&current) {
524                out.push(current);
525            }
526            let tag = odb.read(&current)?;
527            current = tag_object_line_oid(&tag.data).ok_or_else(|| {
528                Error::CorruptObject(format!("tag {current} has no valid object header"))
529            })?;
530        }
531    }
532    Ok(out)
533}
534
535const SEND_PACK_REACH_WANT: u8 = 1;
536const SEND_PACK_REACH_HAVE: u8 = 2;
537
538#[cfg(test)]
539pub(crate) mod send_pack_walk_test_hooks {
540    use std::cell::Cell;
541
542    thread_local! {
543        static COMMIT_READS: Cell<usize> = const { Cell::new(0) };
544    }
545
546    pub fn reset_commit_reads() {
547        COMMIT_READS.with(|c| c.set(0));
548    }
549
550    pub fn commit_reads() -> usize {
551        COMMIT_READS.with(|c| c.get())
552    }
553
554    pub(crate) fn record_commit_read() {
555        COMMIT_READS.with(|c| c.set(c.get().saturating_add(1)));
556    }
557}
558
559fn send_pack_reach_uninteresting(reach: u8) -> bool {
560    reach & SEND_PACK_REACH_HAVE != 0
561}
562
563fn send_pack_reach_interesting(reach: u8) -> bool {
564    reach & SEND_PACK_REACH_WANT != 0 && !send_pack_reach_uninteresting(reach)
565}
566
567/// Commits marked uninteresting for send-pack (`--revs` with `^have` lines).
568///
569/// Peels annotated tags on haves (skips missing ids and tags that peel to non-commits),
570/// seeds wants and haves on a committer-date heap, and walks parents until every queued
571/// commit is uninteresting or the heap is empty.
572fn send_pack_uninteresting_commits(
573    odb: &Odb,
574    wants: &[ObjectId],
575    haves: &[ObjectId],
576    shallow_grafts: &HashSet<ObjectId>,
577    stop: &CancelSignal<'_>,
578) -> Result<HashSet<ObjectId>> {
579    struct SendPackRevWalk<'a> {
580        odb: &'a Odb,
581        graph: Option<Arc<CommitGraphChain>>,
582        shallow_grafts: &'a HashSet<ObjectId>,
583        reach: HashMap<ObjectId, u8>,
584        heap: BinaryHeap<QueuedCommit>,
585        on_heap: HashSet<ObjectId>,
586        processed: HashSet<ObjectId>,
587        uninteresting: HashSet<ObjectId>,
588        interesting_on_heap: usize,
589        next_seq: u64,
590    }
591
592    impl<'a> SendPackRevWalk<'a> {
593        fn note_reach(&mut self, oid: ObjectId, flag: u8) {
594            let entry = self.reach.entry(oid).or_insert(0);
595            let was_interesting = send_pack_reach_interesting(*entry);
596            *entry |= flag;
597            let is_interesting = send_pack_reach_interesting(*entry);
598            if self.on_heap.contains(&oid) {
599                match (was_interesting, is_interesting) {
600                    (false, true) => self.interesting_on_heap += 1,
601                    (true, false) => self.interesting_on_heap -= 1,
602                    _ => {}
603                }
604            }
605            if send_pack_reach_uninteresting(*entry) {
606                self.uninteresting.insert(oid);
607            }
608        }
609
610        /// Stop once no queued commit is still want-only (the interesting frontier).
611        ///
612        /// Unlike waiting for every uninteresting commit's parents to be marked, this
613        /// matches Git's `still_interesting` cutoff and avoids walking all haves to root.
614        fn can_stop_after(&self) -> bool {
615            // Counting reach transitions avoids revisiting every queued have after each
616            // commit, especially when a new commit is dated before advertised history.
617            !self.heap.is_empty() && self.interesting_on_heap == 0
618        }
619
620        /// Note `oid` as reached with `flag`; true when it still has to be read and queued.
621        fn reach_unqueued(&mut self, oid: ObjectId, flag: u8) -> bool {
622            self.note_reach(oid, flag);
623            !(self.processed.contains(&oid)
624                || self.on_heap.contains(&oid)
625                || self.shallow_grafts.contains(&oid))
626        }
627
628        fn try_enqueue(&mut self, oid: ObjectId, flag: u8) -> Result<()> {
629            if !self.reach_unqueued(oid, flag) {
630                return Ok(());
631            }
632            if self.queue_from_graph(oid) {
633                return Ok(());
634            }
635            match self.odb.read(&oid) {
636                Ok(obj) => self.queue_commit(oid, &obj),
637                Err(_) => Ok(()),
638            }
639        }
640
641        /// Queue the commit a ref tip names, following annotated tags.
642        ///
643        /// Each object of the chain is read once; missing objects and tags that end at a
644        /// tree or blob are skipped.
645        fn enqueue_tip(&mut self, tip: ObjectId, flag: u8) -> Result<()> {
646            let mut oid = tip;
647            let mut tags_seen: Option<HashSet<ObjectId>> = None;
648            loop {
649                if self.graph_lists(&oid) {
650                    if self.reach_unqueued(oid, flag) {
651                        self.queue_from_graph(oid);
652                    }
653                    return Ok(());
654                }
655                // A want is often a blob (a lazy fetch names thousands): the header
656                // says so without inflating it.
657                match self.odb.read_info(&oid) {
658                    Ok(info) if matches!(info.kind, ObjectKind::Commit | ObjectKind::Tag) => {}
659                    _ => return Ok(()),
660                }
661                let Ok(obj) = self.odb.read(&oid) else {
662                    return Ok(());
663                };
664                if obj.kind != ObjectKind::Tag {
665                    if obj.kind != ObjectKind::Commit || !self.reach_unqueued(oid, flag) {
666                        return Ok(());
667                    }
668                    return self.queue_commit(oid, &obj);
669                }
670                if !tags_seen.get_or_insert_with(HashSet::new).insert(oid) {
671                    return Ok(());
672                }
673                let Some(target) = tag_object_line_oid(&obj.data) else {
674                    return Ok(());
675                };
676                if self.processed.contains(&target) || self.on_heap.contains(&target) {
677                    self.note_reach(target, flag);
678                    return Ok(());
679                }
680                oid = target;
681            }
682        }
683
684        fn graph_lists(&self, oid: &ObjectId) -> bool {
685            self.graph
686                .as_deref()
687                .is_some_and(|g| g.find_commit(oid).is_some())
688        }
689
690        /// Queue `oid` from the commit-graph without reading the object; false when the
691        /// graph does not list it.
692        fn queue_from_graph(&mut self, oid: ObjectId) -> bool {
693            let Some(entry) = self.graph.as_deref().and_then(|g| g.graph_walk_entry(&oid)) else {
694                return false;
695            };
696            self.push_queued(QueuedCommit {
697                generation: entry.generation,
698                time: entry.time,
699                seq: std::cmp::Reverse(self.next_seq),
700                oid,
701                tree: entry.tree,
702                parents: entry.parents,
703            });
704            true
705        }
706
707        fn queue_commit(&mut self, oid: ObjectId, obj: &Object) -> Result<()> {
708            if obj.kind != ObjectKind::Commit {
709                return Ok(());
710            }
711            let parsed = CommitRef::parse(&obj.data)?;
712            #[cfg(test)]
713            send_pack_walk_test_hooks::record_commit_read();
714            self.push_queued(QueuedCommit {
715                generation: UNRANKED_GENERATION,
716                time: parsed.committer_unix_seconds_for_ordering(),
717                seq: std::cmp::Reverse(self.next_seq),
718                oid,
719                tree: parsed.tree(),
720                parents: parsed.parents().to_vec(),
721            });
722            Ok(())
723        }
724
725        fn push_queued(&mut self, queued: QueuedCommit) {
726            self.on_heap.insert(queued.oid);
727            if send_pack_reach_interesting(*self.reach.get(&queued.oid).unwrap_or(&0)) {
728                self.interesting_on_heap += 1;
729            }
730            self.heap.push(queued);
731            self.next_seq += 1;
732        }
733    }
734
735    // Commits the graph lists come from it without inflating the object, and are ordered by
736    // generation: commits outside the graph (new local work) are expanded first, so a push whose
737    // commits are dated before the remote's history does not drain that whole history before
738    // reaching them. Shallow repositories keep reading commits, since the graph does not know
739    // their grafted boundaries.
740    let graph = if shallow_grafts.is_empty() {
741        odb.commit_graph_chain()
742    } else {
743        None
744    };
745    let mut walk = SendPackRevWalk {
746        odb,
747        graph,
748        shallow_grafts,
749        reach: HashMap::new(),
750        heap: BinaryHeap::new(),
751        on_heap: HashSet::new(),
752        processed: HashSet::new(),
753        uninteresting: HashSet::new(),
754        interesting_on_heap: 0,
755        next_seq: 0,
756    };
757
758    for &want in wants {
759        walk.enqueue_tip(want, SEND_PACK_REACH_WANT)?;
760    }
761    for &have in haves {
762        walk.enqueue_tip(have, SEND_PACK_REACH_HAVE)?;
763    }
764
765    while let Some(commit) = walk.heap.pop() {
766        stop.poll()?;
767        walk.on_heap.remove(&commit.oid);
768        if send_pack_reach_interesting(*walk.reach.get(&commit.oid).unwrap_or(&0)) {
769            walk.interesting_on_heap -= 1;
770        }
771        if !walk.processed.insert(commit.oid) {
772            continue;
773        }
774        let flags = *walk.reach.get(&commit.oid).unwrap_or(&0);
775        if send_pack_reach_uninteresting(flags) {
776            walk.uninteresting.insert(commit.oid);
777        }
778        let parent_flag = if send_pack_reach_uninteresting(flags) {
779            SEND_PACK_REACH_HAVE
780        } else {
781            SEND_PACK_REACH_WANT
782        };
783        if !walk.shallow_grafts.contains(&commit.oid) {
784            for parent in commit.parents {
785                walk.try_enqueue(parent, parent_flag)?;
786            }
787        }
788        if walk.can_stop_after() {
789            break;
790        }
791    }
792    Ok(walk.uninteresting)
793}
794
795/// Commits reachable from client `haves` (commit graph only, no trees).
796fn commits_reachable_from_haves(
797    odb: &Odb,
798    haves: &[ObjectId],
799    shallow_grafts: &HashSet<ObjectId>,
800    skip_missing: bool,
801    stop: &CancelSignal<'_>,
802) -> Result<HashSet<ObjectId>> {
803    let mut seen = HashSet::new();
804    let mut queue: VecDeque<ObjectId> = haves.iter().copied().collect();
805    while let Some(oid) = queue.pop_front() {
806        stop.poll()?;
807        if !seen.insert(oid) {
808            continue;
809        }
810        if shallow_grafts.contains(&oid) {
811            continue;
812        }
813        let obj = match odb.read(&oid) {
814            Ok(o) => o,
815            Err(_) if skip_missing => continue,
816            Err(e) => return Err(e),
817        };
818        if obj.kind != ObjectKind::Commit {
819            continue;
820        }
821        let parsed = CommitRef::parse(&obj.data)?;
822        for parent in parsed.parents() {
823            if !seen.contains(parent) {
824                queue.push_back(*parent);
825            }
826        }
827    }
828    Ok(seen)
829}
830
831/// Objects in a commit's root tree (the commit, its trees and blobs), for negotiation boundaries.
832fn objects_in_commit_snapshot(odb: &Odb, commit_oid: ObjectId) -> Result<HashSet<ObjectId>> {
833    let mut out = HashSet::new();
834    out.insert(commit_oid);
835    let commit_obj = match odb.read(&commit_oid) {
836        Ok(o) => o,
837        Err(_) => return Ok(out),
838    };
839    if commit_obj.kind != ObjectKind::Commit {
840        return Ok(out);
841    }
842    let commit = CommitRef::parse(&commit_obj.data)?;
843    let oid_len = odb.hash_algo().len();
844    let mut queue = VecDeque::from([commit.tree()]);
845    while let Some(tree_oid) = queue.pop_front() {
846        if !out.insert(tree_oid) {
847            continue;
848        }
849        let tree_obj = match odb.read(&tree_oid) {
850            Ok(o) => o,
851            Err(_) => continue,
852        };
853        for entry in TreeRefIter::with_oid_len(&tree_obj.data, oid_len) {
854            let entry = entry?;
855            if entry.mode == 0o160000 {
856                continue;
857            }
858            if entry.mode & 0o170000 == 0o040000 {
859                queue.push_back(entry.oid);
860            } else {
861                out.insert(entry.oid);
862            }
863        }
864    }
865    Ok(out)
866}
867
868fn try_peer_held_bitmap(
869    odb: &Odb,
870    haves: &[ObjectId],
871    opts: &PackEnumerateOptions<'_>,
872) -> Result<Option<Arc<ReachableSet>>> {
873    if haves.is_empty() || !opts.use_bitmaps || opts.has_shallow_grafts() {
874        return Ok(None);
875    }
876    let Some(peer) = try_bitmap_peer_held(odb, haves, opts.shallow_grafts)? else {
877        return Ok(None);
878    };
879    Ok(Some(Arc::new(peer)))
880}
881
882fn try_bitmap_peer_held(
883    odb: &Odb,
884    haves: &[ObjectId],
885    shallow_grafts: &HashSet<ObjectId>,
886) -> Result<Option<ReachableSet>> {
887    if !shallow_grafts.is_empty() {
888        return Ok(None);
889    }
890    let Some(git_dir) = odb.config_git_dir() else {
891        return Ok(None);
892    };
893    let repo = match Repository::open(git_dir, &RepositoryOptions::empty()) {
894        Ok(r) => r,
895        Err(_) => return Ok(None),
896    };
897    let Some(index) = BitmapIndex::open(&repo).ok().flatten() else {
898        return Ok(None);
899    };
900    let index = Arc::new(index);
901    match index.reachability(
902        &repo,
903        ReachabilityQuery {
904            wants: haves,
905            haves: &[],
906            filter: None,
907        },
908        MissingAction::Allow,
909    ) {
910        Ok(set) => Ok(Some(set)),
911        Err(BitmapWalkError::Unsupported(_)) => Ok(None),
912        Err(BitmapWalkError::Bitmap(_)) => Ok(None),
913        Err(BitmapWalkError::Repository(e)) => Err(e),
914    }
915}
916
917/// Objects for a path-based filter (`sparse:oid`), which needs the full object walk.
918fn rev_list_filtered_objects(
919    odb: &Odb,
920    wants: &[ObjectId],
921    haves: &[ObjectId],
922    filter: &ObjectFilter,
923    have_commits: &HashSet<ObjectId>,
924    missing_mode: MissingBlobMode,
925    stop: &CancelSignal<'_>,
926) -> Result<(Vec<ObjectId>, HashMap<ObjectId, u32>)> {
927    let _ = have_commits;
928    stop.poll()?;
929    let Some(git_dir) = odb.config_git_dir() else {
930        return Ok((Vec::new(), HashMap::new()));
931    };
932    let repo = Repository::open(git_dir, &RepositoryOptions::empty())?;
933    let wants: Vec<String> = wants.iter().map(ObjectId::to_hex).collect();
934    let haves: Vec<String> = haves.iter().map(ObjectId::to_hex).collect();
935    let opts = crate::rev_list::RevListOptions {
936        objects: true,
937        no_object_names: false,
938        quiet: true,
939        filter: Some(filter.clone()),
940        cancel_watch: Some(stop.watch_arc()),
941        missing_action: match missing_mode {
942            MissingBlobMode::Fail => MissingAction::Error,
943            MissingBlobMode::Probe => MissingAction::Allow,
944        },
945        ..Default::default()
946    };
947    let result = crate::rev_list::rev_list(&repo, &wants, &haves, &opts)?;
948    stop.poll()?;
949    let mut ordered = Vec::new();
950    let mut seen = HashSet::new();
951    for oid in result
952        .commits
953        .iter()
954        .chain(result.objects.iter().map(|(o, _)| o))
955    {
956        if seen.insert(*oid) {
957            ordered.push(*oid);
958        }
959    }
960    let mut name_hashes = HashMap::new();
961    for (oid, path) in &result.objects {
962        if !path.is_empty() {
963            name_hashes
964                .entry(*oid)
965                .or_insert_with(|| pack_name_hash(path));
966        }
967    }
968    Ok((ordered, name_hashes))
969}
970
971/// Walk objects reachable from `roots`, with the path name hash of every blob
972/// (and, with [`NameTrees::Yes`], every tree) below a root tree, and the
973/// `have_commits` the walk stopped at (its edge commits).
974///
975/// Commits are walked newest committer date first (the order Git's revision walk
976/// uses), all of them before any tree is expanded, so the versions of one path are
977/// listed newest first: delta search breaks size ties by that order. Tree and blob
978/// roots (index entries, cache-tree nodes) carry no path and are walked last, so
979/// objects reachable from history keep the path they have there.
980///
981/// `roots` and the objects tags point at are always listed. With `filter`, parent
982/// commits, trees and blobs are listed only when they pass it, the way
983/// `pack-objects --filter` selects them: a tree's depth counts from its commit's root
984/// tree (`0`), and a blob is checked against the depth of the tree holding it. As in
985/// Git, a root or tag target that is also some commit's root tree counts as that root
986/// tree. Trees whose entries cannot pass are not read, and blob sizes are only looked
987/// up for `blob:limit`.
988///
989/// With [`MissingBlobMode::Probe`], blobs the repository does not store are not
990/// listed but returned in [`WalkedObjects::missing`].
991#[allow(clippy::too_many_arguments)]
992fn collect_reachable_excluding_with_name_hashes(
993    odb: &Odb,
994    roots: &[ObjectId],
995    exclude: &HashSet<ObjectId>,
996    boundary: &mut HashSet<ObjectId>,
997    have_commits: &HashSet<ObjectId>,
998    peer_odb: Option<&Odb>,
999    alternate_sources: Option<Arc<CompositeStore>>,
1000    skip_missing: bool,
1001    shallow_grafts: &HashSet<ObjectId>,
1002    filter: Option<&ObjectFilter>,
1003    name_trees: NameTrees,
1004    missing_mode: MissingBlobMode,
1005    stop: &CancelSignal<'_>,
1006) -> Result<WalkedObjects> {
1007    let oid_len = odb.hash_algo().len();
1008    let mut walk = EnumerationWalk {
1009        odb,
1010        exclude,
1011        boundary,
1012        have_commits,
1013        peer_odb,
1014        alternate_sources,
1015        skip_missing,
1016        filter,
1017        depth_sensitive: filter.is_some_and(ObjectFilter::is_depth_sensitive),
1018        name_trees,
1019        visited: oid_hash_set(),
1020        tree_depths: oid_hash_map(),
1021        unlisted: oid_hash_set(),
1022        ordered: Vec::new(),
1023        commits: BinaryHeap::new(),
1024        next_seq: 0,
1025        queue: VecDeque::new(),
1026        name_hashes: HashMap::new(),
1027        edges: HashSet::new(),
1028        missing: (missing_mode == MissingBlobMode::Probe).then(Vec::new),
1029    };
1030    // Commit root trees the filter drops, even where a root or tag names them.
1031    let mut dropped_root_trees: Option<OidHashSet> = filter
1032        .filter(|f| !f.includes_tree(0))
1033        .map(|_| oid_hash_set());
1034
1035    let mut path_less_roots = Vec::new();
1036    for &root in roots {
1037        match odb.read_info(&root) {
1038            Ok(info) if matches!(info.kind, ObjectKind::Tree | ObjectKind::Blob) => {
1039                path_less_roots.push(root);
1040            }
1041            _ => walk.push_root(root)?,
1042        }
1043    }
1044    let mut path_less_roots = path_less_roots.into_iter();
1045
1046    loop {
1047        stop.poll()?;
1048        if let Some(commit) = walk.commits.pop() {
1049            if !shallow_grafts.contains(&commit.oid) {
1050                for parent in commit.parents {
1051                    walk.push_parent(parent)?;
1052                }
1053            }
1054            if let Some(dropped) = dropped_root_trees.as_mut() {
1055                dropped.insert(commit.tree);
1056            }
1057            walk.push_tree(commit.tree, String::new(), 0);
1058            continue;
1059        }
1060        let Some((oid, path, depth)) = walk.queue.pop_front() else {
1061            match path_less_roots.next() {
1062                Some(root) => {
1063                    walk.push_object(root, String::new());
1064                    continue;
1065                }
1066                None => break,
1067            }
1068        };
1069        // Queued blobs are wanted directly (a lazy fetch names thousands); the
1070        // header says there is nothing to walk without inflating them.
1071        match odb.read_info(&oid) {
1072            Ok(info) if info.kind == ObjectKind::Blob => continue,
1073            Ok(_) => {}
1074            Err(_) if skip_missing => continue,
1075            Err(e) => return Err(e),
1076        }
1077        let obj = match odb.read(&oid) {
1078            Ok(o) => o,
1079            Err(_) if skip_missing => continue,
1080            Err(e) => return Err(e),
1081        };
1082        match obj.kind {
1083            ObjectKind::Commit => {
1084                walk.queue_commit(oid, obj)?;
1085                continue;
1086            }
1087            ObjectKind::Tree => {
1088                for entry in TreeRefIter::with_oid_len(&obj.data, oid_len) {
1089                    let entry = entry?;
1090                    if entry.mode == 0o160000 {
1091                        continue;
1092                    }
1093                    // Regular files and symlinks have no children: record them
1094                    // without inflating their contents. The pack writer reads (or
1095                    // copies verbatim) each one later and fails there if it is
1096                    // missing. Any other mode, including directories written with
1097                    // non-canonical permission bits, is read so the object's own
1098                    // kind decides whether it is walked.
1099                    match entry.mode & 0o170000 {
1100                        0o120000 => walk.push_blob(entry.oid, None, oid, depth)?,
1101                        0o100000 => {
1102                            walk.push_blob(entry.oid, Some((&path, entry.name)), oid, depth)?;
1103                        }
1104                        file_type => {
1105                            let child_path = child_path(&path, entry.name);
1106                            if file_type == 0o040000 {
1107                                walk.push_tree(entry.oid, child_path, depth + 1);
1108                            } else {
1109                                walk.push_object(entry.oid, child_path);
1110                            }
1111                        }
1112                    }
1113                }
1114            }
1115            ObjectKind::Tag => {
1116                let tag = parse_tag(&obj.data)?;
1117                walk.push_object(tag.object, path);
1118            }
1119            ObjectKind::Blob => {}
1120        }
1121    }
1122
1123    if let Some(dropped) = dropped_root_trees.filter(|d| !d.is_empty()) {
1124        walk.ordered.retain(|oid| !dropped.contains(oid));
1125    }
1126    let mut send = walk.ordered;
1127    if let Some(peer) = peer_odb {
1128        if !peer.uses_files_primary() && !peer.custom_primary_is_empty()? {
1129            send = peer.missing_materialized_many(&send)?;
1130        }
1131    }
1132    Ok(WalkedObjects {
1133        send,
1134        name_hashes: walk.name_hashes,
1135        edges: walk.edges,
1136        missing: walk.missing.unwrap_or_default(),
1137    })
1138}
1139
1140/// Result of [`collect_reachable_excluding_with_name_hashes`].
1141struct WalkedObjects {
1142    /// Objects to send, in pack order.
1143    send: Vec<ObjectId>,
1144    /// Pack name hash of each named object.
1145    name_hashes: HashMap<ObjectId, u32>,
1146    /// Have-side commits the walk stopped at.
1147    edges: HashSet<ObjectId>,
1148    /// Blobs left out because they are not stored ([`MissingBlobMode::Probe`] only).
1149    missing: Vec<MissingBlob>,
1150}
1151
1152fn child_path(parent: &str, name: &[u8]) -> String {
1153    let name = String::from_utf8_lossy(name);
1154    if parent.is_empty() {
1155        name.into_owned()
1156    } else {
1157        format!("{parent}/{name}")
1158    }
1159}
1160
1161/// State of [`collect_reachable_excluding_with_name_hashes`].
1162struct EnumerationWalk<'a> {
1163    odb: &'a Odb,
1164    exclude: &'a HashSet<ObjectId>,
1165    /// Negotiation boundaries: do not list or expand past these objects.
1166    boundary: &'a mut HashSet<ObjectId>,
1167    /// Uninteresting commits (send-pack) or full have closure (upload-pack).
1168    have_commits: &'a HashSet<ObjectId>,
1169    /// Objects already on the peer (local fetch): stop without listing.
1170    peer_odb: Option<&'a Odb>,
1171    /// Alternate object directories (`repack -l` / `pack-objects --local`).
1172    alternate_sources: Option<Arc<CompositeStore>>,
1173    skip_missing: bool,
1174    filter: Option<&'a ObjectFilter>,
1175    /// The filter's verdict for trees and blobs depends on where they are met.
1176    depth_sensitive: bool,
1177    name_trees: NameTrees,
1178    /// Objects listed (or held by the peer), and objects the filter left out whose
1179    /// verdict cannot change: parent commits, and without a depth-sensitive filter,
1180    /// trees and size-checked blobs.
1181    visited: OidHashSet,
1182    /// Shallowest depth each tree was walked at (depth-sensitive filters only).
1183    tree_depths: OidHashMap<u64>,
1184    /// Objects in [`Self::visited`] that the filter left out. A root or tag target
1185    /// naming one of them still lists it.
1186    unlisted: OidHashSet,
1187    ordered: Vec<ObjectId>,
1188    /// Commits read but not yet expanded, newest first.
1189    commits: BinaryHeap<QueuedCommit>,
1190    next_seq: u64,
1191    /// Objects to read: id, path for name hashes, and tree depth. Drained only
1192    /// while [`Self::commits`] is empty.
1193    queue: VecDeque<(ObjectId, String, u64)>,
1194    name_hashes: HashMap<ObjectId, u32>,
1195    /// Have-side commits the walk stopped at (Git's edge commits).
1196    edges: HashSet<ObjectId>,
1197    /// With [`MissingBlobMode::Probe`], blobs that would be listed but are not stored.
1198    missing: Option<Vec<MissingBlob>>,
1199}
1200
1201/// A commit waiting in [`EnumerationWalk::commits`], ordered newest first and,
1202/// among equal dates, in the order it was found.
1203struct QueuedCommit {
1204    /// Commit-graph generation; [`UNRANKED_GENERATION`] outside the graph, so those sort first.
1205    generation: u64,
1206    time: i64,
1207    seq: std::cmp::Reverse<u64>,
1208    oid: ObjectId,
1209    tree: ObjectId,
1210    parents: Vec<ObjectId>,
1211}
1212
1213impl PartialEq for QueuedCommit {
1214    fn eq(&self, other: &Self) -> bool {
1215        (self.generation, self.time, self.seq) == (other.generation, other.time, other.seq)
1216    }
1217}
1218
1219impl Eq for QueuedCommit {}
1220
1221impl PartialOrd for QueuedCommit {
1222    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1223        Some(self.cmp(other))
1224    }
1225}
1226
1227impl Ord for QueuedCommit {
1228    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1229        (self.generation, self.time, self.seq).cmp(&(other.generation, other.time, other.seq))
1230    }
1231}
1232
1233impl<'a> EnumerationWalk<'a> {
1234    fn list(&mut self, oid: ObjectId) {
1235        if !self.exclude.contains(&oid) {
1236            self.ordered.push(oid);
1237        }
1238    }
1239
1240    fn note_boundary(&mut self, oid: ObjectId) {
1241        self.visited.insert(oid);
1242    }
1243
1244    fn at_object_boundary(&self, oid: ObjectId) -> bool {
1245        if self
1246            .alternate_sources
1247            .as_ref()
1248            .is_some_and(|s| s.contains(&oid).unwrap_or(false))
1249        {
1250            return true;
1251        }
1252        if self.boundary.contains(&oid) {
1253            return true;
1254        }
1255        self.peer_odb.is_some_and(|peer| {
1256            peer.uses_files_primary() && peer.exists_materialized_local_without_rescan(&oid)
1257        })
1258    }
1259
1260    fn seal_have_reachable_commit(&mut self, oid: ObjectId) -> Result<()> {
1261        if !self.visited.insert(oid) {
1262            return Ok(());
1263        }
1264        self.edges.insert(oid);
1265        self.boundary
1266            .extend(objects_in_commit_snapshot(self.odb, oid)?);
1267        Ok(())
1268    }
1269
1270    fn commit_on_have_side(&mut self, oid: ObjectId) -> Result<bool> {
1271        Ok(self.have_commits.contains(&oid))
1272    }
1273
1274    /// An object that is sent whatever the filter says: a root or a tag target.
1275    fn push_object(&mut self, oid: ObjectId, path: String) {
1276        if let Ok(true) = self.commit_on_have_side(oid) {
1277            let _ = self.seal_have_reachable_commit(oid);
1278            return;
1279        }
1280        if self.at_object_boundary(oid) {
1281            self.note_boundary(oid);
1282            return;
1283        }
1284        if self.visited.insert(oid) {
1285            self.list(oid);
1286            self.queue.push_back((oid, path, 0));
1287        } else if self.unlisted.remove(&oid) {
1288            self.list(oid);
1289        }
1290    }
1291
1292    /// A root or tag target that may be a commit: read now so a commit enters the
1293    /// date-ordered heap at once; anything else waits in [`Self::queue`].
1294    fn push_root(&mut self, oid: ObjectId) -> Result<()> {
1295        if self.commit_on_have_side(oid)? {
1296            return self.seal_have_reachable_commit(oid);
1297        }
1298        if self.at_object_boundary(oid) {
1299            self.note_boundary(oid);
1300            return Ok(());
1301        }
1302        if !self.visited.insert(oid) {
1303            if self.unlisted.remove(&oid) {
1304                self.list(oid);
1305            }
1306            return Ok(());
1307        }
1308        self.list(oid);
1309        // Only a commit is read now; anything else (a blob want, say) is read, if at
1310        // all, when the queue reaches it.
1311        match self.odb.read_info(&oid) {
1312            Ok(info) if info.kind != ObjectKind::Commit => {
1313                self.queue.push_back((oid, String::new(), 0));
1314                return Ok(());
1315            }
1316            Ok(_) => {}
1317            Err(_) if self.skip_missing => return Ok(()),
1318            Err(e) => return Err(e),
1319        }
1320        match self.read(&oid)? {
1321            Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1322            Some(_) => {
1323                self.queue.push_back((oid, String::new(), 0));
1324                Ok(())
1325            }
1326            None => Ok(()),
1327        }
1328    }
1329
1330    /// A parent commit: always walked, listed unless the filter keeps other types only.
1331    /// Read now so it enters the date-ordered heap before any tree is expanded.
1332    fn push_parent(&mut self, oid: ObjectId) -> Result<()> {
1333        if self.commit_on_have_side(oid)? {
1334            return self.seal_have_reachable_commit(oid);
1335        }
1336        if self.at_object_boundary(oid) {
1337            self.note_boundary(oid);
1338            return Ok(());
1339        }
1340        let listed = self
1341            .filter
1342            .is_none_or(|f| f.includes_commit_or_tag_object(ObjectKind::Commit));
1343        if !self.visited.insert(oid) {
1344            if listed && self.unlisted.remove(&oid) {
1345                self.list(oid);
1346            }
1347            return Ok(());
1348        }
1349        if listed {
1350            self.list(oid);
1351        } else {
1352            self.unlisted.insert(oid);
1353        }
1354        match self.read(&oid)? {
1355            Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1356            Some(_) => {
1357                self.queue.push_back((oid, String::new(), 0));
1358                Ok(())
1359            }
1360            None => Ok(()),
1361        }
1362    }
1363
1364    fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
1365        match self.odb.read(oid) {
1366            Ok(obj) => Ok(Some(obj)),
1367            Err(_) if self.skip_missing => Ok(None),
1368            Err(e) => Err(e),
1369        }
1370    }
1371
1372    /// Hold a commit read from [`Self::queue`] until it is the newest one left.
1373    fn queue_commit(&mut self, oid: ObjectId, obj: Object) -> Result<()> {
1374        let parsed = CommitRef::parse(&obj.data)?;
1375        let queued = QueuedCommit {
1376            generation: UNRANKED_GENERATION,
1377            time: parsed.committer_unix_seconds_for_ordering(),
1378            seq: std::cmp::Reverse(self.next_seq),
1379            oid,
1380            tree: parsed.tree(),
1381            parents: parsed.parents().to_vec(),
1382        };
1383        self.next_seq += 1;
1384        self.commits.push(queued);
1385        Ok(())
1386    }
1387
1388    /// A commit's root tree (`depth` 0) or a subtree found in a tree.
1389    fn push_tree(&mut self, oid: ObjectId, path: String, depth: u64) {
1390        if self.name_trees == NameTrees::Yes && !path.is_empty() && !self.visited.contains(&oid) {
1391            self.name_hashes
1392                .entry(oid)
1393                .or_insert_with(|| pack_name_hash(&path));
1394        }
1395        let Some(filter) = self.filter else {
1396            self.push_object(oid, path);
1397            return;
1398        };
1399        // A tree in a have commit's snapshot is already on the peer, whatever the filter.
1400        if self.at_object_boundary(oid) {
1401            self.note_boundary(oid);
1402            return;
1403        }
1404        let first_walk = if self.depth_sensitive {
1405            match self.tree_depths.get(&oid) {
1406                Some(&walked_at) if walked_at <= depth => false,
1407                _ => {
1408                    self.tree_depths.insert(oid, depth);
1409                    true
1410                }
1411            }
1412        } else {
1413            self.visited.insert(oid)
1414        };
1415        if !first_walk {
1416            return;
1417        }
1418        if filter.includes_tree(depth) {
1419            if !self.depth_sensitive || self.visited.insert(oid) {
1420                self.list(oid);
1421            }
1422        } else if !self.depth_sensitive {
1423            self.unlisted.insert(oid);
1424        }
1425        if !filter.prunes_tree_entries(depth) {
1426            self.queue.push_back((oid, path, depth));
1427        }
1428    }
1429
1430    /// A blob in a tree at `tree_depth`; `name` is `(tree path, entry name)` for regular
1431    /// files, which get a pack name hash.
1432    fn push_blob(
1433        &mut self,
1434        oid: ObjectId,
1435        name: Option<(&str, &[u8])>,
1436        referrer: ObjectId,
1437        tree_depth: u64,
1438    ) -> Result<()> {
1439        if self.at_object_boundary(oid) {
1440            self.note_boundary(oid);
1441            return Ok(());
1442        }
1443        if self.visited.contains(&oid) {
1444            return Ok(());
1445        }
1446        let (passes, size_checked) = match self.filter {
1447            None => (true, false),
1448            // Missing (probe mode only): as in Git's filters, an object whose size
1449            // cannot be read is not filtered out but reported missing.
1450            Some(filter) => self
1451                .blob_passes(filter, &oid, tree_depth)?
1452                .unwrap_or((true, true)),
1453        };
1454        if passes && self.missing.is_some() && !self.odb.contains(&oid)? {
1455            self.visited.insert(oid);
1456            if let Some(missing) = self.missing.as_mut() {
1457                missing.push(MissingBlob {
1458                    oid,
1459                    referrer: Some(referrer),
1460                });
1461            }
1462            return Ok(());
1463        }
1464        if passes {
1465            self.visited.insert(oid);
1466            self.list(oid);
1467            if let Some((parent, entry_name)) = name {
1468                self.name_hashes
1469                    .insert(oid, pack_name_hash(&child_path(parent, entry_name)));
1470            }
1471        } else if size_checked && !self.depth_sensitive {
1472            self.visited.insert(oid);
1473            self.unlisted.insert(oid);
1474        }
1475        Ok(())
1476    }
1477
1478    /// Whether the blob passes `filter`, and whether its size had to be looked up.
1479    ///
1480    /// `None` when the size was needed but the blob is not stored, which only
1481    /// happens with [`MissingBlobMode::Probe`] (the size is read without a lazy
1482    /// fetch there); otherwise a missing blob is an error, or with `skip_missing`
1483    /// filtered out.
1484    fn blob_passes(
1485        &self,
1486        filter: &ObjectFilter,
1487        oid: &ObjectId,
1488        tree_depth: u64,
1489    ) -> Result<Option<(bool, bool)>> {
1490        // A smaller blob never fares worse, so the size only matters between the extremes.
1491        if !filter.includes_blob_under_tree(0, tree_depth) {
1492            return Ok(Some((false, false)));
1493        }
1494        if filter.includes_blob_under_tree(u64::MAX, tree_depth) {
1495            return Ok(Some((true, false)));
1496        }
1497        let info = if self.missing.is_some() {
1498            match self.odb.read_info_without_fetch(oid) {
1499                Err(crate::error::Error::ObjectNotFound(_)) => return Ok(None),
1500                other => other,
1501            }
1502        } else {
1503            self.odb.read_info(oid)
1504        };
1505        match info {
1506            Ok(info) => Ok(Some((
1507                filter.includes_blob_under_tree(info.size, tree_depth),
1508                true,
1509            ))),
1510            Err(_) if self.skip_missing => Ok(Some((false, true))),
1511            Err(e) => Err(e),
1512        }
1513    }
1514}
1515
1516#[cfg(test)]
1517mod tests {
1518    #![allow(clippy::unwrap_used, clippy::expect_used)]
1519
1520    use super::*;
1521
1522    use crate::repo::{InitOptions, Repository};
1523    use tempfile::TempDir;
1524
1525    fn walk(repo: &Repository, roots: &[ObjectId], name_trees: NameTrees) -> PackObjectSelection {
1526        let empty = HashSet::new();
1527        enumerate_pack_objects_naming(
1528            repo.odb(),
1529            roots,
1530            &[],
1531            &PackEnumerateOptions {
1532                filter: None,
1533                shallow_grafts: &empty,
1534                have_shallow_grafts: Some(&empty),
1535                use_bitmaps: false,
1536                exclude_objects: None,
1537                peer_odb: None,
1538                have_boundary: HaveBoundary::CommitClosure,
1539            },
1540            name_trees,
1541            false,
1542            &CancelSignal::never(),
1543        )
1544        .expect("walk")
1545    }
1546
1547    fn write_tree(repo: &Repository, entries: &[(u32, &str, ObjectId)]) -> ObjectId {
1548        let entries: Vec<crate::objects::TreeEntry> = entries
1549            .iter()
1550            .map(|&(mode, name, oid)| crate::objects::TreeEntry {
1551                mode,
1552                name: name.as_bytes().to_vec(),
1553                oid,
1554            })
1555            .collect();
1556        repo.odb()
1557            .write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
1558            .expect("tree")
1559    }
1560
1561    fn write_commit(
1562        repo: &Repository,
1563        tree: ObjectId,
1564        parents: &[ObjectId],
1565        time: i64,
1566    ) -> ObjectId {
1567        let mut body = format!("tree {}\n", tree.to_hex());
1568        for parent in parents {
1569            body.push_str(&format!("parent {}\n", parent.to_hex()));
1570        }
1571        body.push_str(&format!(
1572            "author A <a@example.com> {time} +0000\ncommitter A <a@example.com> {time} +0000\n\nc{time}\n"
1573        ));
1574        repo.odb()
1575            .write(ObjectKind::Commit, body.as_bytes())
1576            .expect("commit")
1577    }
1578
1579    #[test]
1580    fn tips_only_boundary_matches_closure_on_linear_incremental_push() {
1581        let dir = TempDir::new().unwrap();
1582        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1583        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1584        let mut parent = write_commit(
1585            &repo,
1586            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1587            &[],
1588            0,
1589        );
1590        for i in 1..800 {
1591            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1592            parent = write_commit(&repo, tree, &[parent], i);
1593        }
1594        let remote_tip = parent;
1595        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1596        let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1597        let empty = HashSet::new();
1598        let closure_opts = PackEnumerateOptions {
1599            filter: None,
1600            shallow_grafts: &empty,
1601            have_shallow_grafts: Some(&empty),
1602            use_bitmaps: false,
1603            exclude_objects: None,
1604            peer_odb: None,
1605            have_boundary: HaveBoundary::CommitClosure,
1606        };
1607        let tips_opts = PackEnumerateOptions {
1608            have_boundary: HaveBoundary::TipsOnly,
1609            ..closure_opts
1610        };
1611        let closure = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &closure_opts)
1612            .expect("closure");
1613        let tips = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &tips_opts)
1614            .expect("tips");
1615        let closure_set: HashSet<ObjectId> = closure.send.iter().copied().collect();
1616        let tips_set: HashSet<ObjectId> = tips.send.iter().copied().collect();
1617        assert_eq!(closure_set, tips_set);
1618        assert!(
1619            tips.send.len() <= 8,
1620            "one new commit should enumerate a handful of objects, got {}",
1621            tips.send.len()
1622        );
1623    }
1624
1625    #[test]
1626    fn tips_only_send_pack_does_not_prewalk_deep_history() {
1627        use crate::merge_base::intersect_walk_test_hooks::{
1628            commit_parent_lookups, reset_commit_parent_lookups,
1629        };
1630
1631        let dir = TempDir::new().unwrap();
1632        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1633        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1634        let mut parent = write_commit(
1635            &repo,
1636            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1637            &[],
1638            0,
1639        );
1640        for i in 1..800 {
1641            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1642            parent = write_commit(&repo, tree, &[parent], i);
1643        }
1644        let remote_tip = parent;
1645        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1646        let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1647        let haves = vec![remote_tip];
1648        let empty = HashSet::new();
1649        reset_commit_parent_lookups();
1650        let _selection = enumerate_pack_objects(
1651            repo.odb(),
1652            &[new_tip],
1653            &haves,
1654            &PackEnumerateOptions {
1655                filter: None,
1656                shallow_grafts: &empty,
1657                have_shallow_grafts: Some(&empty),
1658                use_bitmaps: false,
1659                exclude_objects: None,
1660                peer_odb: None,
1661                have_boundary: HaveBoundary::TipsOnly,
1662            },
1663        )
1664        .expect("enumerate");
1665        assert!(
1666            commit_parent_lookups() <= 128,
1667            "send-pack hide must stay coupled to the want walk ({} parent lookups)",
1668            commit_parent_lookups()
1669        );
1670    }
1671
1672    #[test]
1673    fn push_dated_before_graphed_history_stops_at_the_frontier() {
1674        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1675        let dir = TempDir::new().unwrap();
1676        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1677        let tree = write_tree(&repo, &[]);
1678        // The remote's history, in the commit-graph, with a tag partway down.
1679        let mut tip = write_commit(&repo, tree, &[], 1_300_000_000);
1680        let mut tagged = tip;
1681        for i in 1..300 {
1682            tip = write_commit(&repo, tree, &[tip], 1_300_000_000 + i);
1683            if i == 150 {
1684                tagged = tip;
1685            }
1686        }
1687        std::fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n")).unwrap();
1688        repo.odb().write_commit_graph().unwrap();
1689        // New local work outside the graph, dated before all of that history (a skewed
1690        // clock, or fixed dates in a script).
1691        let mut new_tip = tip;
1692        for i in 0..3 {
1693            new_tip = write_commit(&repo, tree, &[new_tip], 1_000_000_000 + i);
1694        }
1695
1696        reset_commit_reads();
1697        let hidden = send_pack_uninteresting_commits(
1698            repo.odb(),
1699            &[new_tip],
1700            &[tip, tagged],
1701            &HashSet::new(),
1702            &CancelSignal::never(),
1703        )
1704        .unwrap();
1705        assert!(hidden.contains(&tip));
1706        assert!(hidden.contains(&tagged));
1707        assert!(!hidden.contains(&new_tip));
1708        assert_eq!(
1709            commit_reads(),
1710            3,
1711            "only the commits outside the graph are read from the object store"
1712        );
1713        assert!(
1714            hidden.len() <= 4,
1715            "the walk must stop at the frontier, not drain the remote's history ({} marked)",
1716            hidden.len()
1717        );
1718    }
1719
1720    #[test]
1721    fn duplicated_wants_already_on_peer_balance_the_frontier() {
1722        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1723        let dir = TempDir::new().unwrap();
1724        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1725        let tree = write_tree(&repo, &[]);
1726        let mut tip = write_commit(&repo, tree, &[], 0);
1727        for time in 1..100 {
1728            tip = write_commit(&repo, tree, &[tip], time);
1729        }
1730        reset_commit_reads();
1731        let hidden = send_pack_uninteresting_commits(
1732            repo.odb(),
1733            &[tip, tip],
1734            &[tip, tip],
1735            &HashSet::new(),
1736            &CancelSignal::never(),
1737        )
1738        .unwrap();
1739        assert!(hidden.contains(&tip));
1740        assert!(
1741            commit_reads() <= 2,
1742            "shared want/have must stop at the frontier"
1743        );
1744        let empty = HashSet::new();
1745        let selection = enumerate_pack_objects(
1746            repo.odb(),
1747            &[tip, tip],
1748            &[tip, tip],
1749            &PackEnumerateOptions {
1750                filter: None,
1751                shallow_grafts: &empty,
1752                have_shallow_grafts: None,
1753                use_bitmaps: false,
1754                exclude_objects: None,
1755                peer_odb: None,
1756                have_boundary: HaveBoundary::TipsOnly,
1757            },
1758        )
1759        .unwrap();
1760        assert!(selection.send.is_empty());
1761    }
1762
1763    #[test]
1764    fn tips_only_many_advertised_haves_does_not_prewalk_deep_history() {
1765        use crate::merge_base::intersect_walk_test_hooks::{
1766            commit_parent_lookups, reset_commit_parent_lookups,
1767        };
1768
1769        let dir = TempDir::new().unwrap();
1770        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1771        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1772        let mut chain = Vec::new();
1773        let mut parent = write_commit(
1774            &repo,
1775            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1776            &[],
1777            0,
1778        );
1779        chain.push(parent);
1780        for i in 1..800 {
1781            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1782            parent = write_commit(&repo, tree, &[parent], i);
1783            chain.push(parent);
1784        }
1785        let remote_tip = parent;
1786        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1787        let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1788        let mut haves = vec![remote_tip];
1789        for n in (2..400).step_by(2) {
1790            let idx = chain.len().checked_sub(1 + n);
1791            if let Some(idx) = idx {
1792                haves.push(chain[idx]);
1793            }
1794        }
1795        let empty = HashSet::new();
1796        reset_commit_parent_lookups();
1797        let selection = enumerate_pack_objects(
1798            repo.odb(),
1799            &[new_tip],
1800            &haves,
1801            &PackEnumerateOptions {
1802                filter: None,
1803                shallow_grafts: &empty,
1804                have_shallow_grafts: Some(&empty),
1805                use_bitmaps: false,
1806                exclude_objects: None,
1807                peer_odb: None,
1808                have_boundary: HaveBoundary::TipsOnly,
1809            },
1810        )
1811        .expect("enumerate");
1812        assert!(
1813            selection.send.len() <= 8,
1814            "one new commit should stay small with many haves, got {}",
1815            selection.send.len()
1816        );
1817        assert!(
1818            commit_parent_lookups() <= 512,
1819            "many advertised haves must not pre-walk deep history ({} parent lookups)",
1820            commit_parent_lookups()
1821        );
1822    }
1823
1824    #[test]
1825    fn send_pack_have_walk_stops_without_scanning_deep_haves() {
1826        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1827
1828        let dir = TempDir::new().unwrap();
1829        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1830        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1831        let mut parent = write_commit(
1832            &repo,
1833            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1834            &[],
1835            0,
1836        );
1837        let near_root = parent;
1838        for i in 1..800 {
1839            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1840            parent = write_commit(&repo, tree, &[parent], i);
1841        }
1842        let direct_parent = parent;
1843        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1844        let new_tip = write_commit(&repo, new_tree, &[direct_parent], 900);
1845        let empty = HashSet::new();
1846        reset_commit_reads();
1847        let have_commits = send_pack_uninteresting_commits(
1848            repo.odb(),
1849            &[new_tip],
1850            &[direct_parent, near_root],
1851            &empty,
1852            &CancelSignal::never(),
1853        )
1854        .expect("send-pack have walk");
1855        assert!(have_commits.contains(&direct_parent));
1856        assert!(have_commits.contains(&near_root));
1857        let reads = commit_reads();
1858        assert!(
1859            reads < 20,
1860            "send-pack have walk should not scan deep history, read {reads} commits"
1861        );
1862    }
1863
1864    #[test]
1865    fn walk_lists_trees_of_newer_commits_first() {
1866        let dir = TempDir::new().unwrap();
1867        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1868        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1869        let tree_a = write_tree(&repo, &[(0o100644, "f", blob("a"))]);
1870        let tree_b = write_tree(&repo, &[(0o100644, "f", blob("b"))]);
1871        let tree_c = write_tree(&repo, &[(0o100644, "f", blob("c"))]);
1872        let base = write_commit(&repo, tree_a, &[], 100);
1873        // Two branches: the older tip is listed as the first root.
1874        let old_tip = write_commit(&repo, tree_b, &[base], 200);
1875        let new_tip = write_commit(&repo, tree_c, &[base], 300);
1876
1877        let selection = walk(&repo, &[old_tip, new_tip], NameTrees::No);
1878        let pos = |oid: ObjectId| selection.send.iter().position(|o| *o == oid).unwrap();
1879        assert!(pos(old_tip) < pos(base) && pos(new_tip) < pos(base));
1880        assert!(pos(tree_c) < pos(tree_b) && pos(tree_b) < pos(tree_a));
1881    }
1882
1883    #[test]
1884    fn walk_names_trees_and_keeps_history_paths_for_path_less_roots() {
1885        let dir = TempDir::new().unwrap();
1886        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1887        let file = repo.odb().write(ObjectKind::Blob, b"x\n").unwrap();
1888        let link = repo.odb().write(ObjectKind::Blob, b"target").unwrap();
1889        let sub = write_tree(
1890            &repo,
1891            &[(0o100644, "file.rs", file), (0o120000, "link", link)],
1892        );
1893        let root = write_tree(&repo, &[(0o040000, "src", sub)]);
1894        let commit = write_commit(&repo, root, &[], 100);
1895
1896        // Index roots (the subtree and the file) come first and have no path.
1897        let selection = walk(&repo, &[sub, file, commit], NameTrees::Yes);
1898        let hash = |oid: ObjectId| selection.name_hashes.get(&oid).copied();
1899        assert_eq!(hash(sub), Some(pack_name_hash("src")));
1900        assert_eq!(hash(file), Some(pack_name_hash("src/file.rs")));
1901        assert_eq!(hash(root), None, "root trees have no path");
1902        assert_eq!(selection.send.len(), 5);
1903
1904        let blobs_only = walk(&repo, &[sub, file, commit], NameTrees::No);
1905        assert_eq!(blobs_only.name_hashes.get(&sub), None);
1906        assert_eq!(
1907            blobs_only.name_hashes.get(&file),
1908            Some(&pack_name_hash("src/file.rs"))
1909        );
1910    }
1911
1912    fn tree_entry(mode: &str, name: &str, oid: ObjectId) -> Vec<u8> {
1913        let mut bytes = format!("{mode} {name}\0").into_bytes();
1914        bytes.extend_from_slice(oid.as_bytes());
1915        bytes
1916    }
1917
1918    /// Old tools wrote tree entries with non-canonical modes (`40755` for a
1919    /// directory, `100664` for a file). Git walks them by their type bits, so the
1920    /// enumeration must too, or everything below such a directory is left out.
1921    #[cfg_attr(
1922        not(feature = "full-tests"),
1923        ignore = "starts system git or is heavy; run with --features full-tests"
1924    )]
1925    #[test]
1926    fn walk_descends_into_trees_with_non_canonical_modes() {
1927        let tmp = tempfile::tempdir().unwrap();
1928        Repository::init(tmp.path(), &InitOptions::new().with_bare(true)).unwrap();
1929        let repo = Repository::open(tmp.path(), &RepositoryOptions::empty()).unwrap();
1930        let odb = repo.odb();
1931        let deep = odb
1932            .write_loose_materialize(ObjectKind::Blob, b"deep\n")
1933            .unwrap();
1934        let odd = odb
1935            .write_loose_materialize(ObjectKind::Blob, b"odd\n")
1936            .unwrap();
1937        let inner = odb
1938            .write_loose_materialize(ObjectKind::Tree, &tree_entry("100644", "deep.txt", deep))
1939            .unwrap();
1940        let mut root = tree_entry("100664", "odd.txt", odd);
1941        root.extend(tree_entry("40755", "sub", inner));
1942        let root = odb
1943            .write_loose_materialize(ObjectKind::Tree, &root)
1944            .unwrap();
1945        let commit = format!(
1946            "tree {root}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nodd modes\n"
1947        );
1948        let commit = odb
1949            .write_loose_materialize(ObjectKind::Commit, commit.as_bytes())
1950            .unwrap();
1951
1952        let empty = HashSet::new();
1953        let selection = enumerate_pack_objects(
1954            odb,
1955            &[commit],
1956            &[],
1957            &PackEnumerateOptions {
1958                filter: None,
1959                shallow_grafts: &empty,
1960                have_shallow_grafts: Some(&empty),
1961                use_bitmaps: false,
1962                exclude_objects: None,
1963                peer_odb: None,
1964                have_boundary: HaveBoundary::CommitClosure,
1965            },
1966        )
1967        .unwrap();
1968
1969        let ours: HashSet<ObjectId> = selection.send.iter().copied().collect();
1970        let git: HashSet<ObjectId> =
1971            grit_test_support::git(tmp.path(), &["rev-list", "--objects", &commit.to_hex()])
1972                .lines()
1973                .map(|line| ObjectId::from_hex(&line[..40]).unwrap())
1974                .collect();
1975        assert_eq!(ours, git, "same objects as git rev-list --objects");
1976        assert!(ours.contains(&inner) && ours.contains(&deep));
1977        assert_eq!(selection.send.len(), 5);
1978        assert_eq!(
1979            selection.name_hashes.get(&deep),
1980            Some(&pack_name_hash("sub/deep.txt"))
1981        );
1982        assert_eq!(
1983            selection.name_hashes.get(&odd),
1984            Some(&pack_name_hash("odd.txt"))
1985        );
1986    }
1987
1988    fn tips_only_options(empty: &HashSet<ObjectId>) -> PackEnumerateOptions<'_> {
1989        PackEnumerateOptions {
1990            filter: None,
1991            shallow_grafts: empty,
1992            have_shallow_grafts: Some(empty),
1993            use_bitmaps: false,
1994            exclude_objects: None,
1995            peer_odb: None,
1996            have_boundary: HaveBoundary::TipsOnly,
1997        }
1998    }
1999
2000    fn write_tag(repo: &Repository, target: ObjectId, kind: &str, name: &str) -> ObjectId {
2001        let body = format!(
2002            "object {}\ntype {kind}\ntag {name}\ntagger A <a@example.com> 0 +0000\n\n{name}\n",
2003            target.to_hex()
2004        );
2005        repo.odb().write(ObjectKind::Tag, body.as_bytes()).unwrap()
2006    }
2007
2008    #[test]
2009    fn tips_only_thin_bases_come_from_edge_commits_not_every_advertised_tip() {
2010        let dir = TempDir::new().unwrap();
2011        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2012        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2013        let base_tree = write_tree(&repo, &[(0o100644, "f", blob("base"))]);
2014        let root = write_commit(&repo, base_tree, &[], 0);
2015        let main = write_commit(
2016            &repo,
2017            write_tree(&repo, &[(0o100644, "f", blob("main"))]),
2018            &[root],
2019            10,
2020        );
2021        let mut haves = vec![main];
2022        for i in 0..40 {
2023            let tree = write_tree(&repo, &[(0o100644, "f", blob(&format!("side {i}")))]);
2024            let side = write_commit(&repo, tree, &[root], 20 + i);
2025            haves.push(write_tag(&repo, side, "commit", &format!("t{i}")));
2026            haves.push(side);
2027        }
2028        let new_tip = write_commit(
2029            &repo,
2030            write_tree(&repo, &[(0o100644, "f", blob("main\nmore"))]),
2031            &[main],
2032            100,
2033        );
2034        let empty = HashSet::new();
2035        let selection =
2036            enumerate_pack_objects(repo.odb(), &[new_tip], &haves, &tips_only_options(&empty))
2037                .unwrap();
2038        assert_eq!(selection.send.len(), 3, "commit, tree and the changed blob");
2039        assert_eq!(
2040            selection.have_closure,
2041            HashSet::from([main]),
2042            "thin bases come from the commit the walk stopped at"
2043        );
2044
2045        let closure = enumerate_pack_objects(
2046            repo.odb(),
2047            &[new_tip],
2048            &haves,
2049            &PackEnumerateOptions {
2050                have_boundary: HaveBoundary::CommitClosure,
2051                ..tips_only_options(&empty)
2052            },
2053        )
2054        .unwrap();
2055        assert_eq!(closure.send, selection.send);
2056        assert_eq!(
2057            closure.have_closure,
2058            HashSet::from([main]),
2059            "the full have pre-walk seeds thin bases from the same edge commit"
2060        );
2061    }
2062
2063    #[test]
2064    fn send_pack_walk_peels_tag_haves_and_ignores_non_commit_targets() {
2065        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2066        let dir = TempDir::new().unwrap();
2067        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2068        let blob = repo.odb().write(ObjectKind::Blob, b"x").unwrap();
2069        let tree = write_tree(&repo, &[(0o100644, "f", blob)]);
2070        let old = write_commit(&repo, tree, &[], 0);
2071        let tip = write_commit(&repo, tree, &[old], 1);
2072        let tag = write_tag(&repo, tip, "commit", "v1");
2073        let nested = write_tag(&repo, tag, "tag", "v1-nested");
2074        let tree_tag = write_tag(&repo, tree, "tree", "tree-tag");
2075        let blob_tag = write_tag(&repo, blob, "blob", "blob-tag");
2076        let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
2077        let new_tip = write_commit(&repo, tree, &[tip], 2);
2078
2079        reset_commit_reads();
2080        let hidden = send_pack_uninteresting_commits(
2081            repo.odb(),
2082            &[new_tip],
2083            &[nested, tag, tree_tag, blob_tag, missing],
2084            &HashSet::new(),
2085            &CancelSignal::never(),
2086        )
2087        .unwrap();
2088        assert!(hidden.contains(&tip), "tags peel to the commit they name");
2089        for oid in [
2090            tag, nested, tree_tag, blob_tag, tree, blob, missing, new_tip,
2091        ] {
2092            assert!(!hidden.contains(&oid), "{oid} is not a have-side commit");
2093        }
2094        assert_eq!(
2095            commit_reads(),
2096            2,
2097            "the want and the tag-named commit, each queued once"
2098        );
2099    }
2100}