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    tree_entry_span, CommitRef, Object, ObjectId, ObjectKind, OidHashMap, OidHashSet, TreeEntryRef,
15    TreeRefIter,
16};
17use crate::odb::store::{CompositeStore, ObjectStore};
18use crate::odb::Odb;
19use crate::pack::SingleLocalPackReader;
20use crate::pack_bitmap::BitmapIndex;
21use crate::pack_name_hash::{pack_name_hash, pack_name_hash_child};
22use crate::pack_tree_read_ahead::{ReadAheadEntry, TreeReadAhead};
23use crate::progress::CancelSignal;
24use crate::repo::Repository;
25use crate::rev_list::{MissingAction, ObjectFilter};
26
27/// Objects to emit in a negotiated pack plus peer-held closure for thin deltas.
28#[derive(Clone)]
29#[allow(clippy::type_complexity)]
30pub struct PackObjectSelection {
31    /// Objects reachable from `wants` but not from `haves`, in valid pack order.
32    pub send: Vec<ObjectId>,
33    /// Peer-held commits whose trees seed thin-pack delta bases, not the full peer
34    /// object closure: the edge commits where the object walk reached the peer's
35    /// history, or the negotiation `haves` when bitmaps (or a path filter) selected
36    /// the objects instead.
37    pub have_closure: HashSet<ObjectId>,
38    /// Peer-held objects when pack reachability bitmaps are available (thin-pack bases).
39    pub peer_held: Option<Arc<ReachableSet>>,
40    /// Pack name hash of the path each object was found at, used to pair delta
41    /// candidates: blobs from the tree walk, every object the bitmap name-hash
42    /// cache covers when bitmaps are used.
43    pub name_hashes: HashMap<ObjectId, u32>,
44}
45
46impl fmt::Debug for PackObjectSelection {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.debug_struct("PackObjectSelection")
49            .field("send", &self.send)
50            .field("have_closure", &self.have_closure)
51            .field(
52                "peer_held",
53                &self
54                    .peer_held
55                    .as_ref()
56                    .map(|held| held.count())
57                    .unwrap_or(0),
58            )
59            .field("name_hashes", &self.name_hashes)
60            .finish()
61    }
62}
63
64/// How client `haves` bound the want-side object walk.
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
66pub enum HaveBoundary {
67    /// Pre-walk all commits reachable from every `have` before the want walk.
68    #[default]
69    CommitClosure,
70    /// Git's `--revs` / `^have` walk, used by send-pack and upload-pack: peel have
71    /// tips, mark them uninteresting, and interleave a commit-date walk with wants
72    /// until the queue is only uninteresting commits (no full have closure pre-walk).
73    TipsOnly,
74}
75
76/// Parameters for [`enumerate_pack_objects`].
77#[derive(Clone, Copy, Debug)]
78pub struct PackEnumerateOptions<'a> {
79    /// Partial-clone filter applied to the want side (when supported).
80    pub filter: Option<&'a ObjectFilter>,
81    /// Shallow graft boundaries on the want side: do not walk past these commits
82    /// when enumerating objects to send. When non-empty, bitmap enumeration is
83    /// skipped and the object walk is used.
84    pub shallow_grafts: &'a HashSet<ObjectId>,
85    /// Shallow graft boundaries when walking client `haves` to build the peer
86    /// closure. Defaults to [`Self::shallow_grafts`] when unset. When non-empty,
87    /// bitmaps are not used: a bitmap closure of a `have` runs past the client's
88    /// shallow boundary into history the client does not hold.
89    pub have_shallow_grafts: Option<&'a HashSet<ObjectId>>,
90    /// When false, always use the object walk even if bitmaps exist.
91    pub use_bitmaps: bool,
92    /// Extra negotiation boundaries merged into the walk (legacy local-fetch hints).
93    pub exclude_objects: Option<&'a HashSet<ObjectId>>,
94    /// When `wants` are read from another ODB, objects already stored here are boundaries.
95    pub peer_odb: Option<&'a Odb>,
96    /// Whether `haves` are expanded to a full commit closure before the want walk.
97    pub have_boundary: HaveBoundary,
98}
99
100impl PackEnumerateOptions<'_> {
101    /// Whether any shallow graft bounds either side of the walk, which rules out
102    /// bitmap reachability (bitmaps record full, ungrafted closures).
103    fn has_shallow_grafts(&self) -> bool {
104        !self.shallow_grafts.is_empty() || self.have_shallow_grafts.is_some_and(|g| !g.is_empty())
105    }
106}
107
108/// Select pack objects using bitmap reachability when possible, otherwise the object walk.
109///
110/// # Errors
111///
112/// Propagates repository read failures. Missing `want` objects error; missing `have` objects are skipped.
113pub fn enumerate_pack_objects(
114    odb: &Odb,
115    wants: &[ObjectId],
116    haves: &[ObjectId],
117    opts: &PackEnumerateOptions<'_>,
118) -> Result<PackObjectSelection> {
119    enumerate_pack_objects_naming(
120        odb,
121        wants,
122        haves,
123        opts,
124        NameTrees::No,
125        false,
126        &CancelSignal::never(),
127    )
128}
129
130/// Whether the object walk records name hashes for trees as well as blobs.
131#[derive(Clone, Copy, PartialEq, Eq)]
132pub(crate) enum NameTrees {
133    /// Blobs only: the packers that pair only blobs skip the extra entries.
134    No,
135    /// Blobs and trees, for a delta search that pairs trees too.
136    Yes,
137}
138
139impl NameTrees {
140    /// [`Self::Yes`] when a pack built with `opts` runs a new delta search, or is thin
141    /// (receiver-held bases are matched to sent trees by path).
142    pub(crate) fn for_delta_search(opts: &crate::pack_objects::PackBuildOptions) -> Self {
143        if opts.delta && (opts.thin || (opts.window > 0 && opts.max_depth > 0)) {
144            Self::Yes
145        } else {
146            Self::No
147        }
148    }
149}
150
151/// Like [`enumerate_pack_objects`], choosing whether the walk names trees and
152/// stopping with [`crate::error::Error::Cancelled`] once `stop` fires (polled for
153/// every commit and object the walk reads).
154pub(crate) fn enumerate_pack_objects_naming(
155    odb: &Odb,
156    wants: &[ObjectId],
157    haves: &[ObjectId],
158    opts: &PackEnumerateOptions<'_>,
159    name_trees: NameTrees,
160    local_only: bool,
161    stop: &CancelSignal<'_>,
162) -> Result<PackObjectSelection> {
163    crate::trace::trace_span!("grit.pack.enumerate");
164    stop.poll()?;
165    if !local_only && opts.use_bitmaps {
166        if let Some(sel) = try_bitmap_enumeration(odb, wants, haves, opts)? {
167            stop.poll()?;
168            return Ok(sel);
169        }
170    }
171    let alternate_sources = if local_only {
172        AlternateSources::load(odb)?.composite()
173    } else {
174        None
175    };
176    walk_enumeration(
177        odb,
178        wants,
179        haves,
180        opts,
181        name_trees,
182        alternate_sources,
183        MissingBlobMode::Fail,
184        stop,
185    )
186    .map(|(selection, _)| selection)
187}
188
189/// What the object walk does with a blob the repository does not store.
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub(crate) enum MissingBlobMode {
192    /// List it anyway; the pack writer fails when it reads it (the plain walk).
193    Fail,
194    /// Check that every blob it would list is stored, and set the missing ones
195    /// aside as [`MissingBlob`]s instead of listing them.
196    Probe,
197}
198
199/// A blob the walk would have sent but the repository does not store.
200#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201pub(crate) struct MissingBlob {
202    /// The missing blob.
203    pub(crate) oid: ObjectId,
204    /// The tree the walk found it in, when known.
205    pub(crate) referrer: Option<ObjectId>,
206}
207
208/// Like [`enumerate_pack_objects_naming`] with [`MissingBlobMode::Probe`]: the object
209/// walk always runs (bitmaps cannot see missing objects), and blobs the repository
210/// lacks come back separately instead of being listed.
211///
212/// Missing commits, trees and tags still fail the walk.
213pub(crate) fn enumerate_pack_objects_probing(
214    odb: &Odb,
215    wants: &[ObjectId],
216    haves: &[ObjectId],
217    opts: &PackEnumerateOptions<'_>,
218    name_trees: NameTrees,
219    stop: &CancelSignal<'_>,
220) -> Result<(PackObjectSelection, Vec<MissingBlob>)> {
221    stop.poll()?;
222    let opts = PackEnumerateOptions {
223        use_bitmaps: false,
224        ..*opts
225    };
226    walk_enumeration(
227        odb,
228        wants,
229        haves,
230        &opts,
231        name_trees,
232        None,
233        MissingBlobMode::Probe,
234        stop,
235    )
236}
237
238/// Bitmap reachability for upload-pack when shallow grafts and filters allow it.
239///
240/// Returns the send-side [`ReachableSet`] and the peer-held [`ReachableSet`].
241pub fn try_bitmap_reachable_for_upload(
242    odb: &Odb,
243    wants: &[ObjectId],
244    haves: &[ObjectId],
245    filter: Option<&ObjectFilter>,
246    shallow_grafts: &HashSet<ObjectId>,
247) -> Result<Option<(ReachableSet, ReachableSet)>> {
248    if !shallow_grafts.is_empty() || filter.is_some_and(selects_object_type) {
249        return Ok(None);
250    }
251    let Some(git_dir) = odb.config_git_dir() else {
252        return Ok(None);
253    };
254    let repo = match Repository::open(git_dir, &RepositoryOptions::empty()) {
255        Ok(r) => r,
256        Err(_) => return Ok(None),
257    };
258    let Some(index) = BitmapIndex::open(&repo).ok().flatten() else {
259        return Ok(None);
260    };
261    let index = Arc::new(index);
262    match index.reachability_and_haves(
263        &repo,
264        ReachabilityQuery {
265            wants,
266            haves,
267            filter,
268        },
269        MissingAction::Error,
270        MissingAction::Allow,
271    ) {
272        Ok(sets) => Ok(Some(sets)),
273        Err(BitmapWalkError::Unsupported(_)) => Ok(None),
274        Err(BitmapWalkError::Bitmap(_)) => Ok(None),
275        Err(BitmapWalkError::Repository(e)) => Err(e),
276    }
277}
278
279/// Bitmap `object:type` selection also drops the wanted commits and tags, which a
280/// pack must always carry, so those filters use the object walk.
281fn selects_object_type(filter: &ObjectFilter) -> bool {
282    match filter {
283        ObjectFilter::ObjectType(_) => true,
284        ObjectFilter::Combine(parts) => parts.iter().any(selects_object_type),
285        _ => false,
286    }
287}
288
289fn try_bitmap_enumeration(
290    odb: &Odb,
291    wants: &[ObjectId],
292    haves: &[ObjectId],
293    opts: &PackEnumerateOptions<'_>,
294) -> Result<Option<PackObjectSelection>> {
295    if opts.has_shallow_grafts() {
296        return Ok(None);
297    }
298    let Some((send_set, peer_held)) =
299        try_bitmap_reachable_for_upload(odb, wants, haves, opts.filter, opts.shallow_grafts)?
300    else {
301        return Ok(None);
302    };
303    let index = send_set.bitmap_index();
304    let send: Vec<ObjectId> = send_set.iter_grouped_by_kind().collect();
305    let have_closure: HashSet<ObjectId> = haves.iter().copied().collect();
306    let name_hashes = name_hashes_from_bitmap(index, &send, &HashSet::new());
307    Ok(Some(PackObjectSelection {
308        send,
309        have_closure,
310        peer_held: Some(Arc::new(peer_held)),
311        name_hashes,
312    }))
313}
314
315pub(crate) fn name_hashes_for_send(
316    index: &BitmapIndex,
317    send: &[ObjectId],
318    have_closure: &HashSet<ObjectId>,
319) -> HashMap<ObjectId, u32> {
320    name_hashes_from_bitmap(index, send, have_closure)
321}
322
323fn name_hashes_from_bitmap(
324    index: &BitmapIndex,
325    send: &[ObjectId],
326    have_closure: &HashSet<ObjectId>,
327) -> HashMap<ObjectId, u32> {
328    let mut out = HashMap::new();
329    for oid in send.iter().chain(have_closure.iter()) {
330        let Some(pos) = index.position_of(oid) else {
331            continue;
332        };
333        if let Some(h) = index.name_hash(pos) {
334            out.insert(*oid, h);
335        }
336    }
337    out
338}
339
340#[allow(clippy::too_many_arguments)]
341fn walk_enumeration(
342    odb: &Odb,
343    wants: &[ObjectId],
344    haves: &[ObjectId],
345    opts: &PackEnumerateOptions<'_>,
346    name_trees: NameTrees,
347    alternate_sources: Option<Arc<CompositeStore>>,
348    missing_mode: MissingBlobMode,
349    stop: &CancelSignal<'_>,
350) -> Result<(PackObjectSelection, Vec<MissingBlob>)> {
351    let have_shallow = opts.have_shallow_grafts.unwrap_or(opts.shallow_grafts);
352    let have_commits = match opts.have_boundary {
353        HaveBoundary::CommitClosure => {
354            commits_reachable_from_haves(odb, haves, have_shallow, true, stop)?
355        }
356        HaveBoundary::TipsOnly => {
357            send_pack_uninteresting_commits(odb, wants, haves, have_shallow, stop)?
358        }
359    };
360    let mut boundary: HashSet<ObjectId> = HashSet::new();
361    if let Some(extra) = opts.exclude_objects {
362        boundary.extend(extra.iter().copied());
363    }
364    let peer_held = try_peer_held_bitmap(odb, haves, opts)?;
365    stop.poll()?;
366    let mut missing = Vec::new();
367    let (send, name_hashes, edges) = match opts.filter {
368        Some(filter) if filter.uses_paths() => {
369            let (mut send, name_hashes) = rev_list_filtered_objects(
370                odb,
371                wants,
372                haves,
373                filter,
374                &have_commits,
375                missing_mode,
376                stop,
377            )?;
378            if missing_mode == MissingBlobMode::Probe {
379                missing = take_unstored_blobs(odb, &mut send)?;
380            }
381            (send, name_hashes, None)
382        }
383        filter => {
384            let walked = collect_reachable_excluding_with_name_hashes(
385                odb,
386                wants,
387                &HashSet::new(),
388                &mut boundary,
389                &have_commits,
390                opts.peer_odb,
391                alternate_sources,
392                false,
393                opts.shallow_grafts,
394                filter,
395                name_trees,
396                missing_mode,
397                stop,
398            )?;
399            missing = walked.missing;
400            (walked.send, walked.name_hashes, Some(walked.edges))
401        }
402    };
403    // Thin bases come only from the edge commits where the walk met the peer's
404    // history, as Git's `--thin` does, not from every advertised tip.
405    let have_closure = match edges {
406        Some(edges) => edges,
407        None => haves.iter().copied().collect(),
408    };
409    Ok((
410        PackObjectSelection {
411            send,
412            have_closure,
413            peer_held,
414            name_hashes,
415        },
416        missing,
417    ))
418}
419
420/// Remove the objects `send` lists that the repository does not store and return
421/// them as missing blobs. Path-filtered walks (`sparse:oid`) only ever list
422/// commits and trees the walk has read, so anything absent is a blob.
423fn take_unstored_blobs(odb: &Odb, send: &mut Vec<ObjectId>) -> Result<Vec<MissingBlob>> {
424    let present = odb.contains_many(send)?;
425    let mut missing = Vec::new();
426    let mut present = present.into_iter();
427    send.retain(|oid| {
428        let keep = present.next().unwrap_or(true);
429        if !keep {
430            missing.push(MissingBlob {
431                oid: *oid,
432                referrer: None,
433            });
434        }
435        keep
436    });
437    Ok(missing)
438}
439
440/// An annotated tag ref that `include-tag` may add to a fetch pack.
441#[derive(Clone, Copy, Debug, PartialEq, Eq)]
442pub(crate) struct TagCandidate {
443    /// The tag object the ref names.
444    pub(crate) tag: ObjectId,
445    /// The non-tag object the tag chain ends at (the advertised `^{}` value).
446    pub(crate) peeled: ObjectId,
447}
448
449/// Append to `send` every candidate tag whose peeled object `send` already holds,
450/// with each tag object of its chain, the way `pack-objects --include-tag` does.
451///
452/// Cost follows the size of `send` and the number of candidates, never the history
453/// behind them: `send` is scanned once (a second time only when a chain passes
454/// through a tag no candidate names) and only the added tags are read.
455///
456/// # Parameters
457///
458/// - `odb`: the database the added tag objects are read from.
459/// - `send`: the objects the pack holds; added tags are appended after them.
460/// - `candidates`: the annotated tag refs the client may follow.
461///
462/// # Errors
463///
464/// [`Error::ObjectNotFound`] or [`Error::CorruptObject`] for an unreadable tag in a
465/// chain that is added.
466pub(crate) fn append_included_tags(
467    odb: &Odb,
468    send: &mut Vec<ObjectId>,
469    candidates: &[TagCandidate],
470) -> Result<()> {
471    if candidates.is_empty() {
472        return Ok(());
473    }
474    let mut named = oid_hash_set_with_capacity(candidates.len() * 2);
475    for c in candidates {
476        named.insert(c.tag);
477        named.insert(c.peeled);
478    }
479    let present: OidHashSet = send
480        .iter()
481        .filter(|o| named.contains(*o))
482        .copied()
483        .collect();
484    let mut added = included_tag_chains(odb, candidates, |oid| present.contains(oid))?;
485    let unnamed: OidHashSet = added
486        .iter()
487        .filter(|o| !named.contains(*o))
488        .copied()
489        .collect();
490    if !unnamed.is_empty() {
491        let already_sent: OidHashSet = send
492            .iter()
493            .filter(|o| unnamed.contains(*o))
494            .copied()
495            .collect();
496        added.retain(|oid| !already_sent.contains(oid));
497    }
498    send.extend(added);
499    Ok(())
500}
501
502/// The tag objects `include-tag` adds for `candidates` to a pack whose membership
503/// `in_pack` answers.
504///
505/// A candidate qualifies when its peeled object is in the pack and its tag is not;
506/// its whole chain (tag of a tag) is then returned, minus objects already packed.
507/// `in_pack` must be exact for candidate tags and peeled ids; for unnamed chain
508/// tags a `false` only means the caller has to deduplicate them.
509///
510/// # Errors
511///
512/// [`Error::ObjectNotFound`] or [`Error::CorruptObject`] for an unreadable tag in a
513/// qualifying chain.
514pub(crate) fn included_tag_chains(
515    odb: &Odb,
516    candidates: &[TagCandidate],
517    in_pack: impl Fn(&ObjectId) -> bool,
518) -> Result<Vec<ObjectId>> {
519    let mut seen = oid_hash_set();
520    let mut out = Vec::new();
521    for c in candidates {
522        if !in_pack(&c.peeled) || in_pack(&c.tag) {
523            continue;
524        }
525        let mut current = c.tag;
526        while current != c.peeled && seen.insert(current) {
527            if !in_pack(&current) {
528                out.push(current);
529            }
530            let tag = odb.read(&current)?;
531            current = tag_object_line_oid(&tag.data).ok_or_else(|| {
532                Error::CorruptObject(format!("tag {current} has no valid object header"))
533            })?;
534        }
535    }
536    Ok(out)
537}
538
539const SEND_PACK_REACH_WANT: u8 = 1;
540const SEND_PACK_REACH_HAVE: u8 = 2;
541
542#[cfg(test)]
543pub(crate) mod send_pack_walk_test_hooks {
544    use std::cell::Cell;
545
546    thread_local! {
547        static COMMIT_READS: Cell<usize> = const { Cell::new(0) };
548    }
549
550    pub fn reset_commit_reads() {
551        COMMIT_READS.with(|c| c.set(0));
552    }
553
554    pub fn commit_reads() -> usize {
555        COMMIT_READS.with(|c| c.get())
556    }
557
558    pub(crate) fn record_commit_read() {
559        COMMIT_READS.with(|c| c.set(c.get().saturating_add(1)));
560    }
561}
562
563fn send_pack_reach_uninteresting(reach: u8) -> bool {
564    reach & SEND_PACK_REACH_HAVE != 0
565}
566
567fn send_pack_reach_interesting(reach: u8) -> bool {
568    reach & SEND_PACK_REACH_WANT != 0 && !send_pack_reach_uninteresting(reach)
569}
570
571/// Commits marked uninteresting for send-pack (`--revs` with `^have` lines).
572///
573/// Peels annotated tags on haves (skips missing ids and tags that peel to non-commits),
574/// seeds wants and haves on a committer-date heap, and walks parents until every queued
575/// commit is uninteresting or the heap is empty.
576fn send_pack_uninteresting_commits(
577    odb: &Odb,
578    wants: &[ObjectId],
579    haves: &[ObjectId],
580    shallow_grafts: &HashSet<ObjectId>,
581    stop: &CancelSignal<'_>,
582) -> Result<HashSet<ObjectId>> {
583    // Only a have makes a commit uninteresting; without one the walk below would read
584    // every commit reachable from the wants to find none.
585    if haves.is_empty() {
586        return Ok(HashSet::new());
587    }
588    struct SendPackRevWalk<'a> {
589        odb: &'a Odb,
590        graph: Option<Arc<CommitGraphChain>>,
591        shallow_grafts: &'a HashSet<ObjectId>,
592        reach: HashMap<ObjectId, u8>,
593        heap: BinaryHeap<QueuedCommit>,
594        on_heap: HashSet<ObjectId>,
595        processed: HashSet<ObjectId>,
596        uninteresting: HashSet<ObjectId>,
597        interesting_on_heap: usize,
598        next_seq: u64,
599        header_buf: Vec<u8>,
600    }
601
602    impl<'a> SendPackRevWalk<'a> {
603        fn note_reach(&mut self, oid: ObjectId, flag: u8) {
604            let entry = self.reach.entry(oid).or_insert(0);
605            let was_interesting = send_pack_reach_interesting(*entry);
606            *entry |= flag;
607            let is_interesting = send_pack_reach_interesting(*entry);
608            if self.on_heap.contains(&oid) {
609                match (was_interesting, is_interesting) {
610                    (false, true) => self.interesting_on_heap += 1,
611                    (true, false) => self.interesting_on_heap -= 1,
612                    _ => {}
613                }
614            }
615            if send_pack_reach_uninteresting(*entry) {
616                self.uninteresting.insert(oid);
617            }
618        }
619
620        /// Stop once no queued commit is still want-only (the interesting frontier).
621        ///
622        /// Unlike waiting for every uninteresting commit's parents to be marked, this
623        /// matches Git's `still_interesting` cutoff and avoids walking all haves to root.
624        fn can_stop_after(&self) -> bool {
625            // Counting reach transitions avoids revisiting every queued have after each
626            // commit, especially when a new commit is dated before advertised history.
627            !self.heap.is_empty() && self.interesting_on_heap == 0
628        }
629
630        /// Note `oid` as reached with `flag`; true when it still has to be read and queued.
631        fn reach_unqueued(&mut self, oid: ObjectId, flag: u8) -> bool {
632            self.note_reach(oid, flag);
633            !(self.processed.contains(&oid)
634                || self.on_heap.contains(&oid)
635                || self.shallow_grafts.contains(&oid))
636        }
637
638        fn try_enqueue(&mut self, oid: ObjectId, flag: u8) -> Result<()> {
639            if !self.reach_unqueued(oid, flag) {
640                return Ok(());
641            }
642            if self.queue_from_graph(oid) {
643                return Ok(());
644            }
645            // Only the headers matter here, and without a commit-graph this walk can read
646            // most of the history (new commits dated before the peer's tips), so skip
647            // inflating messages wherever the pack stores the commit whole.
648            let mut header = std::mem::take(&mut self.header_buf);
649            let queued = if matches!(
650                self.odb
651                    .read_packed_commit_header_block_into(&oid, &mut header),
652                Ok(true)
653            ) {
654                self.queue_commit_data(oid, &header)
655            } else {
656                match self.odb.read(&oid) {
657                    Ok(obj) => self.queue_commit(oid, &obj),
658                    Err(_) => Ok(()),
659                }
660            };
661            self.header_buf = header;
662            queued
663        }
664
665        /// Queue the commit a ref tip names, following annotated tags.
666        ///
667        /// Each object of the chain is read once; missing objects and tags that end at a
668        /// tree or blob are skipped.
669        fn enqueue_tip(&mut self, tip: ObjectId, flag: u8) -> Result<()> {
670            let mut oid = tip;
671            let mut tags_seen: Option<HashSet<ObjectId>> = None;
672            loop {
673                if self.graph_lists(&oid) {
674                    if self.reach_unqueued(oid, flag) {
675                        self.queue_from_graph(oid);
676                    }
677                    return Ok(());
678                }
679                // A want is often a blob (a lazy fetch names thousands): the header
680                // says so without inflating it.
681                match self.odb.read_info(&oid) {
682                    Ok(info) if matches!(info.kind, ObjectKind::Commit | ObjectKind::Tag) => {}
683                    _ => return Ok(()),
684                }
685                let Ok(obj) = self.odb.read(&oid) else {
686                    return Ok(());
687                };
688                if obj.kind != ObjectKind::Tag {
689                    if obj.kind != ObjectKind::Commit || !self.reach_unqueued(oid, flag) {
690                        return Ok(());
691                    }
692                    return self.queue_commit(oid, &obj);
693                }
694                if !tags_seen.get_or_insert_with(HashSet::new).insert(oid) {
695                    return Ok(());
696                }
697                let Some(target) = tag_object_line_oid(&obj.data) else {
698                    return Ok(());
699                };
700                if self.processed.contains(&target) || self.on_heap.contains(&target) {
701                    self.note_reach(target, flag);
702                    return Ok(());
703                }
704                oid = target;
705            }
706        }
707
708        fn graph_lists(&self, oid: &ObjectId) -> bool {
709            self.graph
710                .as_deref()
711                .is_some_and(|g| g.find_commit(oid).is_some())
712        }
713
714        /// Queue `oid` from the commit-graph without reading the object; false when the
715        /// graph does not list it.
716        fn queue_from_graph(&mut self, oid: ObjectId) -> bool {
717            let Some(entry) = self.graph.as_deref().and_then(|g| g.graph_walk_entry(&oid)) else {
718                return false;
719            };
720            self.push_queued(QueuedCommit {
721                generation: entry.generation,
722                time: entry.time,
723                seq: std::cmp::Reverse(self.next_seq),
724                oid,
725                tree: entry.tree,
726                parents: entry.parents,
727            });
728            true
729        }
730
731        fn queue_commit(&mut self, oid: ObjectId, obj: &Object) -> Result<()> {
732            if obj.kind != ObjectKind::Commit {
733                return Ok(());
734            }
735            self.queue_commit_data(oid, &obj.data)
736        }
737
738        /// Queue commit `oid` from its payload, or from just its header block.
739        fn queue_commit_data(&mut self, oid: ObjectId, data: &[u8]) -> Result<()> {
740            let parsed = CommitRef::parse(data)?;
741            #[cfg(test)]
742            send_pack_walk_test_hooks::record_commit_read();
743            self.push_queued(QueuedCommit {
744                generation: UNRANKED_GENERATION,
745                time: parsed.committer_unix_seconds_for_ordering(),
746                seq: std::cmp::Reverse(self.next_seq),
747                oid,
748                tree: parsed.tree(),
749                parents: parsed.parents().to_vec(),
750            });
751            Ok(())
752        }
753
754        fn push_queued(&mut self, queued: QueuedCommit) {
755            self.on_heap.insert(queued.oid);
756            if send_pack_reach_interesting(*self.reach.get(&queued.oid).unwrap_or(&0)) {
757                self.interesting_on_heap += 1;
758            }
759            self.heap.push(queued);
760            self.next_seq += 1;
761        }
762    }
763
764    // Commits the graph lists come from it without inflating the object, and are ordered by
765    // generation: commits outside the graph (new local work) are expanded first, so a push whose
766    // commits are dated before the remote's history does not drain that whole history before
767    // reaching them. Shallow repositories keep reading commits, since the graph does not know
768    // their grafted boundaries.
769    let graph = if shallow_grafts.is_empty() {
770        odb.commit_graph_chain()
771    } else {
772        None
773    };
774    let mut walk = SendPackRevWalk {
775        odb,
776        graph,
777        shallow_grafts,
778        reach: HashMap::new(),
779        heap: BinaryHeap::new(),
780        on_heap: HashSet::new(),
781        processed: HashSet::new(),
782        uninteresting: HashSet::new(),
783        interesting_on_heap: 0,
784        next_seq: 0,
785        header_buf: Vec::new(),
786    };
787
788    for &want in wants {
789        walk.enqueue_tip(want, SEND_PACK_REACH_WANT)?;
790    }
791    for &have in haves {
792        walk.enqueue_tip(have, SEND_PACK_REACH_HAVE)?;
793    }
794
795    while let Some(commit) = walk.heap.pop() {
796        stop.poll()?;
797        walk.on_heap.remove(&commit.oid);
798        if send_pack_reach_interesting(*walk.reach.get(&commit.oid).unwrap_or(&0)) {
799            walk.interesting_on_heap -= 1;
800        }
801        if !walk.processed.insert(commit.oid) {
802            continue;
803        }
804        let flags = *walk.reach.get(&commit.oid).unwrap_or(&0);
805        if send_pack_reach_uninteresting(flags) {
806            walk.uninteresting.insert(commit.oid);
807        }
808        let parent_flag = if send_pack_reach_uninteresting(flags) {
809            SEND_PACK_REACH_HAVE
810        } else {
811            SEND_PACK_REACH_WANT
812        };
813        if !walk.shallow_grafts.contains(&commit.oid) {
814            for parent in commit.parents {
815                walk.try_enqueue(parent, parent_flag)?;
816            }
817        }
818        if walk.can_stop_after() {
819            break;
820        }
821    }
822    Ok(walk.uninteresting)
823}
824
825/// Commits reachable from client `haves` (commit graph only, no trees).
826fn commits_reachable_from_haves(
827    odb: &Odb,
828    haves: &[ObjectId],
829    shallow_grafts: &HashSet<ObjectId>,
830    skip_missing: bool,
831    stop: &CancelSignal<'_>,
832) -> Result<HashSet<ObjectId>> {
833    let mut seen = HashSet::new();
834    let mut queue: VecDeque<ObjectId> = haves.iter().copied().collect();
835    while let Some(oid) = queue.pop_front() {
836        stop.poll()?;
837        if !seen.insert(oid) {
838            continue;
839        }
840        if shallow_grafts.contains(&oid) {
841            continue;
842        }
843        let obj = match odb.read(&oid) {
844            Ok(o) => o,
845            Err(_) if skip_missing => continue,
846            Err(e) => return Err(e),
847        };
848        if obj.kind != ObjectKind::Commit {
849            continue;
850        }
851        let parsed = CommitRef::parse(&obj.data)?;
852        for parent in parsed.parents() {
853            if !seen.contains(parent) {
854                queue.push_back(*parent);
855            }
856        }
857    }
858    Ok(seen)
859}
860
861/// Objects in a commit's root tree (the commit, its trees and blobs), for negotiation boundaries.
862fn objects_in_commit_snapshot(odb: &Odb, commit_oid: ObjectId) -> Result<HashSet<ObjectId>> {
863    let mut out = HashSet::new();
864    out.insert(commit_oid);
865    let commit_obj = match odb.read(&commit_oid) {
866        Ok(o) => o,
867        Err(_) => return Ok(out),
868    };
869    if commit_obj.kind != ObjectKind::Commit {
870        return Ok(out);
871    }
872    let commit = CommitRef::parse(&commit_obj.data)?;
873    let oid_len = odb.hash_algo().len();
874    let mut queue = VecDeque::from([commit.tree()]);
875    while let Some(tree_oid) = queue.pop_front() {
876        if !out.insert(tree_oid) {
877            continue;
878        }
879        let tree_obj = match odb.read(&tree_oid) {
880            Ok(o) => o,
881            Err(_) => continue,
882        };
883        for entry in TreeRefIter::with_oid_len(&tree_obj.data, oid_len) {
884            let entry = entry?;
885            if entry.mode == 0o160000 {
886                continue;
887            }
888            if entry.mode & 0o170000 == 0o040000 {
889                queue.push_back(entry.oid);
890            } else {
891                out.insert(entry.oid);
892            }
893        }
894    }
895    Ok(out)
896}
897
898fn try_peer_held_bitmap(
899    odb: &Odb,
900    haves: &[ObjectId],
901    opts: &PackEnumerateOptions<'_>,
902) -> Result<Option<Arc<ReachableSet>>> {
903    if haves.is_empty() || !opts.use_bitmaps || opts.has_shallow_grafts() {
904        return Ok(None);
905    }
906    let Some(peer) = try_bitmap_peer_held(odb, haves, opts.shallow_grafts)? else {
907        return Ok(None);
908    };
909    Ok(Some(Arc::new(peer)))
910}
911
912fn try_bitmap_peer_held(
913    odb: &Odb,
914    haves: &[ObjectId],
915    shallow_grafts: &HashSet<ObjectId>,
916) -> Result<Option<ReachableSet>> {
917    if !shallow_grafts.is_empty() {
918        return Ok(None);
919    }
920    let Some(git_dir) = odb.config_git_dir() else {
921        return Ok(None);
922    };
923    let repo = match Repository::open(git_dir, &RepositoryOptions::empty()) {
924        Ok(r) => r,
925        Err(_) => return Ok(None),
926    };
927    let Some(index) = BitmapIndex::open(&repo).ok().flatten() else {
928        return Ok(None);
929    };
930    let index = Arc::new(index);
931    match index.reachability(
932        &repo,
933        ReachabilityQuery {
934            wants: haves,
935            haves: &[],
936            filter: None,
937        },
938        MissingAction::Allow,
939    ) {
940        Ok(set) => Ok(Some(set)),
941        Err(BitmapWalkError::Unsupported(_)) => Ok(None),
942        Err(BitmapWalkError::Bitmap(_)) => Ok(None),
943        Err(BitmapWalkError::Repository(e)) => Err(e),
944    }
945}
946
947/// Objects for a path-based filter (`sparse:oid`), which needs the full object walk.
948fn rev_list_filtered_objects(
949    odb: &Odb,
950    wants: &[ObjectId],
951    haves: &[ObjectId],
952    filter: &ObjectFilter,
953    have_commits: &HashSet<ObjectId>,
954    missing_mode: MissingBlobMode,
955    stop: &CancelSignal<'_>,
956) -> Result<(Vec<ObjectId>, HashMap<ObjectId, u32>)> {
957    let _ = have_commits;
958    stop.poll()?;
959    let Some(git_dir) = odb.config_git_dir() else {
960        return Ok((Vec::new(), HashMap::new()));
961    };
962    let repo = Repository::open(git_dir, &RepositoryOptions::empty())?;
963    let wants: Vec<String> = wants.iter().map(ObjectId::to_hex).collect();
964    let haves: Vec<String> = haves.iter().map(ObjectId::to_hex).collect();
965    let opts = crate::rev_list::RevListOptions {
966        objects: true,
967        no_object_names: false,
968        quiet: true,
969        filter: Some(filter.clone()),
970        cancel_watch: Some(stop.watch_arc()),
971        missing_action: match missing_mode {
972            MissingBlobMode::Fail => MissingAction::Error,
973            MissingBlobMode::Probe => MissingAction::Allow,
974        },
975        ..Default::default()
976    };
977    let result = crate::rev_list::rev_list(&repo, &wants, &haves, &opts)?;
978    stop.poll()?;
979    let mut ordered = Vec::new();
980    let mut seen = HashSet::new();
981    for oid in result
982        .commits
983        .iter()
984        .chain(result.objects.iter().map(|(o, _)| o))
985    {
986        if seen.insert(*oid) {
987            ordered.push(*oid);
988        }
989    }
990    let mut name_hashes = HashMap::new();
991    for (oid, path) in &result.objects {
992        if !path.is_empty() {
993            name_hashes
994                .entry(*oid)
995                .or_insert_with(|| pack_name_hash(path));
996        }
997    }
998    Ok((ordered, name_hashes))
999}
1000
1001/// Walk objects reachable from `roots`, with the path name hash of every blob
1002/// (and, with [`NameTrees::Yes`], every tree) below a root tree, and the
1003/// `have_commits` the walk stopped at (its edge commits).
1004///
1005/// Commits are walked newest committer date first (the order Git's revision walk
1006/// uses), all of them before any tree is expanded, so the versions of one path are
1007/// listed newest first: delta search breaks size ties by that order. Tree and blob
1008/// roots (index entries, cache-tree nodes) carry no path and are walked last, so
1009/// objects reachable from history keep the path they have there.
1010///
1011/// `roots` and the objects tags point at are always listed. With `filter`, parent
1012/// commits, trees and blobs are listed only when they pass it, the way
1013/// `pack-objects --filter` selects them: a tree's depth counts from its commit's root
1014/// tree (`0`), and a blob is checked against the depth of the tree holding it. As in
1015/// Git, a root or tag target that is also some commit's root tree counts as that root
1016/// tree. Trees whose entries cannot pass are not read, and blob sizes are only looked
1017/// up for `blob:limit`.
1018///
1019/// With [`MissingBlobMode::Probe`], blobs the repository does not store are not
1020/// listed but returned in [`WalkedObjects::missing`].
1021#[allow(clippy::too_many_arguments)]
1022fn collect_reachable_excluding_with_name_hashes(
1023    odb: &Odb,
1024    roots: &[ObjectId],
1025    exclude: &HashSet<ObjectId>,
1026    boundary: &mut HashSet<ObjectId>,
1027    have_commits: &HashSet<ObjectId>,
1028    peer_odb: Option<&Odb>,
1029    alternate_sources: Option<Arc<CompositeStore>>,
1030    skip_missing: bool,
1031    shallow_grafts: &HashSet<ObjectId>,
1032    filter: Option<&ObjectFilter>,
1033    name_trees: NameTrees,
1034    missing_mode: MissingBlobMode,
1035    stop: &CancelSignal<'_>,
1036) -> Result<WalkedObjects> {
1037    let oid_len = odb.hash_algo().len();
1038    let mut walk = EnumerationWalk {
1039        odb,
1040        exclude,
1041        boundary,
1042        have_commits,
1043        peer_odb,
1044        alternate_sources,
1045        skip_missing,
1046        filter,
1047        depth_sensitive: filter.is_some_and(ObjectFilter::is_depth_sensitive),
1048        name_trees,
1049        visited: oid_hash_set(),
1050        tree_depths: oid_hash_map(),
1051        unlisted: oid_hash_set(),
1052        ordered: Vec::new(),
1053        commits: BinaryHeap::new(),
1054        next_seq: 0,
1055        queue: VecDeque::new(),
1056        name_hashes: HashMap::new(),
1057        edges: HashSet::new(),
1058        missing: (missing_mode == MissingBlobMode::Probe).then(Vec::new),
1059        session_pack: odb.open_session_pack_reader().ok().flatten(),
1060        expanded: filter.is_none().then(HashMap::new),
1061    };
1062    // Commit root trees the filter drops, even where a root or tag names them.
1063    let mut dropped_root_trees: Option<OidHashSet> = filter
1064        .filter(|f| !f.includes_tree(0))
1065        .map(|_| oid_hash_set());
1066
1067    let mut path_less_roots = Vec::new();
1068    for &root in roots {
1069        match odb.read_info(&root) {
1070            Ok(info) if matches!(info.kind, ObjectKind::Tree | ObjectKind::Blob) => {
1071                path_less_roots.push(root);
1072            }
1073            _ => walk.push_root(root)?,
1074        }
1075    }
1076    let mut path_less_roots = path_less_roots.into_iter();
1077    // The walk's thread parses trees; the others read the queued ones ahead of it.
1078    let read_ahead_workers = crate::pack_delta::pack_delta_search_parallel_options(odb, None)
1079        .map_or(0, |par| par.threads.get() - 1);
1080
1081    std::thread::scope(|scope| -> Result<()> {
1082        let mut read_ahead = TreeReadAhead::new(scope, odb, read_ahead_workers);
1083        loop {
1084            stop.poll()?;
1085            if let Some(commit) = walk.commits.pop() {
1086                if !shallow_grafts.contains(&commit.oid) {
1087                    for parent in commit.parents {
1088                        walk.push_parent(parent)?;
1089                    }
1090                }
1091                if let Some(dropped) = dropped_root_trees.as_mut() {
1092                    dropped.insert(commit.tree);
1093                }
1094                walk.push_tree(commit.tree, None, 0);
1095                read_ahead.top_up(&walk.queue);
1096                continue;
1097            }
1098            let Some(QueuedObject {
1099                oid,
1100                path_hash,
1101                depth,
1102                found_as_tree,
1103            }) = walk.queue.pop_front()
1104            else {
1105                match path_less_roots.next() {
1106                    Some(root) => {
1107                        walk.push_object(root, None, false);
1108                        continue;
1109                    }
1110                    None => break,
1111                }
1112            };
1113            let read_ahead_result = read_ahead.take_front();
1114            read_ahead.top_up(&walk.queue);
1115            // Queued blobs are wanted directly (a lazy fetch names thousands); the
1116            // header says there is nothing to walk without inflating them. Objects
1117            // found as trees are read straight away; their own kind still decides below.
1118            if !found_as_tree {
1119                match odb.read_info(&oid) {
1120                    Ok(info) if info.kind == ObjectKind::Blob => continue,
1121                    Ok(_) => {}
1122                    Err(_) if skip_missing => continue,
1123                    Err(e) => return Err(e),
1124                }
1125            }
1126            let read = match read_ahead_result {
1127                Some(obj) => Ok(obj),
1128                None => walk.read_object(&oid),
1129            };
1130            let obj = match read {
1131                Ok(o) => o,
1132                Err(_) if skip_missing => continue,
1133                Err(e) => return Err(e),
1134            };
1135            walk.expand(oid, obj, path_hash, depth, oid_len)?;
1136        }
1137        Ok(())
1138    })?;
1139    // The delta bases cached while reading trees are not read again by what follows a
1140    // walk: delta planning reads entry headers and the writer copies stored entries.
1141    odb.clear_delta_base_cache();
1142
1143    if let Some(dropped) = dropped_root_trees.filter(|d| !d.is_empty()) {
1144        walk.ordered.retain(|oid| !dropped.contains(oid));
1145    }
1146    let mut send = walk.ordered;
1147    if let Some(peer) = peer_odb {
1148        if !peer.uses_files_primary() && !peer.custom_primary_is_empty()? {
1149            send = peer.missing_materialized_many(&send)?;
1150        }
1151    }
1152    Ok(WalkedObjects {
1153        send,
1154        name_hashes: walk.name_hashes,
1155        edges: walk.edges,
1156        missing: walk.missing.unwrap_or_default(),
1157    })
1158}
1159
1160/// Result of [`collect_reachable_excluding_with_name_hashes`].
1161struct WalkedObjects {
1162    /// Objects to send, in pack order.
1163    send: Vec<ObjectId>,
1164    /// Pack name hash of each named object.
1165    name_hashes: HashMap<ObjectId, u32>,
1166    /// Have-side commits the walk stopped at.
1167    edges: HashSet<ObjectId>,
1168    /// Blobs left out because they are not stored ([`MissingBlobMode::Probe`] only).
1169    missing: Vec<MissingBlob>,
1170}
1171
1172/// An object waiting in [`EnumerationWalk::queue`].
1173struct QueuedObject {
1174    oid: ObjectId,
1175    /// Pack name hash of the path the object was found at; `None` for a root tree or
1176    /// an object reached without a path (a root or a tag target).
1177    path_hash: Option<u32>,
1178    /// Tree depth, for depth-sensitive filters.
1179    depth: u64,
1180    /// Named as a tree by a commit or a tree entry, so its kind is not looked up
1181    /// before it is read.
1182    found_as_tree: bool,
1183}
1184
1185impl ReadAheadEntry for QueuedObject {
1186    fn read_ahead_oid(&self) -> Option<ObjectId> {
1187        self.found_as_tree.then_some(self.oid)
1188    }
1189}
1190
1191/// State of [`collect_reachable_excluding_with_name_hashes`].
1192struct EnumerationWalk<'a> {
1193    odb: &'a Odb,
1194    exclude: &'a HashSet<ObjectId>,
1195    /// Negotiation boundaries: do not list or expand past these objects.
1196    boundary: &'a mut HashSet<ObjectId>,
1197    /// Uninteresting commits (send-pack) or full have closure (upload-pack).
1198    have_commits: &'a HashSet<ObjectId>,
1199    /// Objects already on the peer (local fetch): stop without listing.
1200    peer_odb: Option<&'a Odb>,
1201    /// Alternate object directories (`repack -l` / `pack-objects --local`).
1202    alternate_sources: Option<Arc<CompositeStore>>,
1203    skip_missing: bool,
1204    filter: Option<&'a ObjectFilter>,
1205    /// The filter's verdict for trees and blobs depends on where they are met.
1206    depth_sensitive: bool,
1207    name_trees: NameTrees,
1208    /// Objects listed (or held by the peer), and objects the filter left out whose
1209    /// verdict cannot change: parent commits, and without a depth-sensitive filter,
1210    /// trees and size-checked blobs.
1211    visited: OidHashSet,
1212    /// Shallowest depth each tree was walked at (depth-sensitive filters only).
1213    tree_depths: OidHashMap<u64>,
1214    /// Objects in [`Self::visited`] that the filter left out. A root or tag target
1215    /// naming one of them still lists it.
1216    unlisted: OidHashSet,
1217    ordered: Vec<ObjectId>,
1218    /// Commits read but not yet expanded, newest first.
1219    commits: BinaryHeap<QueuedCommit>,
1220    next_seq: u64,
1221    /// Objects to read. Drained only while [`Self::commits`] is empty.
1222    queue: VecDeque<QueuedObject>,
1223    name_hashes: HashMap<ObjectId, u32>,
1224    /// Have-side commits the walk stopped at (Git's edge commits).
1225    edges: HashSet<ObjectId>,
1226    /// With [`MissingBlobMode::Probe`], blobs that would be listed but are not stored.
1227    missing: Option<Vec<MissingBlob>>,
1228    /// Reader over the single local pack, kept for the whole walk instead of the one
1229    /// [`Odb::read`] opens per object (see [`Odb::open_session_pack_reader`]).
1230    session_pack: Option<SingleLocalPackReader>,
1231    /// Without a filter, the last tree expanded at each path, keyed by the path's
1232    /// name hash (see [`ExpandedTree`]).
1233    expanded: Option<HashMap<Option<u32>, ExpandedTree>>,
1234}
1235
1236/// A tree whose every entry the walk has pushed.
1237///
1238/// Without a filter, pushing an entry always leaves the object it names in
1239/// [`EnumerationWalk::visited`], so an entry that another tree repeats byte for byte
1240/// has nothing left to do. Successive versions of one directory usually differ in a
1241/// few entries: a new version is compared with the last one expanded at its path, and
1242/// only the entries between their shared leading and trailing bytes are parsed. Of
1243/// those, an entry the earlier version also has, byte for byte, is skipped as well.
1244///
1245/// Which tree a version is compared with only decides how much is skipped, never what
1246/// is listed, so paths whose name hashes collide may share an entry.
1247struct ExpandedTree {
1248    data: Vec<u8>,
1249    /// Byte offset of every entry, ascending, then `data.len()`.
1250    entry_starts: Vec<usize>,
1251}
1252
1253impl ExpandedTree {
1254    /// Entry offsets from `pos` on, when `pos` is where an entry starts.
1255    fn starts_from(&self, pos: usize) -> Option<&[usize]> {
1256        let at = self.entry_starts.binary_search(&pos).ok()?;
1257        Some(&self.entry_starts[at..])
1258    }
1259
1260    /// The raw bytes of entry `index`, if there is one.
1261    fn entry(&self, index: usize) -> Option<&[u8]> {
1262        let start = *self.entry_starts.get(index)?;
1263        let end = *self.entry_starts.get(index + 1)?;
1264        self.data.get(start..end)
1265    }
1266
1267    /// Whether `bytes`, a whole entry named `name` of a newer version, is also an entry
1268    /// here, looked for from entry `*cursor` on.
1269    ///
1270    /// Both versions list entries in tree order, so the cursor only moves forward: past
1271    /// entries whose names sort before `name`, and past the one entry matched. Names
1272    /// compare as plain bytes, which can differ from tree order where a directory name
1273    /// is a prefix of another name; that only costs a missed match, since a match
1274    /// always means equal bytes.
1275    fn repeats_entry(&self, cursor: &mut usize, bytes: &[u8], name: &[u8], oid_len: usize) -> bool {
1276        while let Some(old) = self.entry(*cursor) {
1277            if old == bytes {
1278                *cursor += 1;
1279                return true;
1280            }
1281            let old_name = memchr::memchr(b' ', old)
1282                .and_then(|space| old.get(space + 1..old.len().checked_sub(oid_len + 1)?));
1283            match old_name.map(|old_name| old_name.cmp(name)) {
1284                Some(std::cmp::Ordering::Less) => *cursor += 1,
1285                Some(std::cmp::Ordering::Equal) => {
1286                    *cursor += 1;
1287                    return false;
1288                }
1289                _ => return false,
1290            }
1291        }
1292        false
1293    }
1294}
1295
1296/// Length of the longest common prefix of `a` and `b`.
1297fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
1298    let (a_chunks, _) = a.as_chunks::<16>();
1299    let (b_chunks, _) = b.as_chunks::<16>();
1300    let whole = a_chunks
1301        .iter()
1302        .zip(b_chunks)
1303        .take_while(|(x, y)| x == y)
1304        .count()
1305        * 16;
1306    whole
1307        + a[whole..]
1308            .iter()
1309            .zip(&b[whole..])
1310            .take_while(|(x, y)| x == y)
1311            .count()
1312}
1313
1314/// Length of the longest common suffix of `a` and `b`.
1315fn common_suffix_len(a: &[u8], b: &[u8]) -> usize {
1316    let (_, a_chunks) = a.as_rchunks::<16>();
1317    let (_, b_chunks) = b.as_rchunks::<16>();
1318    let whole = a_chunks
1319        .iter()
1320        .rev()
1321        .zip(b_chunks.iter().rev())
1322        .take_while(|(x, y)| x == y)
1323        .count()
1324        * 16;
1325    whole
1326        + a[..a.len() - whole]
1327            .iter()
1328            .rev()
1329            .zip(b[..b.len() - whole].iter().rev())
1330            .take_while(|(x, y)| x == y)
1331            .count()
1332}
1333
1334/// A commit waiting in [`EnumerationWalk::commits`], ordered newest first and,
1335/// among equal dates, in the order it was found.
1336struct QueuedCommit {
1337    /// Commit-graph generation; [`UNRANKED_GENERATION`] outside the graph, so those sort first.
1338    generation: u64,
1339    time: i64,
1340    seq: std::cmp::Reverse<u64>,
1341    oid: ObjectId,
1342    tree: ObjectId,
1343    parents: Vec<ObjectId>,
1344}
1345
1346impl PartialEq for QueuedCommit {
1347    fn eq(&self, other: &Self) -> bool {
1348        (self.generation, self.time, self.seq) == (other.generation, other.time, other.seq)
1349    }
1350}
1351
1352impl Eq for QueuedCommit {}
1353
1354impl PartialOrd for QueuedCommit {
1355    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1356        Some(self.cmp(other))
1357    }
1358}
1359
1360impl Ord for QueuedCommit {
1361    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1362        (self.generation, self.time, self.seq).cmp(&(other.generation, other.time, other.seq))
1363    }
1364}
1365
1366impl<'a> EnumerationWalk<'a> {
1367    fn list(&mut self, oid: ObjectId) {
1368        if !self.exclude.contains(&oid) {
1369            self.ordered.push(oid);
1370        }
1371    }
1372
1373    fn note_boundary(&mut self, oid: ObjectId) {
1374        self.visited.insert(oid);
1375    }
1376
1377    fn at_object_boundary(&self, oid: ObjectId) -> bool {
1378        if self
1379            .alternate_sources
1380            .as_ref()
1381            .is_some_and(|s| s.contains(&oid).unwrap_or(false))
1382        {
1383            return true;
1384        }
1385        if self.boundary.contains(&oid) {
1386            return true;
1387        }
1388        self.peer_odb.is_some_and(|peer| {
1389            peer.uses_files_primary() && peer.exists_materialized_local_without_rescan(&oid)
1390        })
1391    }
1392
1393    fn seal_have_reachable_commit(&mut self, oid: ObjectId) -> Result<()> {
1394        if !self.visited.insert(oid) {
1395            return Ok(());
1396        }
1397        self.edges.insert(oid);
1398        self.boundary
1399            .extend(objects_in_commit_snapshot(self.odb, oid)?);
1400        Ok(())
1401    }
1402
1403    fn commit_on_have_side(&mut self, oid: ObjectId) -> Result<bool> {
1404        Ok(self.have_commits.contains(&oid))
1405    }
1406
1407    /// An object that is sent whatever the filter says: a root or a tag target.
1408    fn push_object(&mut self, oid: ObjectId, path_hash: Option<u32>, found_as_tree: bool) {
1409        if let Ok(true) = self.commit_on_have_side(oid) {
1410            let _ = self.seal_have_reachable_commit(oid);
1411            return;
1412        }
1413        if self.at_object_boundary(oid) {
1414            self.note_boundary(oid);
1415            return;
1416        }
1417        if self.visited.insert(oid) {
1418            self.list(oid);
1419            self.queue.push_back(QueuedObject {
1420                oid,
1421                path_hash,
1422                depth: 0,
1423                found_as_tree,
1424            });
1425        } else if self.unlisted.remove(&oid) {
1426            self.list(oid);
1427        }
1428    }
1429
1430    /// A root or tag target that may be a commit: read now so a commit enters the
1431    /// date-ordered heap at once; anything else waits in [`Self::queue`].
1432    fn push_root(&mut self, oid: ObjectId) -> Result<()> {
1433        if self.commit_on_have_side(oid)? {
1434            return self.seal_have_reachable_commit(oid);
1435        }
1436        if self.at_object_boundary(oid) {
1437            self.note_boundary(oid);
1438            return Ok(());
1439        }
1440        if !self.visited.insert(oid) {
1441            if self.unlisted.remove(&oid) {
1442                self.list(oid);
1443            }
1444            return Ok(());
1445        }
1446        self.list(oid);
1447        // Only a commit is read now; anything else (a blob want, say) is read, if at
1448        // all, when the queue reaches it.
1449        match self.odb.read_info(&oid) {
1450            Ok(info) if info.kind != ObjectKind::Commit => {
1451                self.queue_unnamed(oid, info.kind == ObjectKind::Tree);
1452                return Ok(());
1453            }
1454            Ok(_) => {}
1455            Err(_) if self.skip_missing => return Ok(()),
1456            Err(e) => return Err(e),
1457        }
1458        match self.read(&oid)? {
1459            Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1460            Some(obj) => {
1461                self.queue_unnamed(oid, obj.kind == ObjectKind::Tree);
1462                Ok(())
1463            }
1464            None => Ok(()),
1465        }
1466    }
1467
1468    /// A parent commit: always walked, listed unless the filter keeps other types only.
1469    /// Read now so it enters the date-ordered heap before any tree is expanded.
1470    fn push_parent(&mut self, oid: ObjectId) -> Result<()> {
1471        if self.commit_on_have_side(oid)? {
1472            return self.seal_have_reachable_commit(oid);
1473        }
1474        if self.at_object_boundary(oid) {
1475            self.note_boundary(oid);
1476            return Ok(());
1477        }
1478        let listed = self
1479            .filter
1480            .is_none_or(|f| f.includes_commit_or_tag_object(ObjectKind::Commit));
1481        if !self.visited.insert(oid) {
1482            if listed && self.unlisted.remove(&oid) {
1483                self.list(oid);
1484            }
1485            return Ok(());
1486        }
1487        if listed {
1488            self.list(oid);
1489        } else {
1490            self.unlisted.insert(oid);
1491        }
1492        match self.read(&oid)? {
1493            Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1494            Some(obj) => {
1495                self.queue_unnamed(oid, obj.kind == ObjectKind::Tree);
1496                Ok(())
1497            }
1498            None => Ok(()),
1499        }
1500    }
1501
1502    /// Queue an object reached without a path (a root, or a parent that is not a commit).
1503    fn queue_unnamed(&mut self, oid: ObjectId, found_as_tree: bool) {
1504        self.queue.push_back(QueuedObject {
1505            oid,
1506            path_hash: None,
1507            depth: 0,
1508            found_as_tree,
1509        });
1510    }
1511
1512    /// Expand an object read from [`Self::queue`]: queue a commit, list and queue the
1513    /// entries of a tree, or follow a tag.
1514    fn expand(
1515        &mut self,
1516        oid: ObjectId,
1517        obj: Object,
1518        path_hash: Option<u32>,
1519        depth: u64,
1520        oid_len: usize,
1521    ) -> Result<()> {
1522        match obj.kind {
1523            ObjectKind::Commit => {
1524                self.queue_commit(oid, obj)?;
1525            }
1526            ObjectKind::Tree => self.expand_tree(oid, obj.data, path_hash, depth, oid_len)?,
1527            ObjectKind::Tag => {
1528                let tag = parse_tag(&obj.data)?;
1529                self.push_object(tag.object, path_hash, false);
1530            }
1531            ObjectKind::Blob => {}
1532        }
1533        Ok(())
1534    }
1535
1536    /// Push every entry of tree `oid` (payload `data`, found at `path_hash` and `depth`).
1537    fn expand_tree(
1538        &mut self,
1539        oid: ObjectId,
1540        data: Vec<u8>,
1541        path_hash: Option<u32>,
1542        depth: u64,
1543        oid_len: usize,
1544    ) -> Result<()> {
1545        let prev = self
1546            .expanded
1547            .as_mut()
1548            .and_then(|by_path| by_path.remove(&path_hash));
1549        let mut entry_starts = Vec::new();
1550        let mut pos = 0;
1551        // Entries before `pos` are byte-identical to `prev`'s, and from `shared_tail`
1552        // on the two trees end in the same bytes.
1553        let mut shared_tail = data.len();
1554        // Index in `prev.entry_starts` of the earlier version's next unmatched entry.
1555        let mut prev_entry = 0;
1556        if let Some(prev) = &prev {
1557            let prefix = common_prefix_len(&data, &prev.data);
1558            let shared = prev.entry_starts.partition_point(|&start| start <= prefix);
1559            if let Some(&last) = shared.checked_sub(1).and_then(|i| prev.entry_starts.get(i)) {
1560                pos = last;
1561                prev_entry = shared - 1;
1562                entry_starts.extend_from_slice(&prev.entry_starts[..shared - 1]);
1563            }
1564            shared_tail = data.len() - common_suffix_len(&data[pos..], &prev.data[pos..]);
1565        }
1566        loop {
1567            let at = pos;
1568            if let Some(prev) = prev.as_ref().filter(|_| at >= shared_tail) {
1569                // `at` is an entry start here; when it is one in `prev` as well, both
1570                // trees parse the same from there to the end.
1571                let prev_at = at + prev.data.len() - data.len();
1572                if let Some(rest) = prev.starts_from(prev_at) {
1573                    entry_starts.extend(rest.iter().map(|&s| s + data.len() - prev.data.len()));
1574                    break;
1575                }
1576            }
1577            entry_starts.push(at);
1578            if at >= data.len() {
1579                break;
1580            }
1581            let entry = match tree_entry_span(&data, at, oid_len) {
1582                Some(span) => {
1583                    pos = span.end;
1584                    if let Some(prev) = &prev {
1585                        let name = &data[span.name.clone()];
1586                        if prev.repeats_entry(&mut prev_entry, &data[at..pos], name, oid_len) {
1587                            continue;
1588                        }
1589                    }
1590                    TreeEntryRef {
1591                        mode: span.mode,
1592                        name: &data[span.name],
1593                        oid: ObjectId::from_bytes(&data[pos - oid_len..pos])?,
1594                    }
1595                }
1596                None => {
1597                    let mut entries = TreeRefIter::with_oid_len_at(&data, oid_len, at);
1598                    let Some(entry) = entries.next().transpose()? else {
1599                        break;
1600                    };
1601                    pos = entries.position();
1602                    entry
1603                }
1604            };
1605            self.push_tree_entry(entry, oid, path_hash, depth)?;
1606        }
1607        if let Some(by_path) = self.expanded.as_mut() {
1608            by_path.insert(path_hash, ExpandedTree { data, entry_starts });
1609        }
1610        Ok(())
1611    }
1612
1613    fn push_tree_entry(
1614        &mut self,
1615        entry: TreeEntryRef<'_>,
1616        tree: ObjectId,
1617        path_hash: Option<u32>,
1618        depth: u64,
1619    ) -> Result<()> {
1620        if entry.mode == 0o160000 {
1621            return Ok(());
1622        }
1623        // Only a filter leaves objects unlisted, so without one an object already
1624        // visited has nothing left to record.
1625        if self.filter.is_none() && self.visited.contains(&entry.oid) {
1626            return Ok(());
1627        }
1628        // Regular files and symlinks have no children: record them without inflating
1629        // their contents. The pack writer reads (or copies verbatim) each one later
1630        // and fails there if it is missing. Any other mode, including directories
1631        // written with non-canonical permission bits, is read so the object's own
1632        // kind decides whether it is walked.
1633        match entry.mode & 0o170000 {
1634            0o120000 => self.push_blob(entry.oid, None, tree, depth)?,
1635            0o100000 => {
1636                let name = pack_name_hash_child(path_hash, entry.name);
1637                self.push_blob(entry.oid, Some(name), tree, depth)?;
1638            }
1639            file_type => {
1640                let name = Some(pack_name_hash_child(path_hash, entry.name));
1641                if file_type == 0o040000 {
1642                    self.push_tree(entry.oid, name, depth + 1);
1643                } else {
1644                    self.push_object(entry.oid, name, false);
1645                }
1646            }
1647        }
1648        Ok(())
1649    }
1650
1651    /// Read `oid`, preferring the walk's pack reader; a miss or error there falls back
1652    /// to the object database, which reports the authoritative result.
1653    fn read_object(&mut self, oid: &ObjectId) -> Result<Object> {
1654        if let Some(Ok(obj)) = self.session_pack.as_mut().map(|r| r.read_object(oid)) {
1655            return Ok(obj);
1656        }
1657        self.odb.read_once(oid)
1658    }
1659
1660    fn read(&mut self, oid: &ObjectId) -> Result<Option<Object>> {
1661        match self.read_object(oid) {
1662            Ok(obj) => Ok(Some(obj)),
1663            Err(_) if self.skip_missing => Ok(None),
1664            Err(e) => Err(e),
1665        }
1666    }
1667
1668    /// Hold a commit read from [`Self::queue`] until it is the newest one left.
1669    fn queue_commit(&mut self, oid: ObjectId, obj: Object) -> Result<()> {
1670        let parsed = CommitRef::parse(&obj.data)?;
1671        let queued = QueuedCommit {
1672            generation: UNRANKED_GENERATION,
1673            time: parsed.committer_unix_seconds_for_ordering(),
1674            seq: std::cmp::Reverse(self.next_seq),
1675            oid,
1676            tree: parsed.tree(),
1677            parents: parsed.parents().to_vec(),
1678        };
1679        self.next_seq += 1;
1680        self.commits.push(queued);
1681        Ok(())
1682    }
1683
1684    /// A commit's root tree (`depth` 0) or a subtree found in a tree.
1685    /// `path_hash` is the pack name hash of the tree's path, `None` for a root tree.
1686    fn push_tree(&mut self, oid: ObjectId, path_hash: Option<u32>, depth: u64) {
1687        if let Some(hash) = path_hash.filter(|_| self.name_trees == NameTrees::Yes) {
1688            if !self.visited.contains(&oid) {
1689                self.name_hashes.entry(oid).or_insert(hash);
1690            }
1691        }
1692        let Some(filter) = self.filter else {
1693            self.push_object(oid, path_hash, true);
1694            return;
1695        };
1696        // A tree in a have commit's snapshot is already on the peer, whatever the filter.
1697        if self.at_object_boundary(oid) {
1698            self.note_boundary(oid);
1699            return;
1700        }
1701        let first_walk = if self.depth_sensitive {
1702            match self.tree_depths.get(&oid) {
1703                Some(&walked_at) if walked_at <= depth => false,
1704                _ => {
1705                    self.tree_depths.insert(oid, depth);
1706                    true
1707                }
1708            }
1709        } else {
1710            self.visited.insert(oid)
1711        };
1712        if !first_walk {
1713            return;
1714        }
1715        if filter.includes_tree(depth) {
1716            if !self.depth_sensitive || self.visited.insert(oid) {
1717                self.list(oid);
1718            }
1719        } else if !self.depth_sensitive {
1720            self.unlisted.insert(oid);
1721        }
1722        if !filter.prunes_tree_entries(depth) {
1723            self.queue.push_back(QueuedObject {
1724                oid,
1725                path_hash,
1726                depth,
1727                found_as_tree: true,
1728            });
1729        }
1730    }
1731
1732    /// A blob in a tree at `tree_depth`; `name_hash` is the pack name hash of its path,
1733    /// given for regular files only.
1734    fn push_blob(
1735        &mut self,
1736        oid: ObjectId,
1737        name_hash: Option<u32>,
1738        referrer: ObjectId,
1739        tree_depth: u64,
1740    ) -> Result<()> {
1741        if self.at_object_boundary(oid) {
1742            self.note_boundary(oid);
1743            return Ok(());
1744        }
1745        if self.visited.contains(&oid) {
1746            return Ok(());
1747        }
1748        let (passes, size_checked) = match self.filter {
1749            None => (true, false),
1750            // Missing (probe mode only): as in Git's filters, an object whose size
1751            // cannot be read is not filtered out but reported missing.
1752            Some(filter) => self
1753                .blob_passes(filter, &oid, tree_depth)?
1754                .unwrap_or((true, true)),
1755        };
1756        if passes && self.missing.is_some() && !self.odb.contains(&oid)? {
1757            self.visited.insert(oid);
1758            if let Some(missing) = self.missing.as_mut() {
1759                missing.push(MissingBlob {
1760                    oid,
1761                    referrer: Some(referrer),
1762                });
1763            }
1764            return Ok(());
1765        }
1766        if passes {
1767            self.visited.insert(oid);
1768            self.list(oid);
1769            if let Some(hash) = name_hash {
1770                self.name_hashes.insert(oid, hash);
1771            }
1772        } else if size_checked && !self.depth_sensitive {
1773            self.visited.insert(oid);
1774            self.unlisted.insert(oid);
1775        }
1776        Ok(())
1777    }
1778
1779    /// Whether the blob passes `filter`, and whether its size had to be looked up.
1780    ///
1781    /// `None` when the size was needed but the blob is not stored, which only
1782    /// happens with [`MissingBlobMode::Probe`] (the size is read without a lazy
1783    /// fetch there); otherwise a missing blob is an error, or with `skip_missing`
1784    /// filtered out.
1785    fn blob_passes(
1786        &self,
1787        filter: &ObjectFilter,
1788        oid: &ObjectId,
1789        tree_depth: u64,
1790    ) -> Result<Option<(bool, bool)>> {
1791        // A smaller blob never fares worse, so the size only matters between the extremes.
1792        if !filter.includes_blob_under_tree(0, tree_depth) {
1793            return Ok(Some((false, false)));
1794        }
1795        if filter.includes_blob_under_tree(u64::MAX, tree_depth) {
1796            return Ok(Some((true, false)));
1797        }
1798        let info = if self.missing.is_some() {
1799            match self.odb.read_info_without_fetch(oid) {
1800                Err(crate::error::Error::ObjectNotFound(_)) => return Ok(None),
1801                other => other,
1802            }
1803        } else {
1804            self.odb.read_info(oid)
1805        };
1806        match info {
1807            Ok(info) => Ok(Some((
1808                filter.includes_blob_under_tree(info.size, tree_depth),
1809                true,
1810            ))),
1811            Err(_) if self.skip_missing => Ok(Some((false, true))),
1812            Err(e) => Err(e),
1813        }
1814    }
1815}
1816
1817#[cfg(test)]
1818mod tests {
1819    #![allow(clippy::unwrap_used, clippy::expect_used)]
1820
1821    use super::*;
1822
1823    use crate::repo::{InitOptions, Repository};
1824    use tempfile::TempDir;
1825
1826    fn walk(repo: &Repository, roots: &[ObjectId], name_trees: NameTrees) -> PackObjectSelection {
1827        let empty = HashSet::new();
1828        enumerate_pack_objects_naming(
1829            repo.odb(),
1830            roots,
1831            &[],
1832            &PackEnumerateOptions {
1833                filter: None,
1834                shallow_grafts: &empty,
1835                have_shallow_grafts: Some(&empty),
1836                use_bitmaps: false,
1837                exclude_objects: None,
1838                peer_odb: None,
1839                have_boundary: HaveBoundary::CommitClosure,
1840            },
1841            name_trees,
1842            false,
1843            &CancelSignal::never(),
1844        )
1845        .expect("walk")
1846    }
1847
1848    fn write_tree(repo: &Repository, entries: &[(u32, &str, ObjectId)]) -> ObjectId {
1849        let entries: Vec<crate::objects::TreeEntry> = entries
1850            .iter()
1851            .map(|&(mode, name, oid)| crate::objects::TreeEntry {
1852                mode,
1853                name: name.as_bytes().to_vec(),
1854                oid,
1855            })
1856            .collect();
1857        repo.odb()
1858            .write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
1859            .expect("tree")
1860    }
1861
1862    fn write_commit(
1863        repo: &Repository,
1864        tree: ObjectId,
1865        parents: &[ObjectId],
1866        time: i64,
1867    ) -> ObjectId {
1868        let mut body = format!("tree {}\n", tree.to_hex());
1869        for parent in parents {
1870            body.push_str(&format!("parent {}\n", parent.to_hex()));
1871        }
1872        body.push_str(&format!(
1873            "author A <a@example.com> {time} +0000\ncommitter A <a@example.com> {time} +0000\n\nc{time}\n"
1874        ));
1875        repo.odb()
1876            .write(ObjectKind::Commit, body.as_bytes())
1877            .expect("commit")
1878    }
1879
1880    #[test]
1881    fn tips_only_boundary_matches_closure_on_linear_incremental_push() {
1882        let dir = TempDir::new().unwrap();
1883        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1884        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1885        let mut parent = write_commit(
1886            &repo,
1887            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1888            &[],
1889            0,
1890        );
1891        for i in 1..800 {
1892            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1893            parent = write_commit(&repo, tree, &[parent], i);
1894        }
1895        let remote_tip = parent;
1896        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1897        let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1898        let empty = HashSet::new();
1899        let closure_opts = PackEnumerateOptions {
1900            filter: None,
1901            shallow_grafts: &empty,
1902            have_shallow_grafts: Some(&empty),
1903            use_bitmaps: false,
1904            exclude_objects: None,
1905            peer_odb: None,
1906            have_boundary: HaveBoundary::CommitClosure,
1907        };
1908        let tips_opts = PackEnumerateOptions {
1909            have_boundary: HaveBoundary::TipsOnly,
1910            ..closure_opts
1911        };
1912        let closure = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &closure_opts)
1913            .expect("closure");
1914        let tips = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &tips_opts)
1915            .expect("tips");
1916        let closure_set: HashSet<ObjectId> = closure.send.iter().copied().collect();
1917        let tips_set: HashSet<ObjectId> = tips.send.iter().copied().collect();
1918        assert_eq!(closure_set, tips_set);
1919        assert!(
1920            tips.send.len() <= 8,
1921            "one new commit should enumerate a handful of objects, got {}",
1922            tips.send.len()
1923        );
1924    }
1925
1926    #[test]
1927    fn tips_only_send_pack_does_not_prewalk_deep_history() {
1928        use crate::merge_base::intersect_walk_test_hooks::{
1929            commit_parent_lookups, reset_commit_parent_lookups,
1930        };
1931
1932        let dir = TempDir::new().unwrap();
1933        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1934        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1935        let mut parent = write_commit(
1936            &repo,
1937            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1938            &[],
1939            0,
1940        );
1941        for i in 1..800 {
1942            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1943            parent = write_commit(&repo, tree, &[parent], i);
1944        }
1945        let remote_tip = parent;
1946        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1947        let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1948        let haves = vec![remote_tip];
1949        let empty = HashSet::new();
1950        reset_commit_parent_lookups();
1951        let _selection = enumerate_pack_objects(
1952            repo.odb(),
1953            &[new_tip],
1954            &haves,
1955            &PackEnumerateOptions {
1956                filter: None,
1957                shallow_grafts: &empty,
1958                have_shallow_grafts: Some(&empty),
1959                use_bitmaps: false,
1960                exclude_objects: None,
1961                peer_odb: None,
1962                have_boundary: HaveBoundary::TipsOnly,
1963            },
1964        )
1965        .expect("enumerate");
1966        assert!(
1967            commit_parent_lookups() <= 128,
1968            "send-pack hide must stay coupled to the want walk ({} parent lookups)",
1969            commit_parent_lookups()
1970        );
1971    }
1972
1973    #[test]
1974    fn tree_read_ahead_leaves_the_walk_output_unchanged() {
1975        let dir = TempDir::new().unwrap();
1976        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1977        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1978        let mut tip: Option<ObjectId> = None;
1979        // Two trees per commit, so the queue holds far more than one read-ahead batch.
1980        for i in 0..400i64 {
1981            let sub = write_tree(&repo, &[(0o100644, "f", blob(&format!("sub {i}")))]);
1982            let tree = write_tree(
1983                &repo,
1984                &[
1985                    (0o040000, "dir", sub),
1986                    (0o100644, "top", blob(&format!("top {}", i / 3))),
1987                ],
1988            );
1989            tip = Some(write_commit(&repo, tree, tip.as_slice(), 1_000 + i));
1990        }
1991        let tip = tip.unwrap();
1992        let config_path = repo.git_dir().join("config");
1993        let config = std::fs::read_to_string(&config_path).unwrap();
1994        let walk_with_threads = |threads: u32| {
1995            std::fs::write(
1996                &config_path,
1997                format!("{config}[pack]\n\tthreads = {threads}\n"),
1998            )
1999            .unwrap();
2000            let repo = Repository::open(repo.git_dir(), &RepositoryOptions::empty()).unwrap();
2001            walk(&repo, &[tip], NameTrees::Yes)
2002        };
2003        let serial = walk_with_threads(1);
2004        let parallel = walk_with_threads(4);
2005        assert_eq!(serial.send.len(), 400 * 3 + 400 + 134);
2006        assert_eq!(parallel.send, serial.send);
2007        assert_eq!(parallel.name_hashes, serial.name_hashes);
2008    }
2009
2010    #[test]
2011    fn send_pack_walk_without_haves_reads_no_commits() {
2012        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2013        let dir = TempDir::new().unwrap();
2014        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2015        let tree = write_tree(&repo, &[]);
2016        let mut tip = write_commit(&repo, tree, &[], 1_300_000_000);
2017        for i in 1..50 {
2018            tip = write_commit(&repo, tree, &[tip], 1_300_000_000 + i);
2019        }
2020
2021        reset_commit_reads();
2022        let hidden = send_pack_uninteresting_commits(
2023            repo.odb(),
2024            &[tip],
2025            &[],
2026            &HashSet::new(),
2027            &CancelSignal::never(),
2028        )
2029        .unwrap();
2030        assert!(hidden.is_empty());
2031        assert_eq!(
2032            commit_reads(),
2033            0,
2034            "nothing can be uninteresting without a have"
2035        );
2036
2037        let selection = walk(&repo, &[tip], NameTrees::No);
2038        assert_eq!(
2039            selection.send.len(),
2040            51,
2041            "every commit and the tree are still sent"
2042        );
2043    }
2044
2045    #[test]
2046    fn push_dated_before_graphed_history_stops_at_the_frontier() {
2047        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2048        let dir = TempDir::new().unwrap();
2049        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2050        let tree = write_tree(&repo, &[]);
2051        // The remote's history, in the commit-graph, with a tag partway down.
2052        let mut tip = write_commit(&repo, tree, &[], 1_300_000_000);
2053        let mut tagged = tip;
2054        for i in 1..300 {
2055            tip = write_commit(&repo, tree, &[tip], 1_300_000_000 + i);
2056            if i == 150 {
2057                tagged = tip;
2058            }
2059        }
2060        std::fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n")).unwrap();
2061        repo.odb().write_commit_graph().unwrap();
2062        // New local work outside the graph, dated before all of that history (a skewed
2063        // clock, or fixed dates in a script).
2064        let mut new_tip = tip;
2065        for i in 0..3 {
2066            new_tip = write_commit(&repo, tree, &[new_tip], 1_000_000_000 + i);
2067        }
2068
2069        reset_commit_reads();
2070        let hidden = send_pack_uninteresting_commits(
2071            repo.odb(),
2072            &[new_tip],
2073            &[tip, tagged],
2074            &HashSet::new(),
2075            &CancelSignal::never(),
2076        )
2077        .unwrap();
2078        assert!(hidden.contains(&tip));
2079        assert!(hidden.contains(&tagged));
2080        assert!(!hidden.contains(&new_tip));
2081        assert_eq!(
2082            commit_reads(),
2083            3,
2084            "only the commits outside the graph are read from the object store"
2085        );
2086        assert!(
2087            hidden.len() <= 4,
2088            "the walk must stop at the frontier, not drain the remote's history ({} marked)",
2089            hidden.len()
2090        );
2091    }
2092
2093    #[test]
2094    fn push_dated_before_packed_history_reads_commit_headers() {
2095        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2096        let dir = TempDir::new().unwrap();
2097        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2098        let tree = write_tree(&repo, &[]);
2099        let body = "a long message line\n".repeat(100);
2100        let write = |parents: &[ObjectId], time: i64| {
2101            let mut text = format!("tree {tree}\n");
2102            for parent in parents {
2103                text.push_str(&format!("parent {parent}\n"));
2104            }
2105            text.push_str(&format!(
2106                "author A <a@example.com> {time} +0000\n\
2107                 committer A <a@example.com> {time} +0000\n\nc{time}\n\n{body}"
2108            ));
2109            repo.odb()
2110                .write(ObjectKind::Commit, text.as_bytes())
2111                .unwrap()
2112        };
2113        // The remote's history, packed and without a commit-graph, with a merge in it.
2114        let root = write(&[], 1_300_000_000);
2115        let mut history = vec![root];
2116        let mut tip = root;
2117        for i in 1..120 {
2118            tip = write(&[tip], 1_300_000_000 + i);
2119            history.push(tip);
2120        }
2121        let side = write(&[root], 1_300_000_500);
2122        tip = write(&[tip, side], 1_300_000_600);
2123        history.extend([side, tip]);
2124        // New local work dated before all of that history, so the walk drains it.
2125        let mut new_tip = tip;
2126        let mut new_commits = Vec::new();
2127        for i in 0..3 {
2128            new_tip = write(&[new_tip], 1_000_000_000 + i);
2129            new_commits.push(new_tip);
2130        }
2131        std::fs::write(
2132            repo.git_dir().join("refs/heads/main"),
2133            format!("{new_tip}\n"),
2134        )
2135        .unwrap();
2136        crate::maintenance::repack_all(&repo, &crate::maintenance::RepackOptions::default())
2137            .unwrap();
2138        assert!(repo.odb().commit_graph_chain().is_none());
2139        let mut header = Vec::new();
2140        assert!(repo
2141            .odb()
2142            .read_packed_commit_header_block_into(&tip, &mut header)
2143            .unwrap());
2144
2145        reset_commit_reads();
2146        let hidden = repo
2147            .odb()
2148            .with_pack_read_context(|| {
2149                send_pack_uninteresting_commits(
2150                    repo.odb(),
2151                    &[new_tip],
2152                    &[tip],
2153                    &HashSet::new(),
2154                    &CancelSignal::never(),
2155                )
2156            })
2157            .unwrap();
2158        let expected: HashSet<ObjectId> = history.iter().copied().collect();
2159        assert_eq!(hidden, expected, "the remote's whole history, nothing new");
2160        assert!(new_commits.iter().all(|c| !hidden.contains(c)));
2161        assert_eq!(commit_reads(), history.len() + new_commits.len());
2162    }
2163
2164    #[test]
2165    fn duplicated_wants_already_on_peer_balance_the_frontier() {
2166        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2167        let dir = TempDir::new().unwrap();
2168        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2169        let tree = write_tree(&repo, &[]);
2170        let mut tip = write_commit(&repo, tree, &[], 0);
2171        for time in 1..100 {
2172            tip = write_commit(&repo, tree, &[tip], time);
2173        }
2174        reset_commit_reads();
2175        let hidden = send_pack_uninteresting_commits(
2176            repo.odb(),
2177            &[tip, tip],
2178            &[tip, tip],
2179            &HashSet::new(),
2180            &CancelSignal::never(),
2181        )
2182        .unwrap();
2183        assert!(hidden.contains(&tip));
2184        assert!(
2185            commit_reads() <= 2,
2186            "shared want/have must stop at the frontier"
2187        );
2188        let empty = HashSet::new();
2189        let selection = enumerate_pack_objects(
2190            repo.odb(),
2191            &[tip, tip],
2192            &[tip, tip],
2193            &PackEnumerateOptions {
2194                filter: None,
2195                shallow_grafts: &empty,
2196                have_shallow_grafts: None,
2197                use_bitmaps: false,
2198                exclude_objects: None,
2199                peer_odb: None,
2200                have_boundary: HaveBoundary::TipsOnly,
2201            },
2202        )
2203        .unwrap();
2204        assert!(selection.send.is_empty());
2205    }
2206
2207    #[test]
2208    fn tips_only_many_advertised_haves_does_not_prewalk_deep_history() {
2209        use crate::merge_base::intersect_walk_test_hooks::{
2210            commit_parent_lookups, reset_commit_parent_lookups,
2211        };
2212
2213        let dir = TempDir::new().unwrap();
2214        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2215        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2216        let mut chain = Vec::new();
2217        let mut parent = write_commit(
2218            &repo,
2219            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
2220            &[],
2221            0,
2222        );
2223        chain.push(parent);
2224        for i in 1..800 {
2225            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
2226            parent = write_commit(&repo, tree, &[parent], i);
2227            chain.push(parent);
2228        }
2229        let remote_tip = parent;
2230        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
2231        let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
2232        let mut haves = vec![remote_tip];
2233        for n in (2..400).step_by(2) {
2234            let idx = chain.len().checked_sub(1 + n);
2235            if let Some(idx) = idx {
2236                haves.push(chain[idx]);
2237            }
2238        }
2239        let empty = HashSet::new();
2240        reset_commit_parent_lookups();
2241        let selection = enumerate_pack_objects(
2242            repo.odb(),
2243            &[new_tip],
2244            &haves,
2245            &PackEnumerateOptions {
2246                filter: None,
2247                shallow_grafts: &empty,
2248                have_shallow_grafts: Some(&empty),
2249                use_bitmaps: false,
2250                exclude_objects: None,
2251                peer_odb: None,
2252                have_boundary: HaveBoundary::TipsOnly,
2253            },
2254        )
2255        .expect("enumerate");
2256        assert!(
2257            selection.send.len() <= 8,
2258            "one new commit should stay small with many haves, got {}",
2259            selection.send.len()
2260        );
2261        assert!(
2262            commit_parent_lookups() <= 512,
2263            "many advertised haves must not pre-walk deep history ({} parent lookups)",
2264            commit_parent_lookups()
2265        );
2266    }
2267
2268    #[test]
2269    fn send_pack_have_walk_stops_without_scanning_deep_haves() {
2270        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2271
2272        let dir = TempDir::new().unwrap();
2273        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2274        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2275        let mut parent = write_commit(
2276            &repo,
2277            write_tree(&repo, &[(0o100644, "f", blob("0"))]),
2278            &[],
2279            0,
2280        );
2281        let near_root = parent;
2282        for i in 1..800 {
2283            let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
2284            parent = write_commit(&repo, tree, &[parent], i);
2285        }
2286        let direct_parent = parent;
2287        let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
2288        let new_tip = write_commit(&repo, new_tree, &[direct_parent], 900);
2289        let empty = HashSet::new();
2290        reset_commit_reads();
2291        let have_commits = send_pack_uninteresting_commits(
2292            repo.odb(),
2293            &[new_tip],
2294            &[direct_parent, near_root],
2295            &empty,
2296            &CancelSignal::never(),
2297        )
2298        .expect("send-pack have walk");
2299        assert!(have_commits.contains(&direct_parent));
2300        assert!(have_commits.contains(&near_root));
2301        let reads = commit_reads();
2302        assert!(
2303            reads < 20,
2304            "send-pack have walk should not scan deep history, read {reads} commits"
2305        );
2306    }
2307
2308    #[test]
2309    fn walk_lists_trees_of_newer_commits_first() {
2310        let dir = TempDir::new().unwrap();
2311        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2312        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2313        let tree_a = write_tree(&repo, &[(0o100644, "f", blob("a"))]);
2314        let tree_b = write_tree(&repo, &[(0o100644, "f", blob("b"))]);
2315        let tree_c = write_tree(&repo, &[(0o100644, "f", blob("c"))]);
2316        let base = write_commit(&repo, tree_a, &[], 100);
2317        // Two branches: the older tip is listed as the first root.
2318        let old_tip = write_commit(&repo, tree_b, &[base], 200);
2319        let new_tip = write_commit(&repo, tree_c, &[base], 300);
2320
2321        let selection = walk(&repo, &[old_tip, new_tip], NameTrees::No);
2322        let pos = |oid: ObjectId| selection.send.iter().position(|o| *o == oid).unwrap();
2323        assert!(pos(old_tip) < pos(base) && pos(new_tip) < pos(base));
2324        assert!(pos(tree_c) < pos(tree_b) && pos(tree_b) < pos(tree_a));
2325    }
2326
2327    #[test]
2328    fn walk_names_trees_and_keeps_history_paths_for_path_less_roots() {
2329        let dir = TempDir::new().unwrap();
2330        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2331        let file = repo.odb().write(ObjectKind::Blob, b"x\n").unwrap();
2332        let link = repo.odb().write(ObjectKind::Blob, b"target").unwrap();
2333        let sub = write_tree(
2334            &repo,
2335            &[(0o100644, "file.rs", file), (0o120000, "link", link)],
2336        );
2337        let root = write_tree(&repo, &[(0o040000, "src", sub)]);
2338        let commit = write_commit(&repo, root, &[], 100);
2339
2340        // Index roots (the subtree and the file) come first and have no path.
2341        let selection = walk(&repo, &[sub, file, commit], NameTrees::Yes);
2342        let hash = |oid: ObjectId| selection.name_hashes.get(&oid).copied();
2343        assert_eq!(hash(sub), Some(pack_name_hash("src")));
2344        assert_eq!(hash(file), Some(pack_name_hash("src/file.rs")));
2345        assert_eq!(hash(root), None, "root trees have no path");
2346        assert_eq!(selection.send.len(), 5);
2347
2348        let blobs_only = walk(&repo, &[sub, file, commit], NameTrees::No);
2349        assert_eq!(blobs_only.name_hashes.get(&sub), None);
2350        assert_eq!(
2351            blobs_only.name_hashes.get(&file),
2352            Some(&pack_name_hash("src/file.rs"))
2353        );
2354    }
2355
2356    fn tree_entry(mode: &str, name: &str, oid: ObjectId) -> Vec<u8> {
2357        let mut bytes = format!("{mode} {name}\0").into_bytes();
2358        bytes.extend_from_slice(oid.as_bytes());
2359        bytes
2360    }
2361
2362    /// Old tools wrote tree entries with non-canonical modes (`40755` for a
2363    /// directory, `100664` for a file). Git walks them by their type bits, so the
2364    /// enumeration must too, or everything below such a directory is left out.
2365    #[cfg_attr(
2366        not(feature = "full-tests"),
2367        ignore = "starts system git or is heavy; run with --features full-tests"
2368    )]
2369    #[test]
2370    fn walk_descends_into_trees_with_non_canonical_modes() {
2371        let tmp = tempfile::tempdir().unwrap();
2372        Repository::init(tmp.path(), &InitOptions::new().with_bare(true)).unwrap();
2373        let repo = Repository::open(tmp.path(), &RepositoryOptions::empty()).unwrap();
2374        let odb = repo.odb();
2375        let deep = odb
2376            .write_loose_materialize(ObjectKind::Blob, b"deep\n")
2377            .unwrap();
2378        let odd = odb
2379            .write_loose_materialize(ObjectKind::Blob, b"odd\n")
2380            .unwrap();
2381        let inner = odb
2382            .write_loose_materialize(ObjectKind::Tree, &tree_entry("100644", "deep.txt", deep))
2383            .unwrap();
2384        let mut root = tree_entry("100664", "odd.txt", odd);
2385        root.extend(tree_entry("40755", "sub", inner));
2386        let root = odb
2387            .write_loose_materialize(ObjectKind::Tree, &root)
2388            .unwrap();
2389        let commit = format!(
2390            "tree {root}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nodd modes\n"
2391        );
2392        let commit = odb
2393            .write_loose_materialize(ObjectKind::Commit, commit.as_bytes())
2394            .unwrap();
2395
2396        let empty = HashSet::new();
2397        let selection = enumerate_pack_objects(
2398            odb,
2399            &[commit],
2400            &[],
2401            &PackEnumerateOptions {
2402                filter: None,
2403                shallow_grafts: &empty,
2404                have_shallow_grafts: Some(&empty),
2405                use_bitmaps: false,
2406                exclude_objects: None,
2407                peer_odb: None,
2408                have_boundary: HaveBoundary::CommitClosure,
2409            },
2410        )
2411        .unwrap();
2412
2413        let ours: HashSet<ObjectId> = selection.send.iter().copied().collect();
2414        let git: HashSet<ObjectId> =
2415            grit_test_support::git(tmp.path(), &["rev-list", "--objects", &commit.to_hex()])
2416                .lines()
2417                .map(|line| ObjectId::from_hex(&line[..40]).unwrap())
2418                .collect();
2419        assert_eq!(ours, git, "same objects as git rev-list --objects");
2420        assert!(ours.contains(&inner) && ours.contains(&deep));
2421        assert_eq!(selection.send.len(), 5);
2422        assert_eq!(
2423            selection.name_hashes.get(&deep),
2424            Some(&pack_name_hash("sub/deep.txt"))
2425        );
2426        assert_eq!(
2427            selection.name_hashes.get(&odd),
2428            Some(&pack_name_hash("odd.txt"))
2429        );
2430    }
2431
2432    fn tips_only_options(empty: &HashSet<ObjectId>) -> PackEnumerateOptions<'_> {
2433        PackEnumerateOptions {
2434            filter: None,
2435            shallow_grafts: empty,
2436            have_shallow_grafts: Some(empty),
2437            use_bitmaps: false,
2438            exclude_objects: None,
2439            peer_odb: None,
2440            have_boundary: HaveBoundary::TipsOnly,
2441        }
2442    }
2443
2444    fn write_tag(repo: &Repository, target: ObjectId, kind: &str, name: &str) -> ObjectId {
2445        let body = format!(
2446            "object {}\ntype {kind}\ntag {name}\ntagger A <a@example.com> 0 +0000\n\n{name}\n",
2447            target.to_hex()
2448        );
2449        repo.odb().write(ObjectKind::Tag, body.as_bytes()).unwrap()
2450    }
2451
2452    #[test]
2453    fn tips_only_thin_bases_come_from_edge_commits_not_every_advertised_tip() {
2454        let dir = TempDir::new().unwrap();
2455        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2456        let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2457        let base_tree = write_tree(&repo, &[(0o100644, "f", blob("base"))]);
2458        let root = write_commit(&repo, base_tree, &[], 0);
2459        let main = write_commit(
2460            &repo,
2461            write_tree(&repo, &[(0o100644, "f", blob("main"))]),
2462            &[root],
2463            10,
2464        );
2465        let mut haves = vec![main];
2466        for i in 0..40 {
2467            let tree = write_tree(&repo, &[(0o100644, "f", blob(&format!("side {i}")))]);
2468            let side = write_commit(&repo, tree, &[root], 20 + i);
2469            haves.push(write_tag(&repo, side, "commit", &format!("t{i}")));
2470            haves.push(side);
2471        }
2472        let new_tip = write_commit(
2473            &repo,
2474            write_tree(&repo, &[(0o100644, "f", blob("main\nmore"))]),
2475            &[main],
2476            100,
2477        );
2478        let empty = HashSet::new();
2479        let selection =
2480            enumerate_pack_objects(repo.odb(), &[new_tip], &haves, &tips_only_options(&empty))
2481                .unwrap();
2482        assert_eq!(selection.send.len(), 3, "commit, tree and the changed blob");
2483        assert_eq!(
2484            selection.have_closure,
2485            HashSet::from([main]),
2486            "thin bases come from the commit the walk stopped at"
2487        );
2488
2489        let closure = enumerate_pack_objects(
2490            repo.odb(),
2491            &[new_tip],
2492            &haves,
2493            &PackEnumerateOptions {
2494                have_boundary: HaveBoundary::CommitClosure,
2495                ..tips_only_options(&empty)
2496            },
2497        )
2498        .unwrap();
2499        assert_eq!(closure.send, selection.send);
2500        assert_eq!(
2501            closure.have_closure,
2502            HashSet::from([main]),
2503            "the full have pre-walk seeds thin bases from the same edge commit"
2504        );
2505    }
2506
2507    #[test]
2508    fn send_pack_walk_peels_tag_haves_and_ignores_non_commit_targets() {
2509        use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2510        let dir = TempDir::new().unwrap();
2511        let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2512        let blob = repo.odb().write(ObjectKind::Blob, b"x").unwrap();
2513        let tree = write_tree(&repo, &[(0o100644, "f", blob)]);
2514        let old = write_commit(&repo, tree, &[], 0);
2515        let tip = write_commit(&repo, tree, &[old], 1);
2516        let tag = write_tag(&repo, tip, "commit", "v1");
2517        let nested = write_tag(&repo, tag, "tag", "v1-nested");
2518        let tree_tag = write_tag(&repo, tree, "tree", "tree-tag");
2519        let blob_tag = write_tag(&repo, blob, "blob", "blob-tag");
2520        let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
2521        let new_tip = write_commit(&repo, tree, &[tip], 2);
2522
2523        reset_commit_reads();
2524        let hidden = send_pack_uninteresting_commits(
2525            repo.odb(),
2526            &[new_tip],
2527            &[nested, tag, tree_tag, blob_tag, missing],
2528            &HashSet::new(),
2529            &CancelSignal::never(),
2530        )
2531        .unwrap();
2532        assert!(hidden.contains(&tip), "tags peel to the commit they name");
2533        for oid in [
2534            tag, nested, tree_tag, blob_tag, tree, blob, missing, new_tip,
2535        ] {
2536            assert!(!hidden.contains(&oid), "{oid} is not a have-side commit");
2537        }
2538        assert_eq!(
2539            commit_reads(),
2540            2,
2541            "the want and the tag-named commit, each queued once"
2542        );
2543    }
2544
2545    #[test]
2546    fn common_prefix_and_suffix_lengths_cover_chunked_and_tail_bytes() {
2547        let a: Vec<u8> = (0..100u8).collect();
2548        assert_eq!(common_prefix_len(&a, &a), 100);
2549        assert_eq!(common_suffix_len(&a, &a), 100);
2550        assert_eq!(common_prefix_len(&a, &[]), 0);
2551        assert_eq!(common_suffix_len(&[], &a), 0);
2552        for at in [0, 1, 15, 16, 17, 31, 32, 63, 64, 99] {
2553            let mut b = a.clone();
2554            b[at] ^= 0xff;
2555            assert_eq!(common_prefix_len(&a, &b), at, "prefix, mismatch at {at}");
2556            assert_eq!(
2557                common_suffix_len(&a, &b),
2558                99 - at,
2559                "suffix, mismatch at {at}"
2560            );
2561        }
2562        // Different lengths: the prefix stops at the shorter end, the suffix aligns ends.
2563        assert_eq!(common_prefix_len(&a[..40], &a), 40);
2564        assert_eq!(common_suffix_len(&a[37..], &a), 63);
2565        assert_eq!(common_suffix_len(&a[..50], &a), 0);
2566    }
2567
2568    /// Every commit, tree and blob reachable from `tip`, with the paths each blob has as
2569    /// a regular file.
2570    fn reachable_with_blob_paths(
2571        repo: &Repository,
2572        tip: ObjectId,
2573    ) -> (HashSet<ObjectId>, HashMap<ObjectId, HashSet<String>>) {
2574        let mut objects = HashSet::new();
2575        let mut blob_paths: HashMap<ObjectId, HashSet<String>> = HashMap::new();
2576        let mut commits = vec![tip];
2577        let mut trees = Vec::new();
2578        while let Some(commit) = commits.pop() {
2579            if !objects.insert(commit) {
2580                continue;
2581            }
2582            let obj = repo.odb().read(&commit).unwrap();
2583            let parsed = CommitRef::parse(&obj.data).unwrap();
2584            commits.extend(parsed.parents().iter().copied());
2585            trees.push((parsed.tree(), String::new()));
2586        }
2587        while let Some((tree, path)) = trees.pop() {
2588            objects.insert(tree);
2589            let obj = repo.odb().read(&tree).unwrap();
2590            for entry in TreeRefIter::with_oid_len(&obj.data, 20) {
2591                let entry = entry.unwrap();
2592                let child = if path.is_empty() {
2593                    String::from_utf8(entry.name.to_vec()).unwrap()
2594                } else {
2595                    format!("{path}/{}", String::from_utf8_lossy(entry.name))
2596                };
2597                match entry.mode {
2598                    0o160000 => {}
2599                    0o040000 => trees.push((entry.oid, child)),
2600                    0o120000 => {
2601                        objects.insert(entry.oid);
2602                    }
2603                    _ => {
2604                        objects.insert(entry.oid);
2605                        blob_paths.entry(entry.oid).or_default().insert(child);
2606                    }
2607                }
2608            }
2609        }
2610        (objects, blob_paths)
2611    }
2612
2613    fn assert_walk_lists_reachable(repo: &Repository, tip: ObjectId) {
2614        let walked = walk(repo, &[tip], NameTrees::Yes);
2615        let listed: HashSet<ObjectId> = walked.send.iter().copied().collect();
2616        assert_eq!(listed.len(), walked.send.len(), "nothing is listed twice");
2617        let (expected, blob_paths) = reachable_with_blob_paths(repo, tip);
2618        assert_eq!(listed, expected);
2619        for (blob, paths) in &blob_paths {
2620            let hash = walked.name_hashes.get(blob).copied();
2621            assert!(
2622                paths.iter().any(|p| Some(pack_name_hash(p)) == hash),
2623                "blob {blob} is named after one of its paths {paths:?}"
2624            );
2625        }
2626    }
2627
2628    /// Deterministic xorshift so the edit sequence is the same on every run.
2629    struct Xorshift(u64);
2630
2631    impl Xorshift {
2632        fn below(&mut self, n: usize) -> usize {
2633            self.0 ^= self.0 << 13;
2634            self.0 ^= self.0 >> 7;
2635            self.0 ^= self.0 << 17;
2636            (self.0 % n as u64) as usize
2637        }
2638    }
2639
2640    #[test]
2641    fn walk_lists_every_entry_of_successive_tree_versions() {
2642        let dir = TempDir::new().unwrap();
2643        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2644        let mut rng = Xorshift(0x2545_f491_4f6c_dd1d);
2645        let mut next_blob = 0usize;
2646        let mut new_blob = |repo: &Repository| {
2647            next_blob += 1;
2648            repo.odb()
2649                .write(ObjectKind::Blob, format!("blob {next_blob}\n").as_bytes())
2650                .unwrap()
2651        };
2652        // Names of varying length so edits shift every later entry's offset.
2653        let mut root: std::collections::BTreeMap<String, ObjectId> = (0..40)
2654            .map(|i| {
2655                (
2656                    format!("f{}", "x".repeat(i % 7) + &i.to_string()),
2657                    new_blob(&repo),
2658                )
2659            })
2660            .collect();
2661        let mut sub: std::collections::BTreeMap<String, ObjectId> = (0..12)
2662            .map(|i| (format!("s{i}"), new_blob(&repo)))
2663            .collect();
2664        let gitlink = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
2665        let link_target = repo.odb().write(ObjectKind::Blob, b"target").unwrap();
2666        let mut parent: Option<ObjectId> = None;
2667        for version in 0..300i64 {
2668            let files = if rng.below(3) == 0 {
2669                &mut sub
2670            } else {
2671                &mut root
2672            };
2673            let names: Vec<String> = files.keys().cloned().collect();
2674            match rng.below(4) {
2675                0 if names.len() > 2 => {
2676                    files.remove(&names[rng.below(names.len())]);
2677                }
2678                1 => {
2679                    let name = format!("f{}", "y".repeat(rng.below(5)) + &version.to_string());
2680                    files.insert(name, new_blob(&repo));
2681                }
2682                _ => {
2683                    let name = names[rng.below(names.len())].clone();
2684                    // Restoring an earlier blob makes a version repeat older bytes.
2685                    let blob = if rng.below(4) == 0 {
2686                        *files.values().next().unwrap()
2687                    } else {
2688                        new_blob(&repo)
2689                    };
2690                    files.insert(name, blob);
2691                }
2692            }
2693            let sub_entries: Vec<(u32, &str, ObjectId)> = sub
2694                .iter()
2695                .map(|(name, &oid)| (0o100644, name.as_str(), oid))
2696                .collect();
2697            let sub_tree = write_tree(&repo, &sub_entries);
2698            let mut entries: Vec<(u32, &str, ObjectId)> = vec![
2699                (0o160000, "a-module", gitlink),
2700                (0o120000, "a-link", link_target),
2701                (0o040000, "d", sub_tree),
2702            ];
2703            entries.extend(
2704                root.iter()
2705                    .map(|(name, &oid)| (0o100644, name.as_str(), oid)),
2706            );
2707            entries.sort_by(|a, b| a.1.cmp(b.1));
2708            let tree = write_tree(&repo, &entries);
2709            let parents: Vec<ObjectId> = parent.into_iter().collect();
2710            parent = Some(write_commit(&repo, tree, &parents, version));
2711        }
2712        assert_walk_lists_reachable(&repo, parent.unwrap());
2713    }
2714
2715    #[test]
2716    fn walk_reparses_a_tree_whose_shared_tail_starts_inside_an_entry() {
2717        let dir = TempDir::new().unwrap();
2718        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2719        let kept = repo.odb().write(ObjectKind::Blob, b"kept").unwrap();
2720        let older = repo.odb().write(ObjectKind::Blob, b"older").unwrap();
2721        let newer = repo.odb().write(ObjectKind::Blob, b"newer").unwrap();
2722        // The newest commit's tree is expanded first. `100644 g\0<kept>` is the whole
2723        // second entry of the older tree and the tail of the newer tree's entry named
2724        // `w100644 g`, so the trees share their last bytes from an offset that is no
2725        // entry start in the tree expanded before.
2726        let old_tree = write_tree(&repo, &[(0o100644, "a", older), (0o100644, "g", kept)]);
2727        let new_tree = write_tree(
2728            &repo,
2729            &[(0o100644, "a", newer), (0o100644, "w100644 g", kept)],
2730        );
2731        let old = write_commit(&repo, old_tree, &[], 1);
2732        let new = write_commit(&repo, new_tree, &[old], 2);
2733        assert_walk_lists_reachable(&repo, new);
2734    }
2735
2736    #[cfg_attr(
2737        not(feature = "full-tests"),
2738        ignore = "starts system git; run with --features full-tests"
2739    )]
2740    #[test]
2741    fn walk_matches_git_rev_list_objects_on_edited_history() {
2742        let dir = TempDir::new().unwrap();
2743        let git = |args: &[&str]| {
2744            let out = std::process::Command::new("git")
2745                .current_dir(dir.path())
2746                .args(args)
2747                .env("GIT_CONFIG_GLOBAL", "/dev/null")
2748                .env("GIT_CONFIG_SYSTEM", "/dev/null")
2749                .env("GIT_AUTHOR_NAME", "T")
2750                .env("GIT_AUTHOR_EMAIL", "t@example.com")
2751                .env("GIT_COMMITTER_NAME", "T")
2752                .env("GIT_COMMITTER_EMAIL", "t@example.com")
2753                .output()
2754                .expect("run git");
2755            assert!(out.status.success(), "git {args:?}: {out:?}");
2756            String::from_utf8(out.stdout).unwrap()
2757        };
2758        git(&["init", "-q", "-b", "main"]);
2759        let mut rng = Xorshift(0x9e37_79b9_7f4a_7c15);
2760        for i in 0..60 {
2761            let dir_name = ["src", "src/deep", "docs", "."][rng.below(4)];
2762            std::fs::create_dir_all(dir.path().join(dir_name)).unwrap();
2763            let file = dir
2764                .path()
2765                .join(dir_name)
2766                .join(format!("f{}", rng.below(15)));
2767            if rng.below(5) == 0 && file.exists() {
2768                std::fs::remove_file(&file).unwrap();
2769            } else {
2770                std::fs::write(&file, format!("{i}\n")).unwrap();
2771            }
2772            git(&["add", "-A"]);
2773            git(&["commit", "-q", "--allow-empty", "-m", &format!("c{i}")]);
2774        }
2775        git(&["repack", "-adq"]);
2776        let expected: HashSet<ObjectId> = git(&["rev-list", "--objects", "--all"])
2777            .lines()
2778            .map(|line| ObjectId::from_hex(&line[..40]).unwrap())
2779            .collect();
2780        let repo =
2781            Repository::open(&dir.path().join(".git"), &RepositoryOptions::empty()).expect("open");
2782        let tip = ObjectId::from_hex(git(&["rev-parse", "HEAD"]).trim()).unwrap();
2783        let walked = walk(&repo, &[tip], NameTrees::Yes);
2784        let listed: HashSet<ObjectId> = walked.send.iter().copied().collect();
2785        assert_eq!(listed.len(), walked.send.len());
2786        assert_eq!(listed, expected);
2787        assert_walk_lists_reachable(&repo, tip);
2788    }
2789}