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