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    Ok(WalkedObjects {
1127        send: walk.ordered,
1128        name_hashes: walk.name_hashes,
1129        edges: walk.edges,
1130        missing: walk.missing.unwrap_or_default(),
1131    })
1132}
1133
1134/// Result of [`collect_reachable_excluding_with_name_hashes`].
1135struct WalkedObjects {
1136    /// Objects to send, in pack order.
1137    send: Vec<ObjectId>,
1138    /// Pack name hash of each named object.
1139    name_hashes: HashMap<ObjectId, u32>,
1140    /// Have-side commits the walk stopped at.
1141    edges: HashSet<ObjectId>,
1142    /// Blobs left out because they are not stored ([`MissingBlobMode::Probe`] only).
1143    missing: Vec<MissingBlob>,
1144}
1145
1146fn child_path(parent: &str, name: &[u8]) -> String {
1147    let name = String::from_utf8_lossy(name);
1148    if parent.is_empty() {
1149        name.into_owned()
1150    } else {
1151        format!("{parent}/{name}")
1152    }
1153}
1154
1155/// State of [`collect_reachable_excluding_with_name_hashes`].
1156struct EnumerationWalk<'a> {
1157    odb: &'a Odb,
1158    exclude: &'a HashSet<ObjectId>,
1159    /// Negotiation boundaries: do not list or expand past these objects.
1160    boundary: &'a mut HashSet<ObjectId>,
1161    /// Uninteresting commits (send-pack) or full have closure (upload-pack).
1162    have_commits: &'a HashSet<ObjectId>,
1163    /// Objects already on the peer (local fetch): stop without listing.
1164    peer_odb: Option<&'a Odb>,
1165    /// Alternate object directories (`repack -l` / `pack-objects --local`).
1166    alternate_sources: Option<Arc<CompositeStore>>,
1167    skip_missing: bool,
1168    filter: Option<&'a ObjectFilter>,
1169    /// The filter's verdict for trees and blobs depends on where they are met.
1170    depth_sensitive: bool,
1171    name_trees: NameTrees,
1172    /// Objects listed (or held by the peer), and objects the filter left out whose
1173    /// verdict cannot change: parent commits, and without a depth-sensitive filter,
1174    /// trees and size-checked blobs.
1175    visited: OidHashSet,
1176    /// Shallowest depth each tree was walked at (depth-sensitive filters only).
1177    tree_depths: OidHashMap<u64>,
1178    /// Objects in [`Self::visited`] that the filter left out. A root or tag target
1179    /// naming one of them still lists it.
1180    unlisted: OidHashSet,
1181    ordered: Vec<ObjectId>,
1182    /// Commits read but not yet expanded, newest first.
1183    commits: BinaryHeap<QueuedCommit>,
1184    next_seq: u64,
1185    /// Objects to read: id, path for name hashes, and tree depth. Drained only
1186    /// while [`Self::commits`] is empty.
1187    queue: VecDeque<(ObjectId, String, u64)>,
1188    name_hashes: HashMap<ObjectId, u32>,
1189    /// Have-side commits the walk stopped at (Git's edge commits).
1190    edges: HashSet<ObjectId>,
1191    /// With [`MissingBlobMode::Probe`], blobs that would be listed but are not stored.
1192    missing: Option<Vec<MissingBlob>>,
1193}
1194
1195/// A commit waiting in [`EnumerationWalk::commits`], ordered newest first and,
1196/// among equal dates, in the order it was found.
1197struct QueuedCommit {
1198    /// Commit-graph generation; [`UNRANKED_GENERATION`] outside the graph, so those sort first.
1199    generation: u64,
1200    time: i64,
1201    seq: std::cmp::Reverse<u64>,
1202    oid: ObjectId,
1203    tree: ObjectId,
1204    parents: Vec<ObjectId>,
1205}
1206
1207impl PartialEq for QueuedCommit {
1208    fn eq(&self, other: &Self) -> bool {
1209        (self.generation, self.time, self.seq) == (other.generation, other.time, other.seq)
1210    }
1211}
1212
1213impl Eq for QueuedCommit {}
1214
1215impl PartialOrd for QueuedCommit {
1216    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1217        Some(self.cmp(other))
1218    }
1219}
1220
1221impl Ord for QueuedCommit {
1222    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1223        (self.generation, self.time, self.seq).cmp(&(other.generation, other.time, other.seq))
1224    }
1225}
1226
1227impl<'a> EnumerationWalk<'a> {
1228    fn list(&mut self, oid: ObjectId) {
1229        if !self.exclude.contains(&oid) {
1230            self.ordered.push(oid);
1231        }
1232    }
1233
1234    fn note_boundary(&mut self, oid: ObjectId) {
1235        self.visited.insert(oid);
1236    }
1237
1238    fn at_object_boundary(&self, oid: ObjectId) -> bool {
1239        if self
1240            .alternate_sources
1241            .as_ref()
1242            .is_some_and(|s| s.contains(&oid).unwrap_or(false))
1243        {
1244            return true;
1245        }
1246        self.boundary.contains(&oid)
1247            || self
1248                .peer_odb
1249                .is_some_and(|peer| peer.exists_materialized_local_without_rescan(&oid))
1250    }
1251
1252    fn seal_have_reachable_commit(&mut self, oid: ObjectId) -> Result<()> {
1253        if !self.visited.insert(oid) {
1254            return Ok(());
1255        }
1256        self.edges.insert(oid);
1257        self.boundary
1258            .extend(objects_in_commit_snapshot(self.odb, oid)?);
1259        Ok(())
1260    }
1261
1262    fn commit_on_have_side(&mut self, oid: ObjectId) -> Result<bool> {
1263        Ok(self.have_commits.contains(&oid))
1264    }
1265
1266    /// An object that is sent whatever the filter says: a root or a tag target.
1267    fn push_object(&mut self, oid: ObjectId, path: String) {
1268        if let Ok(true) = self.commit_on_have_side(oid) {
1269            let _ = self.seal_have_reachable_commit(oid);
1270            return;
1271        }
1272        if self.at_object_boundary(oid) {
1273            self.note_boundary(oid);
1274            return;
1275        }
1276        if self.visited.insert(oid) {
1277            self.list(oid);
1278            self.queue.push_back((oid, path, 0));
1279        } else if self.unlisted.remove(&oid) {
1280            self.list(oid);
1281        }
1282    }
1283
1284    /// A root or tag target that may be a commit: read now so a commit enters the
1285    /// date-ordered heap at once; anything else waits in [`Self::queue`].
1286    fn push_root(&mut self, oid: ObjectId) -> Result<()> {
1287        if self.commit_on_have_side(oid)? {
1288            return self.seal_have_reachable_commit(oid);
1289        }
1290        if self.at_object_boundary(oid) {
1291            self.note_boundary(oid);
1292            return Ok(());
1293        }
1294        if !self.visited.insert(oid) {
1295            if self.unlisted.remove(&oid) {
1296                self.list(oid);
1297            }
1298            return Ok(());
1299        }
1300        self.list(oid);
1301        // Only a commit is read now; anything else (a blob want, say) is read, if at
1302        // all, when the queue reaches it.
1303        match self.odb.read_info(&oid) {
1304            Ok(info) if info.kind != ObjectKind::Commit => {
1305                self.queue.push_back((oid, String::new(), 0));
1306                return Ok(());
1307            }
1308            Ok(_) => {}
1309            Err(_) if self.skip_missing => return Ok(()),
1310            Err(e) => return Err(e),
1311        }
1312        match self.read(&oid)? {
1313            Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1314            Some(_) => {
1315                self.queue.push_back((oid, String::new(), 0));
1316                Ok(())
1317            }
1318            None => Ok(()),
1319        }
1320    }
1321
1322    /// A parent commit: always walked, listed unless the filter keeps other types only.
1323    /// Read now so it enters the date-ordered heap before any tree is expanded.
1324    fn push_parent(&mut self, oid: ObjectId) -> Result<()> {
1325        if self.commit_on_have_side(oid)? {
1326            return self.seal_have_reachable_commit(oid);
1327        }
1328        if self.at_object_boundary(oid) {
1329            self.note_boundary(oid);
1330            return Ok(());
1331        }
1332        let listed = self
1333            .filter
1334            .is_none_or(|f| f.includes_commit_or_tag_object(ObjectKind::Commit));
1335        if !self.visited.insert(oid) {
1336            if listed && self.unlisted.remove(&oid) {
1337                self.list(oid);
1338            }
1339            return Ok(());
1340        }
1341        if listed {
1342            self.list(oid);
1343        } else {
1344            self.unlisted.insert(oid);
1345        }
1346        match self.read(&oid)? {
1347            Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1348            Some(_) => {
1349                self.queue.push_back((oid, String::new(), 0));
1350                Ok(())
1351            }
1352            None => Ok(()),
1353        }
1354    }
1355
1356    fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
1357        match self.odb.read(oid) {
1358            Ok(obj) => Ok(Some(obj)),
1359            Err(_) if self.skip_missing => Ok(None),
1360            Err(e) => Err(e),
1361        }
1362    }
1363
1364    /// Hold a commit read from [`Self::queue`] until it is the newest one left.
1365    fn queue_commit(&mut self, oid: ObjectId, obj: Object) -> Result<()> {
1366        let parsed = CommitRef::parse(&obj.data)?;
1367        let queued = QueuedCommit {
1368            generation: UNRANKED_GENERATION,
1369            time: parsed.committer_unix_seconds_for_ordering(),
1370            seq: std::cmp::Reverse(self.next_seq),
1371            oid,
1372            tree: parsed.tree(),
1373            parents: parsed.parents().to_vec(),
1374        };
1375        self.next_seq += 1;
1376        self.commits.push(queued);
1377        Ok(())
1378    }
1379
1380    /// A commit's root tree (`depth` 0) or a subtree found in a tree.
1381    fn push_tree(&mut self, oid: ObjectId, path: String, depth: u64) {
1382        if self.name_trees == NameTrees::Yes && !path.is_empty() && !self.visited.contains(&oid) {
1383            self.name_hashes
1384                .entry(oid)
1385                .or_insert_with(|| pack_name_hash(&path));
1386        }
1387        let Some(filter) = self.filter else {
1388            self.push_object(oid, path);
1389            return;
1390        };
1391        // A tree in a have commit's snapshot is already on the peer, whatever the filter.
1392        if self.at_object_boundary(oid) {
1393            self.note_boundary(oid);
1394            return;
1395        }
1396        let first_walk = if self.depth_sensitive {
1397            match self.tree_depths.get(&oid) {
1398                Some(&walked_at) if walked_at <= depth => false,
1399                _ => {
1400                    self.tree_depths.insert(oid, depth);
1401                    true
1402                }
1403            }
1404        } else {
1405            self.visited.insert(oid)
1406        };
1407        if !first_walk {
1408            return;
1409        }
1410        if filter.includes_tree(depth) {
1411            if !self.depth_sensitive || self.visited.insert(oid) {
1412                self.list(oid);
1413            }
1414        } else if !self.depth_sensitive {
1415            self.unlisted.insert(oid);
1416        }
1417        if !filter.prunes_tree_entries(depth) {
1418            self.queue.push_back((oid, path, depth));
1419        }
1420    }
1421
1422    /// A blob in a tree at `tree_depth`; `name` is `(tree path, entry name)` for regular
1423    /// files, which get a pack name hash.
1424    fn push_blob(
1425        &mut self,
1426        oid: ObjectId,
1427        name: Option<(&str, &[u8])>,
1428        referrer: ObjectId,
1429        tree_depth: u64,
1430    ) -> Result<()> {
1431        if self.at_object_boundary(oid) {
1432            self.note_boundary(oid);
1433            return Ok(());
1434        }
1435        if self.visited.contains(&oid) {
1436            return Ok(());
1437        }
1438        let (passes, size_checked) = match self.filter {
1439            None => (true, false),
1440            // Missing (probe mode only): as in Git's filters, an object whose size
1441            // cannot be read is not filtered out but reported missing.
1442            Some(filter) => self
1443                .blob_passes(filter, &oid, tree_depth)?
1444                .unwrap_or((true, true)),
1445        };
1446        if passes && self.missing.is_some() && !self.odb.contains(&oid)? {
1447            self.visited.insert(oid);
1448            if let Some(missing) = self.missing.as_mut() {
1449                missing.push(MissingBlob {
1450                    oid,
1451                    referrer: Some(referrer),
1452                });
1453            }
1454            return Ok(());
1455        }
1456        if passes {
1457            self.visited.insert(oid);
1458            self.list(oid);
1459            if let Some((parent, entry_name)) = name {
1460                self.name_hashes
1461                    .insert(oid, pack_name_hash(&child_path(parent, entry_name)));
1462            }
1463        } else if size_checked && !self.depth_sensitive {
1464            self.visited.insert(oid);
1465            self.unlisted.insert(oid);
1466        }
1467        Ok(())
1468    }
1469
1470    /// Whether the blob passes `filter`, and whether its size had to be looked up.
1471    ///
1472    /// `None` when the size was needed but the blob is not stored, which only
1473    /// happens with [`MissingBlobMode::Probe`] (the size is read without a lazy
1474    /// fetch there); otherwise a missing blob is an error, or with `skip_missing`
1475    /// filtered out.
1476    fn blob_passes(
1477        &self,
1478        filter: &ObjectFilter,
1479        oid: &ObjectId,
1480        tree_depth: u64,
1481    ) -> Result<Option<(bool, bool)>> {
1482        // A smaller blob never fares worse, so the size only matters between the extremes.
1483        if !filter.includes_blob_under_tree(0, tree_depth) {
1484            return Ok(Some((false, false)));
1485        }
1486        if filter.includes_blob_under_tree(u64::MAX, tree_depth) {
1487            return Ok(Some((true, false)));
1488        }
1489        let info = if self.missing.is_some() {
1490            match self.odb.read_info_without_fetch(oid) {
1491                Err(crate::error::Error::ObjectNotFound(_)) => return Ok(None),
1492                other => other,
1493            }
1494        } else {
1495            self.odb.read_info(oid)
1496        };
1497        match info {
1498            Ok(info) => Ok(Some((
1499                filter.includes_blob_under_tree(info.size, tree_depth),
1500                true,
1501            ))),
1502            Err(_) if self.skip_missing => Ok(Some((false, true))),
1503            Err(e) => Err(e),
1504        }
1505    }
1506}
1507
1508#[cfg(test)]
1509mod tests {
1510    #![allow(clippy::unwrap_used, clippy::expect_used)]
1511
1512    use super::*;
1513
1514    use crate::repo::{InitOptions, Repository};
1515    use tempfile::TempDir;
1516
1517    fn walk(repo: &Repository, roots: &[ObjectId], name_trees: NameTrees) -> PackObjectSelection {
1518        let empty = HashSet::new();
1519        enumerate_pack_objects_naming(
1520            repo.odb(),
1521            roots,
1522            &[],
1523            &PackEnumerateOptions {
1524                filter: None,
1525                shallow_grafts: &empty,
1526                have_shallow_grafts: Some(&empty),
1527                use_bitmaps: false,
1528                exclude_objects: None,
1529                peer_odb: None,
1530                have_boundary: HaveBoundary::CommitClosure,
1531            },
1532            name_trees,
1533            false,
1534            &CancelSignal::never(),
1535        )
1536        .expect("walk")
1537    }
1538
1539    fn write_tree(repo: &Repository, entries: &[(u32, &str, ObjectId)]) -> ObjectId {
1540        let entries: Vec<crate::objects::TreeEntry> = entries
1541            .iter()
1542            .map(|&(mode, name, oid)| crate::objects::TreeEntry {
1543                mode,
1544                name: name.as_bytes().to_vec(),
1545                oid,
1546            })
1547            .collect();
1548        repo.odb()
1549            .write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
1550            .expect("tree")
1551    }
1552
1553    fn write_commit(
1554        repo: &Repository,
1555        tree: ObjectId,
1556        parents: &[ObjectId],
1557        time: i64,
1558    ) -> ObjectId {
1559        let mut body = format!("tree {}\n", tree.to_hex());
1560        for parent in parents {
1561            body.push_str(&format!("parent {}\n", parent.to_hex()));
1562        }
1563        body.push_str(&format!(
1564            "author A <a@example.com> {time} +0000\ncommitter A <a@example.com> {time} +0000\n\nc{time}\n"
1565        ));
1566        repo.odb()
1567            .write(ObjectKind::Commit, body.as_bytes())
1568            .expect("commit")
1569    }
1570
1571    #[test]
1572    fn tips_only_boundary_matches_closure_on_linear_incremental_push() {
1573        let dir = TempDir::new().unwrap();
1574        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1575        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1576        let mut parent = write_commit(
1577            &repo,
1578            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1579            &[],
1580            0,
1581        );
1582        for i in 1..800 {
1583            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1584            parent = write_commit(&repo, tree, &[parent], i);
1585        }
1586        let remote_tip = parent;
1587        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1588        let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1589        let empty = HashSet::new();
1590        let closure_opts = PackEnumerateOptions {
1591            filter: None,
1592            shallow_grafts: &empty,
1593            have_shallow_grafts: Some(&empty),
1594            use_bitmaps: false,
1595            exclude_objects: None,
1596            peer_odb: None,
1597            have_boundary: HaveBoundary::CommitClosure,
1598        };
1599        let tips_opts = PackEnumerateOptions {
1600            have_boundary: HaveBoundary::TipsOnly,
1601            ..closure_opts
1602        };
1603        let closure = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &closure_opts)
1604            .expect("closure");
1605        let tips = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &tips_opts)
1606            .expect("tips");
1607        let closure_set: HashSet<ObjectId> = closure.send.iter().copied().collect();
1608        let tips_set: HashSet<ObjectId> = tips.send.iter().copied().collect();
1609        assert_eq!(closure_set, tips_set);
1610        assert!(
1611            tips.send.len() <= 8,
1612            "one new commit should enumerate a handful of objects, got {}",
1613            tips.send.len()
1614        );
1615    }
1616
1617    #[test]
1618    fn tips_only_send_pack_does_not_prewalk_deep_history() {
1619        use crate::merge_base::intersect_walk_test_hooks::{
1620            commit_parent_lookups, reset_commit_parent_lookups,
1621        };
1622
1623        let dir = TempDir::new().unwrap();
1624        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1625        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1626        let mut parent = write_commit(
1627            &repo,
1628            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1629            &[],
1630            0,
1631        );
1632        for i in 1..800 {
1633            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1634            parent = write_commit(&repo, tree, &[parent], i);
1635        }
1636        let remote_tip = parent;
1637        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1638        let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1639        let haves = vec![remote_tip];
1640        let empty = HashSet::new();
1641        reset_commit_parent_lookups();
1642        let _selection = enumerate_pack_objects(
1643            repo.odb(),
1644            &[new_tip],
1645            &haves,
1646            &PackEnumerateOptions {
1647                filter: None,
1648                shallow_grafts: &empty,
1649                have_shallow_grafts: Some(&empty),
1650                use_bitmaps: false,
1651                exclude_objects: None,
1652                peer_odb: None,
1653                have_boundary: HaveBoundary::TipsOnly,
1654            },
1655        )
1656        .expect("enumerate");
1657        assert!(
1658            commit_parent_lookups() <= 128,
1659            "send-pack hide must stay coupled to the want walk ({} parent lookups)",
1660            commit_parent_lookups()
1661        );
1662    }
1663
1664    #[test]
1665    fn push_dated_before_graphed_history_stops_at_the_frontier() {
1666        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1667        let dir = TempDir::new().unwrap();
1668        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1669        let tree = write_tree(&repo, &[]);
1670        // The remote's history, in the commit-graph, with a tag partway down.
1671        let mut tip = write_commit(&repo, tree, &[], 1_300_000_000);
1672        let mut tagged = tip;
1673        for i in 1..300 {
1674            tip = write_commit(&repo, tree, &[tip], 1_300_000_000 + i);
1675            if i == 150 {
1676                tagged = tip;
1677            }
1678        }
1679        std::fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n")).unwrap();
1680        repo.odb().write_commit_graph().unwrap();
1681        // New local work outside the graph, dated before all of that history (a skewed
1682        // clock, or fixed dates in a script).
1683        let mut new_tip = tip;
1684        for i in 0..3 {
1685            new_tip = write_commit(&repo, tree, &[new_tip], 1_000_000_000 + i);
1686        }
1687
1688        reset_commit_reads();
1689        let hidden = send_pack_uninteresting_commits(
1690            repo.odb(),
1691            &[new_tip],
1692            &[tip, tagged],
1693            &HashSet::new(),
1694            &CancelSignal::never(),
1695        )
1696        .unwrap();
1697        assert!(hidden.contains(&tip));
1698        assert!(hidden.contains(&tagged));
1699        assert!(!hidden.contains(&new_tip));
1700        assert_eq!(
1701            commit_reads(),
1702            3,
1703            "only the commits outside the graph are read from the object store"
1704        );
1705        assert!(
1706            hidden.len() <= 4,
1707            "the walk must stop at the frontier, not drain the remote's history ({} marked)",
1708            hidden.len()
1709        );
1710    }
1711
1712    #[test]
1713    fn duplicated_wants_already_on_peer_balance_the_frontier() {
1714        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1715        let dir = TempDir::new().unwrap();
1716        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1717        let tree = write_tree(&repo, &[]);
1718        let mut tip = write_commit(&repo, tree, &[], 0);
1719        for time in 1..100 {
1720            tip = write_commit(&repo, tree, &[tip], time);
1721        }
1722        reset_commit_reads();
1723        let hidden = send_pack_uninteresting_commits(
1724            repo.odb(),
1725            &[tip, tip],
1726            &[tip, tip],
1727            &HashSet::new(),
1728            &CancelSignal::never(),
1729        )
1730        .unwrap();
1731        assert!(hidden.contains(&tip));
1732        assert!(
1733            commit_reads() <= 2,
1734            "shared want/have must stop at the frontier"
1735        );
1736        let empty = HashSet::new();
1737        let selection = enumerate_pack_objects(
1738            repo.odb(),
1739            &[tip, tip],
1740            &[tip, tip],
1741            &PackEnumerateOptions {
1742                filter: None,
1743                shallow_grafts: &empty,
1744                have_shallow_grafts: None,
1745                use_bitmaps: false,
1746                exclude_objects: None,
1747                peer_odb: None,
1748                have_boundary: HaveBoundary::TipsOnly,
1749            },
1750        )
1751        .unwrap();
1752        assert!(selection.send.is_empty());
1753    }
1754
1755    #[test]
1756    fn tips_only_many_advertised_haves_does_not_prewalk_deep_history() {
1757        use crate::merge_base::intersect_walk_test_hooks::{
1758            commit_parent_lookups, reset_commit_parent_lookups,
1759        };
1760
1761        let dir = TempDir::new().unwrap();
1762        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1763        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1764        let mut chain = Vec::new();
1765        let mut parent = write_commit(
1766            &repo,
1767            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1768            &[],
1769            0,
1770        );
1771        chain.push(parent);
1772        for i in 1..800 {
1773            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1774            parent = write_commit(&repo, tree, &[parent], i);
1775            chain.push(parent);
1776        }
1777        let remote_tip = parent;
1778        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1779        let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1780        let mut haves = vec![remote_tip];
1781        for n in (2..400).step_by(2) {
1782            let idx = chain.len().checked_sub(1 + n);
1783            if let Some(idx) = idx {
1784                haves.push(chain[idx]);
1785            }
1786        }
1787        let empty = HashSet::new();
1788        reset_commit_parent_lookups();
1789        let selection = enumerate_pack_objects(
1790            repo.odb(),
1791            &[new_tip],
1792            &haves,
1793            &PackEnumerateOptions {
1794                filter: None,
1795                shallow_grafts: &empty,
1796                have_shallow_grafts: Some(&empty),
1797                use_bitmaps: false,
1798                exclude_objects: None,
1799                peer_odb: None,
1800                have_boundary: HaveBoundary::TipsOnly,
1801            },
1802        )
1803        .expect("enumerate");
1804        assert!(
1805            selection.send.len() <= 8,
1806            "one new commit should stay small with many haves, got {}",
1807            selection.send.len()
1808        );
1809        assert!(
1810            commit_parent_lookups() <= 512,
1811            "many advertised haves must not pre-walk deep history ({} parent lookups)",
1812            commit_parent_lookups()
1813        );
1814    }
1815
1816    #[test]
1817    fn send_pack_have_walk_stops_without_scanning_deep_haves() {
1818        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1819
1820        let dir = TempDir::new().unwrap();
1821        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1822        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1823        let mut parent = write_commit(
1824            &repo,
1825            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1826            &[],
1827            0,
1828        );
1829        let near_root = parent;
1830        for i in 1..800 {
1831            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1832            parent = write_commit(&repo, tree, &[parent], i);
1833        }
1834        let direct_parent = parent;
1835        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1836        let new_tip = write_commit(&repo, new_tree, &[direct_parent], 900);
1837        let empty = HashSet::new();
1838        reset_commit_reads();
1839        let have_commits = send_pack_uninteresting_commits(
1840            repo.odb(),
1841            &[new_tip],
1842            &[direct_parent, near_root],
1843            &empty,
1844            &CancelSignal::never(),
1845        )
1846        .expect("send-pack have walk");
1847        assert!(have_commits.contains(&direct_parent));
1848        assert!(have_commits.contains(&near_root));
1849        let reads = commit_reads();
1850        assert!(
1851            reads < 20,
1852            "send-pack have walk should not scan deep history, read {reads} commits"
1853        );
1854    }
1855
1856    #[test]
1857    fn walk_lists_trees_of_newer_commits_first() {
1858        let dir = TempDir::new().unwrap();
1859        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1860        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1861        let tree_a = write_tree(&repo, &[(0o100644, "f", blob("a"))]);
1862        let tree_b = write_tree(&repo, &[(0o100644, "f", blob("b"))]);
1863        let tree_c = write_tree(&repo, &[(0o100644, "f", blob("c"))]);
1864        let base = write_commit(&repo, tree_a, &[], 100);
1865        // Two branches: the older tip is listed as the first root.
1866        let old_tip = write_commit(&repo, tree_b, &[base], 200);
1867        let new_tip = write_commit(&repo, tree_c, &[base], 300);
1868
1869        let selection = walk(&repo, &[old_tip, new_tip], NameTrees::No);
1870        let pos = |oid: ObjectId| selection.send.iter().position(|o| *o == oid).unwrap();
1871        assert!(pos(old_tip) < pos(base) && pos(new_tip) < pos(base));
1872        assert!(pos(tree_c) < pos(tree_b) && pos(tree_b) < pos(tree_a));
1873    }
1874
1875    #[test]
1876    fn walk_names_trees_and_keeps_history_paths_for_path_less_roots() {
1877        let dir = TempDir::new().unwrap();
1878        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1879        let file = repo.odb().write(ObjectKind::Blob, b"x\n").unwrap();
1880        let link = repo.odb().write(ObjectKind::Blob, b"target").unwrap();
1881        let sub = write_tree(
1882            &repo,
1883            &[(0o100644, "file.rs", file), (0o120000, "link", link)],
1884        );
1885        let root = write_tree(&repo, &[(0o040000, "src", sub)]);
1886        let commit = write_commit(&repo, root, &[], 100);
1887
1888        // Index roots (the subtree and the file) come first and have no path.
1889        let selection = walk(&repo, &[sub, file, commit], NameTrees::Yes);
1890        let hash = |oid: ObjectId| selection.name_hashes.get(&oid).copied();
1891        assert_eq!(hash(sub), Some(pack_name_hash("src")));
1892        assert_eq!(hash(file), Some(pack_name_hash("src/file.rs")));
1893        assert_eq!(hash(root), None, "root trees have no path");
1894        assert_eq!(selection.send.len(), 5);
1895
1896        let blobs_only = walk(&repo, &[sub, file, commit], NameTrees::No);
1897        assert_eq!(blobs_only.name_hashes.get(&sub), None);
1898        assert_eq!(
1899            blobs_only.name_hashes.get(&file),
1900            Some(&pack_name_hash("src/file.rs"))
1901        );
1902    }
1903
1904    fn tree_entry(mode: &str, name: &str, oid: ObjectId) -> Vec<u8> {
1905        let mut bytes = format!("{mode} {name}\0").into_bytes();
1906        bytes.extend_from_slice(oid.as_bytes());
1907        bytes
1908    }
1909
1910    /// Old tools wrote tree entries with non-canonical modes (`40755` for a
1911    /// directory, `100664` for a file). Git walks them by their type bits, so the
1912    /// enumeration must too, or everything below such a directory is left out.
1913    #[cfg_attr(
1914        not(feature = "full-tests"),
1915        ignore = "starts system git or is heavy; run with --features full-tests"
1916    )]
1917    #[test]
1918    fn walk_descends_into_trees_with_non_canonical_modes() {
1919        let tmp = tempfile::tempdir().unwrap();
1920        Repository::init(tmp.path(), &InitOptions::new().with_bare(true)).unwrap();
1921        let repo = Repository::open(tmp.path(), &RepositoryOptions::empty()).unwrap();
1922        let odb = repo.odb();
1923        let deep = odb
1924            .write_loose_materialize(ObjectKind::Blob, b"deep\n")
1925            .unwrap();
1926        let odd = odb
1927            .write_loose_materialize(ObjectKind::Blob, b"odd\n")
1928            .unwrap();
1929        let inner = odb
1930            .write_loose_materialize(ObjectKind::Tree, &tree_entry("100644", "deep.txt", deep))
1931            .unwrap();
1932        let mut root = tree_entry("100664", "odd.txt", odd);
1933        root.extend(tree_entry("40755", "sub", inner));
1934        let root = odb
1935            .write_loose_materialize(ObjectKind::Tree, &root)
1936            .unwrap();
1937        let commit = format!(
1938            "tree {root}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nodd modes\n"
1939        );
1940        let commit = odb
1941            .write_loose_materialize(ObjectKind::Commit, commit.as_bytes())
1942            .unwrap();
1943
1944        let empty = HashSet::new();
1945        let selection = enumerate_pack_objects(
1946            odb,
1947            &[commit],
1948            &[],
1949            &PackEnumerateOptions {
1950                filter: None,
1951                shallow_grafts: &empty,
1952                have_shallow_grafts: Some(&empty),
1953                use_bitmaps: false,
1954                exclude_objects: None,
1955                peer_odb: None,
1956                have_boundary: HaveBoundary::CommitClosure,
1957            },
1958        )
1959        .unwrap();
1960
1961        let ours: HashSet<ObjectId> = selection.send.iter().copied().collect();
1962        let git: HashSet<ObjectId> =
1963            grit_test_support::git(tmp.path(), &["rev-list", "--objects", &commit.to_hex()])
1964                .lines()
1965                .map(|line| ObjectId::from_hex(&line[..40]).unwrap())
1966                .collect();
1967        assert_eq!(ours, git, "same objects as git rev-list --objects");
1968        assert!(ours.contains(&inner) && ours.contains(&deep));
1969        assert_eq!(selection.send.len(), 5);
1970        assert_eq!(
1971            selection.name_hashes.get(&deep),
1972            Some(&pack_name_hash("sub/deep.txt"))
1973        );
1974        assert_eq!(
1975            selection.name_hashes.get(&odd),
1976            Some(&pack_name_hash("odd.txt"))
1977        );
1978    }
1979
1980    fn tips_only_options(empty: &HashSet<ObjectId>) -> PackEnumerateOptions<'_> {
1981        PackEnumerateOptions {
1982            filter: None,
1983            shallow_grafts: empty,
1984            have_shallow_grafts: Some(empty),
1985            use_bitmaps: false,
1986            exclude_objects: None,
1987            peer_odb: None,
1988            have_boundary: HaveBoundary::TipsOnly,
1989        }
1990    }
1991
1992    fn write_tag(repo: &Repository, target: ObjectId, kind: &str, name: &str) -> ObjectId {
1993        let body = format!(
1994            "object {}\ntype {kind}\ntag {name}\ntagger A <a@example.com> 0 +0000\n\n{name}\n",
1995            target.to_hex()
1996        );
1997        repo.odb().write(ObjectKind::Tag, body.as_bytes()).unwrap()
1998    }
1999
2000    #[test]
2001    fn tips_only_thin_bases_come_from_edge_commits_not_every_advertised_tip() {
2002        let dir = TempDir::new().unwrap();
2003        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2004        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2005        let base_tree = write_tree(&repo, &[(0o100644, "f", blob("base"))]);
2006        let root = write_commit(&repo, base_tree, &[], 0);
2007        let main = write_commit(
2008            &repo,
2009            write_tree(&repo, &[(0o100644, "f", blob("main"))]),
2010            &[root],
2011            10,
2012        );
2013        let mut haves = vec![main];
2014        for i in 0..40 {
2015            let tree = write_tree(&repo, &[(0o100644, "f", blob(&format!("side {i}")))]);
2016            let side = write_commit(&repo, tree, &[root], 20 + i);
2017            haves.push(write_tag(&repo, side, "commit", &format!("t{i}")));
2018            haves.push(side);
2019        }
2020        let new_tip = write_commit(
2021            &repo,
2022            write_tree(&repo, &[(0o100644, "f", blob("main\nmore"))]),
2023            &[main],
2024            100,
2025        );
2026        let empty = HashSet::new();
2027        let selection =
2028            enumerate_pack_objects(repo.odb(), &[new_tip], &haves, &tips_only_options(&empty))
2029                .unwrap();
2030        assert_eq!(selection.send.len(), 3, "commit, tree and the changed blob");
2031        assert_eq!(
2032            selection.have_closure,
2033            HashSet::from([main]),
2034            "thin bases come from the commit the walk stopped at"
2035        );
2036
2037        let closure = enumerate_pack_objects(
2038            repo.odb(),
2039            &[new_tip],
2040            &haves,
2041            &PackEnumerateOptions {
2042                have_boundary: HaveBoundary::CommitClosure,
2043                ..tips_only_options(&empty)
2044            },
2045        )
2046        .unwrap();
2047        assert_eq!(closure.send, selection.send);
2048        assert_eq!(
2049            closure.have_closure,
2050            HashSet::from([main]),
2051            "the full have pre-walk seeds thin bases from the same edge commit"
2052        );
2053    }
2054
2055    #[test]
2056    fn send_pack_walk_peels_tag_haves_and_ignores_non_commit_targets() {
2057        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2058        let dir = TempDir::new().unwrap();
2059        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2060        let blob = repo.odb().write(ObjectKind::Blob, b"x").unwrap();
2061        let tree = write_tree(&repo, &[(0o100644, "f", blob)]);
2062        let old = write_commit(&repo, tree, &[], 0);
2063        let tip = write_commit(&repo, tree, &[old], 1);
2064        let tag = write_tag(&repo, tip, "commit", "v1");
2065        let nested = write_tag(&repo, tag, "tag", "v1-nested");
2066        let tree_tag = write_tag(&repo, tree, "tree", "tree-tag");
2067        let blob_tag = write_tag(&repo, blob, "blob", "blob-tag");
2068        let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
2069        let new_tip = write_commit(&repo, tree, &[tip], 2);
2070
2071        reset_commit_reads();
2072        let hidden = send_pack_uninteresting_commits(
2073            repo.odb(),
2074            &[new_tip],
2075            &[nested, tag, tree_tag, blob_tag, missing],
2076            &HashSet::new(),
2077            &CancelSignal::never(),
2078        )
2079        .unwrap();
2080        assert!(hidden.contains(&tip), "tags peel to the commit they name");
2081        for oid in [
2082            tag, nested, tree_tag, blob_tag, tree, blob, missing, new_tip,
2083        ] {
2084            assert!(!hidden.contains(&oid), "{oid} is not a have-side commit");
2085        }
2086        assert_eq!(
2087            commit_reads(),
2088            2,
2089            "the want and the tag-named commit, each queued once"
2090        );
2091    }
2092}