Skip to main content

grit_lib/
bitmap_walk.rs

1//! Reachability queries using pack / MIDX commit bitmaps (Git `prepare_bitmap_walk` semantics).
2//!
3//! Computes want/have sets with optional object filters, falling back to callers when a query
4//! cannot be served from on-disk bitmaps alone.
5
6use std::collections::{BinaryHeap, HashSet, VecDeque};
7
8use crate::error::Error;
9use crate::ewah_bitmap::Bitmap;
10use crate::objects::{
11    empty_oid_set, oid_hash_set, parse_tag, CommitRef, ObjectId, ObjectKind, OidHashSet,
12    TreeRefIter,
13};
14use crate::pack::read_object_info_from_packs;
15use crate::pack_bitmap::{BitmapError, BitmapIndex};
16use crate::repo::Repository;
17use crate::rev_list::{shallow_boundary_oids, FilterObjectKind, MissingAction, ObjectFilter};
18
19/// Query parameters for a bitmap reachability walk.
20#[derive(Debug, Clone, Copy)]
21pub struct ReachabilityQuery<'a> {
22    /// Tips whose reachable objects are included in the result (after subtracting [`Self::haves`]).
23    pub wants: &'a [ObjectId],
24    /// Tips whose reachable objects are excluded from the result.
25    pub haves: &'a [ObjectId],
26    /// Optional object filter (see [`ObjectFilter`]); unsupported specs yield [`BitmapWalkError::Unsupported`].
27    pub filter: Option<&'a ObjectFilter>,
28}
29
30/// Bitmap walk could not run (unsupported filter or shallow repo); use a non-bitmap walk instead.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
32#[error("bitmap reachability walk is not supported for this repository or query")]
33pub struct BitmapWalkUnsupported;
34
35/// Errors during a bitmap reachability walk.
36#[derive(Debug, thiserror::Error)]
37pub enum BitmapWalkError {
38    /// Filter or repository shape requires a non-bitmap walk.
39    #[error(transparent)]
40    Unsupported(#[from] BitmapWalkUnsupported),
41    /// On-disk bitmap could not be read.
42    #[error(transparent)]
43    Bitmap(#[from] BitmapError),
44    /// Repository or object access failure.
45    #[error(transparent)]
46    Repository(#[from] Error),
47}
48
49/// Reachable objects from a [`ReachabilityQuery`]: in-index bits plus out-of-namespace oids.
50#[derive(Clone)]
51pub struct ReachableSet {
52    index: std::sync::Arc<BitmapIndex>,
53    bits: Bitmap,
54    extended: Vec<(ObjectId, ObjectKind)>,
55    extended_set: OidHashSet,
56}
57
58impl ReachableSet {
59    /// Total number of reachable objects (indexed and extended).
60    #[must_use]
61    pub fn count(&self) -> usize {
62        self.bits.count_ones() + self.extended.len()
63    }
64
65    /// Whether `oid` is in this set.
66    #[must_use]
67    pub fn contains(&self, oid: &ObjectId) -> bool {
68        if self.extended_set.contains(oid) {
69            return true;
70        }
71        self.index
72            .position_of(oid)
73            .is_some_and(|pos| self.bits.get(pos as usize))
74    }
75
76    /// Count reachable objects of `kind`.
77    ///
78    /// # Errors
79    ///
80    /// Propagates bitmap type-index decode failures.
81    pub fn count_by_kind(&self, kind: ObjectKind) -> std::result::Result<usize, BitmapWalkError> {
82        self.count_by_kind_popcount(kind)
83    }
84
85    /// Count reachable objects of `kind` via type-bitmap intersection popcount.
86    ///
87    /// # Errors
88    ///
89    /// Propagates bitmap type-index decode failures.
90    pub fn count_by_kind_popcount(
91        &self,
92        kind: ObjectKind,
93    ) -> std::result::Result<usize, BitmapWalkError> {
94        let mut type_bits = Bitmap::new();
95        self.index
96            .type_bitmap(kind)
97            .expand_into_bitmap(&mut type_bits)?;
98        type_bits.and_assign(&self.bits);
99        Ok(type_bits.count_ones() + self.extended.iter().filter(|(_, k)| *k == kind).count())
100    }
101
102    /// Iterate reachable oids of `kind` in ascending bitmap position order, then extended oids sorted.
103    pub fn iter_by_kind(&self, kind: ObjectKind) -> impl Iterator<Item = ObjectId> + '_ {
104        let positions = self.type_filtered_positions(kind);
105        positions
106            .into_iter()
107            .filter_map(|pos| self.index.oid_at(pos).ok())
108            .chain(
109                self.extended
110                    .iter()
111                    .filter_map(move |(oid, k)| (*k == kind).then_some(*oid)),
112            )
113    }
114
115    /// All reachable object ids (indexed namespace and extended) as a set.
116    #[must_use]
117    pub fn object_ids(&self) -> HashSet<ObjectId> {
118        let mut out = oid_hash_set();
119        for pos in self.bits.set_bits() {
120            if let Ok(oid) = self.index.oid_at(u32::try_from(pos).unwrap_or(u32::MAX)) {
121                out.insert(oid);
122            }
123        }
124        for (oid, _) in &self.extended {
125            out.insert(*oid);
126        }
127        out.into_iter().collect()
128    }
129
130    /// Bitmap positions the client still needs (indexed reachability minus haves).
131    #[must_use]
132    pub(crate) fn reuse_send_bits(&self) -> &Bitmap {
133        &self.bits
134    }
135
136    /// Bitmap index backing this set (pack or MIDX namespace).
137    #[must_use]
138    pub fn bitmap_index(&self) -> &std::sync::Arc<BitmapIndex> {
139        &self.index
140    }
141
142    /// Loose / out-of-namespace objects included in this set.
143    #[must_use]
144    pub fn extended_objects(&self) -> &[(ObjectId, ObjectKind)] {
145        &self.extended
146    }
147
148    /// All reachable oids grouped by Git's bitmap output order (commits, trees, blobs, tags).
149    pub fn iter_grouped_by_kind(&self) -> impl Iterator<Item = ObjectId> + '_ {
150        [
151            ObjectKind::Commit,
152            ObjectKind::Tree,
153            ObjectKind::Blob,
154            ObjectKind::Tag,
155        ]
156        .into_iter()
157        .flat_map(|kind| self.iter_by_kind(kind))
158    }
159
160    /// Reachable commit bitmap bit positions in ascending order (indexed namespace only).
161    pub(crate) fn reachable_commit_bitmap_positions(
162        &self,
163    ) -> std::result::Result<Vec<u32>, BitmapWalkError> {
164        let Ok(mut type_bits) = (|| {
165            let mut tb = Bitmap::new();
166            self.index
167                .type_bitmap(ObjectKind::Commit)
168                .expand_into_bitmap(&mut tb)?;
169            Ok::<_, BitmapError>(tb)
170        })() else {
171            return Ok(Vec::new());
172        };
173        type_bits.and_assign(&self.bits);
174        Ok(type_bits
175            .set_bits()
176            .map(|p| u32::try_from(p).unwrap_or(u32::MAX))
177            .collect())
178    }
179
180    fn type_filtered_positions(&self, kind: ObjectKind) -> Vec<u32> {
181        let Ok(mut type_bits) = (|| {
182            let mut tb = Bitmap::new();
183            self.index.type_bitmap(kind).expand_into_bitmap(&mut tb)?;
184            Ok::<_, BitmapError>(tb)
185        })() else {
186            return Vec::new();
187        };
188        type_bits.and_assign(&self.bits);
189        type_bits
190            .set_bits()
191            .map(|p| u32::try_from(p).unwrap_or(u32::MAX))
192            .collect()
193    }
194
195    fn subtract(&mut self, other: &Self) {
196        self.bits.and_not_assign(&other.bits);
197        self.extended.retain(|(oid, _)| !other.contains(oid));
198        self.extended_set
199            .retain(|oid| !other.extended_set.contains(oid));
200    }
201}
202
203/// Returns `true` when `filter` can be applied via type bitmaps (mirrors Git `can_filter_bitmap`).
204#[must_use]
205pub fn bitmap_filter_supported(filter: Option<&ObjectFilter>) -> bool {
206    match filter {
207        None => true,
208        Some(ObjectFilter::SparseOid(_)) => false,
209        Some(ObjectFilter::TreeDepth(depth)) if *depth > 0 => false,
210        Some(ObjectFilter::Combine(parts)) => {
211            parts.iter().all(|p| bitmap_filter_supported(Some(p)))
212        }
213        Some(ObjectFilter::BlobNone)
214        | Some(ObjectFilter::BlobLimit(_))
215        | Some(ObjectFilter::TreeDepth(0))
216        | Some(ObjectFilter::ObjectType(_)) => true,
217        Some(ObjectFilter::TreeDepth(_)) => false,
218    }
219}
220
221/// Commit, tag, and loose object tips after ref peeling (avoids re-reading objects in bitmap walks).
222pub(crate) struct PeeledTips {
223    pub commits: Vec<ObjectId>,
224    pub tags: Vec<ObjectId>,
225    pub extra_objects: Vec<ObjectId>,
226}
227
228fn peel_tips(
229    repo: &Repository,
230    tips: &[ObjectId],
231    missing: MissingAction,
232) -> std::result::Result<PeeledTips, BitmapWalkError> {
233    let mut out = PeeledTips {
234        commits: Vec::new(),
235        tags: Vec::new(),
236        extra_objects: Vec::new(),
237    };
238    for &tip in tips {
239        peel_one(repo, tip, missing, &mut out)?;
240    }
241    Ok(out)
242}
243
244fn peel_one(
245    repo: &Repository,
246    mut oid: ObjectId,
247    missing: MissingAction,
248    out: &mut PeeledTips,
249) -> std::result::Result<(), BitmapWalkError> {
250    loop {
251        let object = match repo.odb().read(&oid) {
252            Ok(obj) => obj,
253            Err(Error::ObjectNotFound(_)) if missing != MissingAction::Error => return Ok(()),
254            Err(err) => return Err(err.into()),
255        };
256        match object.kind {
257            ObjectKind::Commit => {
258                out.commits.push(oid);
259                return Ok(());
260            }
261            ObjectKind::Tag => {
262                out.tags.push(oid);
263                let tag = parse_tag(&object.data)?;
264                oid = tag.object;
265            }
266            ObjectKind::Tree | ObjectKind::Blob => {
267                out.extra_objects.push(oid);
268                return Ok(());
269            }
270        }
271    }
272}
273
274impl BitmapIndex {
275    /// Run `query` against this index and `repo`.
276    ///
277    /// # Errors
278    ///
279    /// Returns [`BitmapWalkError::Unsupported`] for shallow repositories or unsupported filters.
280    pub fn reachability(
281        self: &std::sync::Arc<Self>,
282        repo: &Repository,
283        query: ReachabilityQuery<'_>,
284        missing: MissingAction,
285    ) -> std::result::Result<ReachableSet, BitmapWalkError> {
286        self.reachability_and_haves(repo, query, missing, missing)
287            .map(|(wants, _)| wants)
288    }
289
290    /// Like [`Self::reachability`], also returning every object reachable from the haves
291    /// (unfiltered), which the query computes anyway.
292    ///
293    /// `want_missing` applies to the wants, `have_missing` to the haves.
294    pub(crate) fn reachability_and_haves(
295        self: &std::sync::Arc<Self>,
296        repo: &Repository,
297        query: ReachabilityQuery<'_>,
298        want_missing: MissingAction,
299        have_missing: MissingAction,
300    ) -> std::result::Result<(ReachableSet, ReachableSet), BitmapWalkError> {
301        if !shallow_boundary_oids(repo.git_dir()).is_empty()
302            || crate::shallow::shallow_file_path(repo.git_dir()).is_file()
303        {
304            return Err(BitmapWalkUnsupported.into());
305        }
306        if !bitmap_filter_supported(query.filter) {
307            return Err(BitmapWalkUnsupported.into());
308        }
309
310        let haves = find_objects_union(repo, self, query.haves, have_missing, false, None)?;
311        let mut wants =
312            find_objects_union(repo, self, query.wants, want_missing, false, Some(&haves))?;
313        wants.subtract(&haves);
314        apply_filter(repo, self, &mut wants, query.filter, query.wants)?;
315        Ok((wants, haves))
316    }
317
318    /// Compare the on-disk commit bitmap for `commit` with a fresh non-bitmap walk (`rev-list --test-bitmap`).
319    ///
320    /// Returns `Ok(true)` when they match, `Ok(false)` when the commit has no stored bitmap or the
321    /// bitmap differs, and propagates read/decode errors.
322    ///
323    /// # Errors
324    ///
325    /// Propagates object read failures according to `missing`.
326    pub fn verify_commit(
327        self: &std::sync::Arc<Self>,
328        repo: &Repository,
329        commit: &ObjectId,
330        missing: MissingAction,
331    ) -> std::result::Result<bool, BitmapWalkError> {
332        let Some(stored) = self.commit_bitmap(commit) else {
333            return Ok(false);
334        };
335        let walked = find_objects_union(repo, self, &[*commit], missing, true, None)?;
336        let stored_set: HashSet<u32> = stored.positions().collect();
337        let walked_set: HashSet<u32> = walked
338            .bits
339            .set_bits()
340            .map(|p| u32::try_from(p).unwrap_or(u32::MAX))
341            .collect();
342        Ok(stored_set == walked_set && walked.extended.is_empty())
343    }
344
345    /// Like [`Self::reachability`], but `wants` are already split by peeled kind (no tip re-read).
346    pub(crate) fn reachability_with_peeled_wants(
347        self: &std::sync::Arc<Self>,
348        repo: &Repository,
349        wants: PeeledTips,
350        haves: &[ObjectId],
351        filter: Option<&ObjectFilter>,
352        missing: MissingAction,
353        filter_want_tips: &[ObjectId],
354    ) -> std::result::Result<ReachableSet, BitmapWalkError> {
355        if !shallow_boundary_oids(repo.git_dir()).is_empty()
356            || crate::shallow::shallow_file_path(repo.git_dir()).is_file()
357        {
358            return Err(BitmapWalkUnsupported.into());
359        }
360        if !bitmap_filter_supported(filter) {
361            return Err(BitmapWalkUnsupported.into());
362        }
363
364        let haves = find_objects_union(repo, self, haves, missing, false, None)?;
365        let mut want_set = find_objects_peeled(repo, self, wants, missing, false, Some(&haves))?;
366        want_set.subtract(&haves);
367        apply_filter(repo, self, &mut want_set, filter, filter_want_tips)?;
368        Ok(want_set)
369    }
370}
371
372/// Objects reachable from `tips`.
373///
374/// With `seed`, the walk starts from the objects already in it and stops wherever it
375/// reaches one of them, so the result is everything reachable from `tips` plus `seed`;
376/// a caller subtracting `seed` afterwards does not walk what it subtracts.
377fn find_objects_union(
378    repo: &Repository,
379    index: &std::sync::Arc<BitmapIndex>,
380    tips: &[ObjectId],
381    missing: MissingAction,
382    ignore_stored_commit_bitmaps: bool,
383    seed: Option<&ReachableSet>,
384) -> std::result::Result<ReachableSet, BitmapWalkError> {
385    if tips.is_empty() {
386        return Ok(ReachableSet {
387            index: std::sync::Arc::clone(index),
388            bits: Bitmap::new(),
389            extended: Vec::new(),
390            extended_set: oid_hash_set(),
391        });
392    }
393    let peeled = peel_tips(repo, tips, missing)?;
394    find_objects_peeled(
395        repo,
396        index,
397        peeled,
398        missing,
399        ignore_stored_commit_bitmaps,
400        seed,
401    )
402}
403
404/// [`find_objects_union`] for tips already split by kind.
405fn find_objects_peeled(
406    repo: &Repository,
407    index: &std::sync::Arc<BitmapIndex>,
408    peeled: PeeledTips,
409    missing: MissingAction,
410    ignore_stored_commit_bitmaps: bool,
411    seed: Option<&ReachableSet>,
412) -> std::result::Result<ReachableSet, BitmapWalkError> {
413    let (mut bits, mut extended, mut extended_set) = match seed {
414        Some(seed) => (
415            seed.bits.clone(),
416            seed.extended.clone(),
417            seed.extended_set.clone(),
418        ),
419        None => (Bitmap::new(), Vec::new(), oid_hash_set()),
420    };
421    let mut pending_commits = VecDeque::new();
422
423    for commit in peeled.commits {
424        if try_or_commit_bitmap(index, &mut bits, &commit, ignore_stored_commit_bitmaps)? {
425            continue;
426        }
427        if already_marked(index, &bits, &extended_set, &commit) {
428            continue;
429        }
430        pending_commits.push_back(commit);
431    }
432
433    for tag in peeled.tags {
434        mark_object(
435            repo,
436            index,
437            &mut bits,
438            &mut extended,
439            &mut extended_set,
440            tag,
441            ObjectKind::Tag,
442            missing,
443        )?;
444    }
445    for oid in peeled.extra_objects {
446        let kind = repo
447            .odb()
448            .read_info(&oid)
449            .map_err(BitmapWalkError::from)?
450            .kind;
451        mark_object(
452            repo,
453            index,
454            &mut bits,
455            &mut extended,
456            &mut extended_set,
457            oid,
458            kind,
459            missing,
460        )?;
461    }
462
463    // Commits without a stored bitmap are walked newest first, so the bitmaps of the
464    // recent commits that cover most of the history are OR'ed in before the walk
465    // reaches what they cover, and a commit already covered ends its line of the walk.
466    let mut seen_commits = oid_hash_set();
467    let mut queue = BinaryHeap::new();
468    for commit in pending_commits {
469        if seen_commits.insert(commit) {
470            queue_walk_commit(repo, &mut queue, commit)?;
471        }
472    }
473    while let Some(commit) = queue.pop() {
474        if already_marked(index, &bits, &extended_set, &commit.oid) {
475            continue;
476        }
477        mark_index_or_extended(
478            index,
479            &mut bits,
480            &mut extended,
481            &mut extended_set,
482            commit.oid,
483            ObjectKind::Commit,
484        );
485        walk_tree(
486            repo,
487            index,
488            &mut bits,
489            &mut extended,
490            &mut extended_set,
491            commit.tree,
492            missing,
493        )?;
494        for parent in commit.parents {
495            if !seen_commits.insert(parent)
496                || already_marked(index, &bits, &extended_set, &parent)
497                || try_or_commit_bitmap(index, &mut bits, &parent, ignore_stored_commit_bitmaps)?
498            {
499                continue;
500            }
501            queue_walk_commit(repo, &mut queue, parent)?;
502        }
503    }
504
505    Ok(ReachableSet {
506        index: std::sync::Arc::clone(index),
507        bits,
508        extended,
509        extended_set,
510    })
511}
512
513/// A commit waiting in the bitmap fallback walk, ordered newest first.
514struct WalkCommit {
515    time: i64,
516    seq: std::cmp::Reverse<u64>,
517    oid: ObjectId,
518    tree: ObjectId,
519    parents: Vec<ObjectId>,
520}
521
522impl PartialEq for WalkCommit {
523    fn eq(&self, other: &Self) -> bool {
524        (self.time, self.seq) == (other.time, other.seq)
525    }
526}
527
528impl Eq for WalkCommit {}
529
530impl PartialOrd for WalkCommit {
531    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
532        Some(self.cmp(other))
533    }
534}
535
536impl Ord for WalkCommit {
537    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
538        (self.time, self.seq).cmp(&(other.time, other.seq))
539    }
540}
541
542/// Read `oid` and queue it for the fallback walk; a missing commit is skipped.
543fn queue_walk_commit(
544    repo: &Repository,
545    queue: &mut BinaryHeap<WalkCommit>,
546    oid: ObjectId,
547) -> std::result::Result<(), BitmapWalkError> {
548    let object = match repo.odb().read(&oid) {
549        Ok(obj) => obj,
550        Err(Error::ObjectNotFound(_)) => return Ok(()),
551        Err(err) => return Err(err.into()),
552    };
553    if object.kind != ObjectKind::Commit {
554        return Ok(());
555    }
556    let commit = CommitRef::parse(&object.data)?;
557    queue.push(WalkCommit {
558        time: commit.committer_unix_seconds_for_ordering(),
559        seq: std::cmp::Reverse(queue.len() as u64),
560        oid,
561        tree: commit.tree(),
562        parents: commit.parents().to_vec(),
563    });
564    Ok(())
565}
566
567fn try_or_commit_bitmap(
568    index: &BitmapIndex,
569    bits: &mut Bitmap,
570    commit: &ObjectId,
571    ignore_stored: bool,
572) -> std::result::Result<bool, BitmapWalkError> {
573    if ignore_stored {
574        return Ok(false);
575    }
576    let Some(bm) = index.commit_bitmap(commit) else {
577        return Ok(false);
578    };
579    bm.or_into(bits);
580    Ok(true)
581}
582
583fn already_marked(
584    index: &BitmapIndex,
585    bits: &Bitmap,
586    extended: &OidHashSet,
587    oid: &ObjectId,
588) -> bool {
589    if extended.contains(oid) {
590        return true;
591    }
592    index
593        .position_of(oid)
594        .is_some_and(|pos| bits.get(pos as usize))
595}
596
597fn mark_index_or_extended(
598    index: &BitmapIndex,
599    bits: &mut Bitmap,
600    extended: &mut Vec<(ObjectId, ObjectKind)>,
601    extended_set: &mut OidHashSet,
602    oid: ObjectId,
603    kind: ObjectKind,
604) {
605    if let Some(pos) = index.position_of(&oid) {
606        bits.set(pos as usize);
607    } else if extended_set.insert(oid) {
608        extended.push((oid, kind));
609    }
610}
611
612#[allow(clippy::too_many_arguments)]
613fn mark_object(
614    repo: &Repository,
615    index: &BitmapIndex,
616    bits: &mut Bitmap,
617    extended: &mut Vec<(ObjectId, ObjectKind)>,
618    extended_set: &mut OidHashSet,
619    oid: ObjectId,
620    kind: ObjectKind,
621    missing: MissingAction,
622) -> std::result::Result<(), BitmapWalkError> {
623    if already_marked(index, bits, extended_set, &oid) {
624        return Ok(());
625    }
626    match kind {
627        ObjectKind::Commit | ObjectKind::Tag | ObjectKind::Blob => {
628            mark_index_or_extended(index, bits, extended, extended_set, oid, kind);
629        }
630        ObjectKind::Tree => {
631            walk_tree(repo, index, bits, extended, extended_set, oid, missing)?;
632        }
633    }
634    Ok(())
635}
636
637fn walk_tree(
638    repo: &Repository,
639    index: &BitmapIndex,
640    bits: &mut Bitmap,
641    extended: &mut Vec<(ObjectId, ObjectKind)>,
642    extended_set: &mut OidHashSet,
643    tree: ObjectId,
644    missing: MissingAction,
645) -> std::result::Result<(), BitmapWalkError> {
646    if already_marked(index, bits, extended_set, &tree) {
647        return Ok(());
648    }
649    let object = match repo.odb().read(&tree) {
650        Ok(obj) => obj,
651        Err(Error::ObjectNotFound(_)) if missing != MissingAction::Error => return Ok(()),
652        Err(err) => return Err(err.into()),
653    };
654    if object.kind != ObjectKind::Tree {
655        return Ok(());
656    }
657    mark_index_or_extended(index, bits, extended, extended_set, tree, ObjectKind::Tree);
658    let oid_len = repo.odb().hash_algo().len();
659    for entry in TreeRefIter::with_oid_len(&object.data, oid_len) {
660        let entry = entry?;
661        // Submodule gitlinks name commits in another object store; do not walk them.
662        if entry.mode == 0o160000 {
663            continue;
664        }
665        if entry.mode == 0o040000 {
666            walk_tree(
667                repo,
668                index,
669                bits,
670                extended,
671                extended_set,
672                entry.oid,
673                missing,
674            )?;
675        } else {
676            mark_index_or_extended(
677                index,
678                bits,
679                extended,
680                extended_set,
681                entry.oid,
682                ObjectKind::Blob,
683            );
684        }
685    }
686    Ok(())
687}
688
689fn apply_filter(
690    repo: &Repository,
691    index: &BitmapIndex,
692    set: &mut ReachableSet,
693    filter: Option<&ObjectFilter>,
694    want_tips: &[ObjectId],
695) -> std::result::Result<(), BitmapWalkError> {
696    let Some(filter) = filter else {
697        return Ok(());
698    };
699    apply_filter_one(repo, index, set, filter, want_tips)?;
700    Ok(())
701}
702
703fn apply_filter_one(
704    repo: &Repository,
705    index: &BitmapIndex,
706    set: &mut ReachableSet,
707    filter: &ObjectFilter,
708    want_tips: &[ObjectId],
709) -> std::result::Result<(), BitmapWalkError> {
710    match filter {
711        ObjectFilter::Combine(parts) => {
712            for part in parts {
713                apply_filter_one(repo, index, set, part, want_tips)?;
714            }
715        }
716        ObjectFilter::BlobNone => {
717            let keep = explicit_filter_roots(repo, index, want_tips)?.blobs;
718            clear_type_except(index, &mut set.bits, ObjectKind::Blob, &keep);
719            set.extended
720                .retain(|(oid, kind)| *kind != ObjectKind::Blob || keep.contains(oid));
721            rebuild_extended_set(set);
722        }
723        ObjectFilter::BlobLimit(limit) => {
724            let tips = tip_blob_oids(repo, index, want_tips)?;
725            filter_blob_limit(repo, index, &mut set.bits, &tips, *limit)?;
726            set.extended.retain(|(oid, kind)| {
727                if tips.contains(oid) {
728                    return true;
729                }
730                *kind != ObjectKind::Blob || blob_under_limit(repo, oid, *limit)
731            });
732            rebuild_extended_set(set);
733        }
734        ObjectFilter::TreeDepth(0) => {
735            let roots = explicit_filter_roots(repo, index, want_tips)?;
736            clear_type_except(index, &mut set.bits, ObjectKind::Tree, &roots.trees);
737            clear_type_except(index, &mut set.bits, ObjectKind::Blob, &roots.blobs);
738            set.extended.retain(|(oid, kind)| {
739                matches!(kind, ObjectKind::Commit | ObjectKind::Tag)
740                    || roots.trees.contains(oid)
741                    || roots.blobs.contains(oid)
742            });
743            rebuild_extended_set(set);
744        }
745        ObjectFilter::ObjectType(kind) => {
746            keep_only_object_type(set, *kind);
747        }
748        ObjectFilter::SparseOid(_) | ObjectFilter::TreeDepth(_) => {
749            return Err(BitmapWalkUnsupported.into());
750        }
751    }
752    Ok(())
753}
754
755fn rebuild_extended_set(set: &mut ReachableSet) {
756    set.extended_set = set.extended.iter().map(|(oid, _)| *oid).collect();
757}
758
759fn blob_under_limit(repo: &Repository, oid: &ObjectId, limit: u64) -> bool {
760    match read_object_info_from_packs(repo.odb().objects_dir(), oid) {
761        Ok(info) => info.size < limit,
762        Err(_) => repo
763            .odb()
764            .read_info(oid)
765            .map(|info| info.size < limit)
766            .unwrap_or(false),
767    }
768}
769
770fn clear_type(index: &BitmapIndex, bits: &mut Bitmap, kind: ObjectKind) {
771    clear_type_except(index, bits, kind, empty_oid_set());
772}
773
774fn clear_type_except(index: &BitmapIndex, bits: &mut Bitmap, kind: ObjectKind, keep: &OidHashSet) {
775    if let Ok(type_bits) = expand_type(index, kind) {
776        for pos in type_bits.set_bits().collect::<Vec<_>>() {
777            let pos_u32 = u32::try_from(pos).unwrap_or(u32::MAX);
778            if let Ok(oid) = index.oid_at(pos_u32) {
779                if keep.contains(&oid) {
780                    continue;
781                }
782            }
783            bits.clear(pos);
784        }
785    }
786}
787
788struct ExplicitFilterRoots {
789    trees: OidHashSet,
790    blobs: OidHashSet,
791}
792
793fn explicit_filter_roots(
794    repo: &Repository,
795    index: &BitmapIndex,
796    tips: &[ObjectId],
797) -> std::result::Result<ExplicitFilterRoots, BitmapWalkError> {
798    let peeled = peel_tips(repo, tips, MissingAction::Error)?;
799    let mut trees = oid_hash_set();
800    let mut blobs = oid_hash_set();
801    for oid in peeled.extra_objects {
802        match repo
803            .odb()
804            .read_info(&oid)
805            .map_err(BitmapWalkError::from)?
806            .kind
807        {
808            ObjectKind::Tree => {
809                trees.insert(oid);
810            }
811            ObjectKind::Blob => {
812                blobs.insert(oid);
813            }
814            ObjectKind::Commit | ObjectKind::Tag => {}
815        }
816    }
817    for oid in tips {
818        if let Some(pos) = index.position_of(oid) {
819            if index
820                .type_bitmap(ObjectKind::Tree)
821                .contains(pos)
822                .unwrap_or(false)
823            {
824                trees.insert(*oid);
825            }
826            if index
827                .type_bitmap(ObjectKind::Blob)
828                .contains(pos)
829                .unwrap_or(false)
830            {
831                blobs.insert(*oid);
832            }
833        }
834    }
835    Ok(ExplicitFilterRoots { trees, blobs })
836}
837
838fn keep_only_object_type(set: &mut ReachableSet, kind: FilterObjectKind) {
839    let object_kind = match kind {
840        FilterObjectKind::Blob => ObjectKind::Blob,
841        FilterObjectKind::Tree => ObjectKind::Tree,
842        FilterObjectKind::Commit => ObjectKind::Commit,
843        FilterObjectKind::Tag => ObjectKind::Tag,
844    };
845    for k in [
846        ObjectKind::Commit,
847        ObjectKind::Tree,
848        ObjectKind::Blob,
849        ObjectKind::Tag,
850    ] {
851        if k != object_kind {
852            clear_type(&set.index, &mut set.bits, k);
853        }
854    }
855    set.extended.retain(|(_, k)| *k == object_kind);
856    rebuild_extended_set(set);
857}
858
859fn expand_type(
860    index: &BitmapIndex,
861    kind: ObjectKind,
862) -> std::result::Result<Bitmap, BitmapWalkError> {
863    let mut out = Bitmap::new();
864    index
865        .type_bitmap(kind)
866        .expand_into_bitmap(&mut out)
867        .map_err(BitmapWalkError::from)?;
868    Ok(out)
869}
870
871fn tip_blob_oids(
872    repo: &Repository,
873    index: &BitmapIndex,
874    tips: &[ObjectId],
875) -> std::result::Result<OidHashSet, BitmapWalkError> {
876    Ok(explicit_filter_roots(repo, index, tips)?.blobs)
877}
878
879fn filter_blob_limit(
880    repo: &Repository,
881    index: &BitmapIndex,
882    bits: &mut Bitmap,
883    tip_blobs: &OidHashSet,
884    limit: u64,
885) -> std::result::Result<(), BitmapWalkError> {
886    let mut blob_type = expand_type(index, ObjectKind::Blob)?;
887    blob_type.and_assign(bits);
888    let objects_dir = repo.odb().objects_dir();
889    for pos in blob_type.set_bits().collect::<Vec<_>>() {
890        let pos_u32 = u32::try_from(pos).unwrap_or(u32::MAX);
891        let Ok(oid) = index.oid_at(pos_u32) else {
892            continue;
893        };
894        if tip_blobs.contains(&oid) {
895            continue;
896        }
897        let size = read_object_info_from_packs(objects_dir, &oid)
898            .map_err(BitmapWalkError::from)?
899            .size;
900        if size >= limit {
901            bits.clear(pos);
902        }
903    }
904    Ok(())
905}