Skip to main content

znippy_plugin_git/
reach.rs

1//! `__gunnar_reach__` — reachability bitmaps.
2//!
3//! For each *selected* commit, the set of objects reachable from it — the commit
4//! itself, its ancestors, and every tree and blob those commits point at —
5//! recorded as a roaring bitmap over **object ordinals**.
6//!
7//! # Two ordinal spaces, one view
8//!
9//! A **sealed archive** numbers its objects in oid-lexicographic order: the order
10//! the sorted lookup sub-index is in and the order `__gunnar_oid__` records in
11//! its `ordinal` column, so a bitmap position is resolvable with the oid index
12//! alone. [`OrdinalSpace::new`] is that space.
13//!
14//! A **live store** numbers its objects in an append-only space
15//! ([`OrdinalTable`]): a sorted base laid down by the first fold, then every
16//! object a later push brings in appended behind it in arrival order. The point
17//! is that a push never renumbers anything — a bitmap is a fact about an
18//! immutable commit, and under the sorted space every push shifted every ordinal
19//! above the new oids, so every stored bitmap had to be rewritten bit by bit
20//! (`carry_reach`, 512 entries × every object, after every push). Under the
21//! stable space a push appends a few ordinals and the table is untouched. The
22//! sorted order is restored only when the store seals or the appended part
23//! outgrows the base, both through one remap table ([`OrdinalTable::remap_to`]).
24//!
25//! # No hex on any hot path
26//!
27//! Everything below the public builders is in integers: the graph is
28//! [`OrdGraph`] (rows and ordinals), the table is [`ReachTable`] (ordinals and
29//! bitmaps), a tree is read by its raw oid and each of its entries is resolved
30//! with one [`OrdinalSpace::ordinal_of_raw`] — a 16-bit fanout and a short
31//! binary search, no decode. GUN-PERF measured the hex form on git.git
32//! (402 227 objects): the build took 85 to 150 s and `ordinal_of_hex` alone was
33//! 51 % of a 65 s clone (gunnar 18c10860).
34//!
35//! # The builder is git's, not a closure memo
36//!
37//! [`build_reach_table`] walks the commits parents-before-children, starts each
38//! commit's bitmap from its parents' and adds the trees with the skip rule
39//! (`should_include_obj`: a set bit means that object AND everything under it
40//! is already accounted for). Work is O(objects + commits), and the memory is
41//! the frontier — the bitmaps of commits whose children are still to come — not
42//! one closure bitmap per distinct tree. The old builder kept the latter and
43//! paid a hex `String` per tree entry on top.
44
45use std::collections::HashMap;
46use std::path::Path;
47use std::sync::Arc;
48
49use anyhow::{Result, anyhow};
50use roaring::RoaringBitmap;
51use znippy_common::GUNNAR_REACH_MODULE;
52use znippy_common::arrow::array::{Array, BinaryArray, BinaryBuilder, StringArray, StringBuilder};
53use znippy_common::arrow::datatypes::{DataType, Field, Schema};
54use znippy_common::arrow::record_batch::RecordBatch;
55use znippy_common::read_reserved_section_bytes;
56
57use crate::graph::CommitNode;
58use crate::object::{GitObjectKind, tree_entries};
59
60/// "No ordinal" / "no row" in the integer tables.
61pub const NONE: u32 = u32::MAX;
62
63/// Which commits get a bitmap.
64#[derive(Debug, Clone, Copy)]
65pub struct ReachPolicy {
66    /// Upper bound on how many bitmaps are sealed. Tips are taken first (a
67    /// commit with no child inside the archive — the branch heads a clone
68    /// negotiates against), then the remaining commits are sampled at an even
69    /// stride through generation order.
70    pub max_commits: usize,
71}
72
73impl Default for ReachPolicy {
74    fn default() -> Self {
75        // git's own bitmap selection is of this order: cover the tips, then
76        // sample the history so a `have` usually lands near a bitmapped commit.
77        Self { max_commits: 512 }
78    }
79}
80
81/// One sealed bitmap, in the sealed archive's spelling (hex commit oid). The
82/// live store keeps a [`ReachTable`]; this is the `__gunnar_reach__` row.
83#[derive(Debug, Clone, PartialEq)]
84pub struct ReachEntry {
85    pub commit: String,
86    pub bitmap: RoaringBitmap,
87}
88
89pub fn reach_schema() -> Arc<Schema> {
90    Arc::new(Schema::new(vec![
91        Field::new("commit_oid", DataType::Utf8, false),
92        Field::new("cardinality", DataType::UInt64, false),
93        Field::new("bitmap", DataType::Binary, false),
94    ]))
95}
96
97/// **Where a tree's bytes come from** — a lookup, not a table, keyed by the
98/// **raw** oid.
99///
100/// It was `&HashMap<String, Vec<u8>>`: every tree payload in the repository,
101/// resident, keyed by a hex `String`. MEASURED on oden 2026-08-30, the real
102/// `torvalds/linux` mirror through [`crate::git_ops::GitStore`]: **28.24 GB**
103/// of tree payloads held on disk so that map could be rebuilt into RAM on every
104/// fold; peak RSS 50.9 GiB on a 16 GB appliance. So the map became a lookup,
105/// and the store's arm resolves a tree the way any other read does.
106///
107/// `None` is *"not a tree this archive holds"* and is an ordinary answer, not
108/// a fault: a blob is a leaf, a gitlink is a commit in another repository, and
109/// a thin push's base may be outside the archive. Every caller treats it as
110/// "stop here".
111pub trait TreeSource: Sync {
112    fn tree(&self, oid: &[u8]) -> Option<std::borrow::Cow<'_, [u8]>>;
113}
114
115/// The eager arm keyed by hex (the sealed builder and the tests): one encode
116/// per tree *visited*, never per entry.
117impl TreeSource for HashMap<String, Vec<u8>> {
118    fn tree(&self, oid: &[u8]) -> Option<std::borrow::Cow<'_, [u8]>> {
119        self.get(&hex::encode(oid)).map(|v| std::borrow::Cow::Borrowed(v.as_slice()))
120    }
121}
122
123impl TreeSource for HashMap<Vec<u8>, Vec<u8>> {
124    fn tree(&self, oid: &[u8]) -> Option<std::borrow::Cow<'_, [u8]>> {
125        self.get(oid).map(|v| std::borrow::Cow::Borrowed(v.as_slice()))
126    }
127}
128
129/// Number of 16-bit prefixes plus one: `fanout[p]` is the first base index whose
130/// two leading bytes are `>= p`, and `fanout[65536]` is the base length. git's
131/// `.idx` has the 8-bit version of the same table; two bytes cut a 400 000-oid
132/// search to about six entries.
133const FANOUT_LEN: usize = (1 << 16) + 1;
134
135fn build_fanout(base: &[u8], oid_len: usize) -> Vec<u32> {
136    let n = if oid_len == 0 { 0 } else { base.len() / oid_len };
137    let mut fan = vec![0u32; FANOUT_LEN];
138    let mut i = 0usize;
139    for p in 0..(1usize << 16) {
140        while i < n {
141            let at = i * oid_len;
142            let key = (usize::from(base[at]) << 8) | usize::from(base[at + 1]);
143            if key >= p {
144                break;
145            }
146            i += 1;
147        }
148        fan[p] = i as u32;
149    }
150    fan[1 << 16] = n as u32;
151    fan
152}
153
154/// **oid ⇄ ordinal, as a `Copy` view that owns nothing.**
155///
156/// Two layouts share it. A sorted buffer alone ([`Self::new`]) is the sealed
157/// archive's space. An [`OrdinalTable`]'s view adds the fanout and the appended
158/// part: ordinal `o < base_len` is the slice at `o * oid_len` of `base`, and
159/// `o >= base_len` is the slice at `(o - base_len) * oid_len` of `extra`, which
160/// is in arrival order and is searched through `extra_sorted` (its ordinals,
161/// sorted by oid).
162#[derive(Clone, Copy)]
163pub struct OrdinalSpace<'a> {
164    base: &'a [u8],
165    /// Empty for a plain sorted buffer; `FANOUT_LEN` entries otherwise.
166    fanout: &'a [u32],
167    extra: &'a [u8],
168    extra_sorted: &'a [u32],
169    oid_len: usize,
170}
171
172impl<'a> OrdinalSpace<'a> {
173    /// Borrow a flat, **sorted**, fixed-width oid buffer as the ordinal space.
174    /// The sort is the caller's invariant (every sealed builder produces it in
175    /// oid-lexicographic order); the binary search below rests on it.
176    pub fn new(raw: &'a [u8], oid_len: usize) -> Self {
177        debug_assert!(oid_len > 0 && raw.len() % oid_len == 0, "ragged ordinal space");
178        Self { base: raw, fanout: &[], extra: &[], extra_sorted: &[], oid_len }
179    }
180
181    /// How many oids the space holds.
182    pub fn len(&self) -> usize {
183        if self.oid_len == 0 { 0 } else { (self.base.len() + self.extra.len()) / self.oid_len }
184    }
185
186    pub fn is_empty(&self) -> bool {
187        self.base.is_empty() && self.extra.is_empty()
188    }
189
190    pub fn oid_len(&self) -> usize {
191        self.oid_len
192    }
193
194    fn base_len(&self) -> usize {
195        if self.oid_len == 0 { 0 } else { self.base.len() / self.oid_len }
196    }
197
198    /// ordinal → raw oid, a slice into the flat buffers. `None` for an ordinal
199    /// outside the space.
200    pub fn oid(&self, ordinal: u32) -> Option<&'a [u8]> {
201        let o = ordinal as usize;
202        let b = self.base_len();
203        if o < b {
204            let at = o * self.oid_len;
205            self.base.get(at..at + self.oid_len)
206        } else {
207            let at = (o - b).checked_mul(self.oid_len)?;
208            self.extra.get(at..at + self.oid_len)
209        }
210    }
211
212    /// raw oid → ordinal: the fanout narrows the base to one 16-bit prefix, a
213    /// binary search finishes it, and the appended part is searched through
214    /// its sorted index. This is the exact inverse of [`Self::oid`].
215    pub fn ordinal_of_raw(&self, oid: &[u8]) -> Option<u32> {
216        if oid.len() != self.oid_len {
217            return None;
218        }
219        let (mut lo, mut hi) = if self.fanout.len() == FANOUT_LEN && self.oid_len >= 2 {
220            let p = (usize::from(oid[0]) << 8) | usize::from(oid[1]);
221            (self.fanout[p] as usize, self.fanout[p + 1] as usize)
222        } else {
223            (0usize, self.base_len())
224        };
225        while lo < hi {
226            let mid = lo + (hi - lo) / 2;
227            let at = mid * self.oid_len;
228            match self.base[at..at + self.oid_len].cmp(oid) {
229                std::cmp::Ordering::Less => lo = mid + 1,
230                std::cmp::Ordering::Greater => hi = mid,
231                std::cmp::Ordering::Equal => return Some(mid as u32),
232            }
233        }
234        if self.extra_sorted.is_empty() {
235            return None;
236        }
237        let b = self.base_len();
238        let (mut lo, mut hi) = (0usize, self.extra_sorted.len());
239        while lo < hi {
240            let mid = lo + (hi - lo) / 2;
241            let o = self.extra_sorted[mid];
242            let at = (o as usize - b) * self.oid_len;
243            match self.extra[at..at + self.oid_len].cmp(oid) {
244                std::cmp::Ordering::Less => lo = mid + 1,
245                std::cmp::Ordering::Greater => hi = mid,
246                std::cmp::Ordering::Equal => return Some(o),
247            }
248        }
249        None
250    }
251
252    /// hex oid → ordinal. Decodes the hex into a **stack** buffer (≤ 32 bytes),
253    /// so a lookup allocates nothing, then searches the raw form. Not on any
254    /// serving path: the walk and the builder carry raw oids.
255    pub fn ordinal_of_hex(&self, hex: &str) -> Option<u32> {
256        let mut buf = [0u8; 32];
257        let dst = buf.get_mut(..self.oid_len)?;
258        hex::decode_to_slice(hex, dst).ok()?;
259        self.ordinal_of_raw(dst)
260    }
261
262    /// The oids in ordinal order, concatenated — what a sealed builder sorts
263    /// and what [`OrdinalTable::from_sorted`] takes.
264    pub fn concat(&self) -> Vec<u8> {
265        let mut v = Vec::with_capacity(self.base.len() + self.extra.len());
266        v.extend_from_slice(self.base);
267        v.extend_from_slice(self.extra);
268        v
269    }
270}
271
272/// **The live store's ordinal space: a sorted base, then arrivals appended.**
273/// Owned; [`Self::view`] is the lookup. See the module header for why it is
274/// append-only.
275#[derive(Clone, Default)]
276pub struct OrdinalTable {
277    oid_len: usize,
278    base: Vec<u8>,
279    fanout: Vec<u32>,
280    extra: Vec<u8>,
281    extra_sorted: Vec<u32>,
282}
283
284impl OrdinalTable {
285    pub fn empty(oid_len: usize) -> Self {
286        Self { oid_len, ..Default::default() }
287    }
288
289    /// A fresh table over a **sorted** full oid list: base only, nothing appended.
290    pub fn from_sorted(raw: Vec<u8>, oid_len: usize) -> Self {
291        debug_assert!(oid_len > 0 && raw.len() % oid_len == 0, "ragged ordinal table");
292        let fanout = build_fanout(&raw, oid_len);
293        Self { oid_len, base: raw, fanout, extra: Vec::new(), extra_sorted: Vec::new() }
294    }
295
296    /// The persisted form, replayed: `full_sorted` is the store's current sorted
297    /// oid list and `extra` the appended part in arrival order. The base is
298    /// `full − extra`. `None` when the two do not agree (an oid in `extra` that
299    /// the store no longer holds, or a width mismatch) — rebuild from scratch.
300    pub fn from_parts(full_sorted: &[u8], extra: &[u8], oid_len: usize) -> Option<Self> {
301        if oid_len == 0 || full_sorted.len() % oid_len != 0 || extra.len() % oid_len != 0 {
302            return None;
303        }
304        let mut ex: Vec<&[u8]> = extra.chunks_exact(oid_len).collect();
305        ex.sort_unstable();
306        ex.dedup();
307        if ex.len() * oid_len != extra.len() {
308            return None;
309        }
310        let mut base = Vec::with_capacity(full_sorted.len().saturating_sub(extra.len()));
311        let mut j = 0usize;
312        for oid in full_sorted.chunks_exact(oid_len) {
313            if j < ex.len() && ex[j] == oid {
314                j += 1;
315            } else {
316                base.extend_from_slice(oid);
317            }
318        }
319        if j != ex.len() {
320            return None;
321        }
322        let mut t = Self::from_sorted(base, oid_len);
323        t.extra = extra.to_vec();
324        t.reindex_extra();
325        Some(t)
326    }
327
328    /// The persisted form, replayed from the two raw parts: `base` must be
329    /// sorted and distinct (the binary search rests on it), `extra` distinct
330    /// and disjoint from it. `None` otherwise — a malformed file is a rebuild,
331    /// never a wrong ordinal.
332    pub fn from_base_and_extra(base: Vec<u8>, extra: Vec<u8>, oid_len: usize) -> Option<Self> {
333        if oid_len == 0 || base.len() % oid_len != 0 || extra.len() % oid_len != 0 {
334            return None;
335        }
336        if base.chunks_exact(oid_len).zip(base.chunks_exact(oid_len).skip(1)).any(|(a, b)| a >= b) {
337            return None;
338        }
339        let mut t = Self::from_sorted(base, oid_len);
340        t.extra = extra;
341        t.reindex_extra();
342        let v = t.view();
343        for (i, o) in t.extra.chunks_exact(oid_len).enumerate() {
344            // Each appended oid must resolve to ITS ordinal: absent from the
345            // base, and not a duplicate inside `extra`.
346            if v.ordinal_of_raw(o) != Some((t.base_len() + i) as u32) {
347                return None;
348            }
349        }
350        Some(t)
351    }
352
353    /// The sorted base, raw — what the persisted form stores.
354    pub fn base_raw(&self) -> &[u8] {
355        &self.base
356    }
357
358    fn reindex_extra(&mut self) {
359        let b = (self.base.len() / self.oid_len) as u32;
360        let n = self.extra.len() / self.oid_len;
361        let mut idx: Vec<u32> = (0..n as u32).map(|i| i + b).collect();
362        let (extra, w, bb) = (&self.extra, self.oid_len, b as usize);
363        idx.sort_unstable_by(|&x, &y| {
364            let a = &extra[(x as usize - bb) * w..(x as usize - bb + 1) * w];
365            let c = &extra[(y as usize - bb) * w..(y as usize - bb + 1) * w];
366            a.cmp(c)
367        });
368        self.extra_sorted = idx;
369    }
370
371    pub fn len(&self) -> usize {
372        if self.oid_len == 0 { 0 } else { (self.base.len() + self.extra.len()) / self.oid_len }
373    }
374
375    pub fn is_empty(&self) -> bool {
376        self.base.is_empty() && self.extra.is_empty()
377    }
378
379    pub fn oid_len(&self) -> usize {
380        self.oid_len
381    }
382
383    pub fn base_len(&self) -> usize {
384        if self.oid_len == 0 { 0 } else { self.base.len() / self.oid_len }
385    }
386
387    /// How many ordinals were appended behind the sorted base.
388    pub fn extra_len(&self) -> usize {
389        if self.oid_len == 0 { 0 } else { self.extra.len() / self.oid_len }
390    }
391
392    /// The appended part, arrival order — what the persisted form stores.
393    pub fn extra_raw(&self) -> &[u8] {
394        &self.extra
395    }
396
397    pub fn view(&self) -> OrdinalSpace<'_> {
398        OrdinalSpace {
399            base: &self.base,
400            fanout: &self.fanout,
401            extra: &self.extra,
402            extra_sorted: &self.extra_sorted,
403            oid_len: self.oid_len,
404        }
405    }
406
407    /// **Follow the store**: `full_sorted` is every oid the store holds now, in
408    /// sorted order. Every oid in it that this table does not number is
409    /// appended (in sorted order among themselves, after everything already
410    /// numbered) and the range of new ordinals is returned. `None` when an oid
411    /// the table numbers is **absent** from `full_sorted` — something was
412    /// removed under the table (a gc, a retain) and the numbering cannot be
413    /// kept; the caller rebuilds from scratch.
414    pub fn follow(&mut self, full_sorted: &[u8]) -> Option<std::ops::Range<u32>> {
415        let w = self.oid_len;
416        if w == 0 || full_sorted.len() % w != 0 {
417            return None;
418        }
419        let before = self.len() as u32;
420        // The known oids in sorted order: a merge of the base and the sorted
421        // view of the appended part.
422        let mut known = KnownIter { table: self, i: 0, j: 0 };
423        let mut appended: Vec<u8> = Vec::new();
424        let mut cur = known.next();
425        for oid in full_sorted.chunks_exact(w) {
426            loop {
427                match cur {
428                    None => {
429                        appended.extend_from_slice(oid);
430                        break;
431                    }
432                    Some(k) if k == oid => {
433                        cur = known.next();
434                        break;
435                    }
436                    Some(k) if k > oid => {
437                        appended.extend_from_slice(oid);
438                        break;
439                    }
440                    // A known oid smaller than the next store oid: the store
441                    // no longer holds it.
442                    Some(_) => return None,
443                }
444            }
445        }
446        if cur.is_some() {
447            return None;
448        }
449        if appended.is_empty() {
450            return Some(before..before);
451        }
452        self.extra.extend_from_slice(&appended);
453        self.reindex_extra();
454        Some(before..self.len() as u32)
455    }
456
457    /// **old ordinal → ordinal in `target`**, as one table, for a seal or a
458    /// compaction. [`NONE`] where `target` lacks the oid. O(n) lookups through
459    /// the target's fanout, once — not once per set bit.
460    pub fn remap_to(&self, target: &OrdinalSpace<'_>) -> Vec<u32> {
461        let me = self.view();
462        (0..me.len() as u32)
463            .map(|o| me.oid(o).and_then(|oid| target.ordinal_of_raw(oid)).unwrap_or(NONE))
464            .collect()
465    }
466}
467
468/// The oids an [`OrdinalTable`] numbers, in sorted order: a merge of the base
469/// and the sorted view of the appended part.
470struct KnownIter<'a> {
471    table: &'a OrdinalTable,
472    i: usize,
473    j: usize,
474}
475
476impl<'a> Iterator for KnownIter<'a> {
477    type Item = &'a [u8];
478    fn next(&mut self) -> Option<&'a [u8]> {
479        let t = self.table;
480        let w = t.oid_len;
481        let base_n = t.base_len();
482        let a = (self.i < base_n).then(|| &t.base[self.i * w..(self.i + 1) * w]);
483        let b = t.extra_sorted.get(self.j).map(|&o| {
484            let at = (o as usize - base_n) * w;
485            &t.extra[at..at + w]
486        });
487        match (a, b) {
488            (Some(a), Some(b)) => {
489                if a <= b {
490                    self.i += 1;
491                    Some(a)
492                } else {
493                    self.j += 1;
494                    Some(b)
495                }
496            }
497            (Some(a), None) => {
498                self.i += 1;
499                Some(a)
500            }
501            (None, Some(b)) => {
502                self.j += 1;
503                Some(b)
504            }
505            (None, None) => None,
506        }
507    }
508}
509
510/// **The commit graph in integers**: one row per commit, rows in the order the
511/// [`CommitNode`] list came in (parents before children, as
512/// [`crate::graph::assign_generations`] guarantees).
513#[derive(Clone, Default)]
514pub struct OrdGraph {
515    /// row → the commit's ordinal.
516    pub ords: Vec<u32>,
517    /// row → the root tree's ordinal, or [`NONE`].
518    pub tree: Vec<u32>,
519    /// row → parent **rows** (a parent outside the graph contributes nothing).
520    pub parents: Vec<Vec<u32>>,
521    /// row → generation.
522    pub generation: Vec<u32>,
523    /// The commit ordinals, as a set.
524    commits: RoaringBitmap,
525    /// rank in `commits` → row.
526    row_by_rank: Vec<u32>,
527}
528
529impl OrdGraph {
530    /// Decode each node's hex **once** and resolve it against the space. A node
531    /// whose oid the space does not hold is kept as a row with no ordinal (it
532    /// cannot be asked for by ordinal, and its children still see it as a
533    /// parent row).
534    pub fn from_nodes(nodes: &[CommitNode], space: &OrdinalSpace<'_>) -> Self {
535        let n = nodes.len();
536        let mut ords = Vec::with_capacity(n);
537        let mut commits = RoaringBitmap::new();
538        for node in nodes {
539            let o = space.ordinal_of_hex(&node.oid).unwrap_or(NONE);
540            if o != NONE {
541                commits.insert(o);
542            }
543            ords.push(o);
544        }
545        let mut row_by_rank = vec![NONE; commits.len() as usize];
546        for (row, &o) in ords.iter().enumerate() {
547            if o != NONE {
548                row_by_rank[commits.rank(o) as usize - 1] = row as u32;
549            }
550        }
551        let g = Self { ords, tree: Vec::new(), parents: Vec::new(), generation: Vec::new(), commits, row_by_rank };
552        let mut tree = Vec::with_capacity(n);
553        let mut parents = Vec::with_capacity(n);
554        let mut generation = Vec::with_capacity(n);
555        for node in nodes {
556            tree.push(node.tree.as_deref().and_then(|t| space.ordinal_of_hex(t)).unwrap_or(NONE));
557            let mut ps: Vec<u32> = node
558                .parents
559                .iter()
560                .filter_map(|p| space.ordinal_of_hex(p))
561                .filter_map(|o| g.row_of(o))
562                .collect();
563            ps.sort_unstable();
564            ps.dedup();
565            parents.push(ps);
566            generation.push(node.generation);
567        }
568        Self { tree, parents, generation, ..g }
569    }
570
571    pub fn len(&self) -> usize {
572        self.ords.len()
573    }
574
575    pub fn is_empty(&self) -> bool {
576        self.ords.is_empty()
577    }
578
579    /// The set of commit ordinals.
580    pub fn commits(&self) -> &RoaringBitmap {
581        &self.commits
582    }
583
584    #[inline]
585    pub fn is_commit(&self, ordinal: u32) -> bool {
586        self.commits.contains(ordinal)
587    }
588
589    /// ordinal → row, for a commit.
590    #[inline]
591    pub fn row_of(&self, ordinal: u32) -> Option<u32> {
592        if !self.commits.contains(ordinal) {
593            return None;
594        }
595        self.row_by_rank.get(self.commits.rank(ordinal) as usize - 1).copied()
596    }
597
598    /// Rows with no child in the graph — the tips.
599    pub fn tip_rows(&self) -> Vec<u32> {
600        let mut has_child = vec![false; self.len()];
601        for ps in &self.parents {
602            for &p in ps {
603                has_child[p as usize] = true;
604            }
605        }
606        (0..self.len() as u32).filter(|&r| !has_child[r as usize]).collect()
607    }
608}
609
610/// **The live reachability table**: entry `i` is the closure of the commit at
611/// ordinal `commits[i]`.
612#[derive(Clone, Default, Debug, PartialEq)]
613pub struct ReachTable {
614    pub commits: Vec<u32>,
615    pub bitmaps: Vec<RoaringBitmap>,
616}
617
618impl ReachTable {
619    pub fn len(&self) -> usize {
620        self.commits.len()
621    }
622
623    pub fn is_empty(&self) -> bool {
624        self.commits.is_empty()
625    }
626
627    /// row → entry index (or [`NONE`]), over `graph`. Rebuilt at every fold
628    /// beside the graph; the table itself is not.
629    pub fn by_row(&self, graph: &OrdGraph) -> Vec<u32> {
630        let mut v = vec![NONE; graph.len()];
631        for (i, &o) in self.commits.iter().enumerate() {
632            if let Some(r) = graph.row_of(o) {
633                v[r as usize] = i as u32;
634            }
635        }
636        v
637    }
638
639    /// The sealed spelling, with every ordinal mapped through `remap` (old →
640    /// sorted). `None` if a set bit maps to [`NONE`] — an entry naming an object
641    /// the target space lacks would be a silent over-claim.
642    pub fn to_entries(&self, space: &OrdinalSpace<'_>, remap: Option<&[u32]>) -> Option<Vec<ReachEntry>> {
643        let mut out = Vec::with_capacity(self.len());
644        for (i, &o) in self.commits.iter().enumerate() {
645            // The commit's name comes out of the TARGET space, through the
646            // same remap its bits go through.
647            let c = match remap {
648                None => o,
649                Some(m) => match m.get(o as usize).copied() {
650                    Some(n) if n != NONE => n,
651                    _ => return None,
652                },
653            };
654            let commit = hex::encode(space.oid(c)?);
655            let bitmap = match remap {
656                None => self.bitmaps[i].clone(),
657                Some(m) => {
658                    let mut mapped: Vec<u32> = Vec::with_capacity(self.bitmaps[i].len() as usize);
659                    for b in self.bitmaps[i].iter() {
660                        match m.get(b as usize).copied() {
661                            Some(n) if n != NONE => mapped.push(n),
662                            _ => return None,
663                        }
664                    }
665                    mapped.sort_unstable();
666                    RoaringBitmap::from_sorted_iter(mapped).ok()?
667                }
668            };
669            out.push(ReachEntry { commit, bitmap });
670        }
671        Some(out)
672    }
673
674    /// Renumber every bit through `remap` (old ordinal → new ordinal); an entry
675    /// whose commit maps to [`NONE`] is dropped, a BIT that does is a refusal
676    /// (`None`: rebuild). One array index per bit.
677    pub fn remapped(&self, remap: &[u32]) -> Option<ReachTable> {
678        let mut out = ReachTable::default();
679        for (i, &o) in self.commits.iter().enumerate() {
680            let c = *remap.get(o as usize)?;
681            if c == NONE {
682                continue;
683            }
684            let mut mapped: Vec<u32> = Vec::with_capacity(self.bitmaps[i].len() as usize);
685            for b in self.bitmaps[i].iter() {
686                match remap.get(b as usize).copied() {
687                    Some(n) if n != NONE => mapped.push(n),
688                    _ => return None,
689                }
690            }
691            mapped.sort_unstable();
692            out.commits.push(c);
693            out.bitmaps.push(RoaringBitmap::from_sorted_iter(mapped).ok()?);
694        }
695        Some(out)
696    }
697
698    /// Keep at most `max` entries: the current tips first, then an even sample
699    /// of the rest in table order.
700    pub fn pruned(&self, graph: &OrdGraph, max: usize) -> ReachTable {
701        if self.len() <= max {
702            return self.clone();
703        }
704        let mut is_tip = vec![false; graph.len()];
705        for r in graph.tip_rows() {
706            is_tip[r as usize] = true;
707        }
708        let tip_idx: Vec<usize> = (0..self.len())
709            .filter(|&i| graph.row_of(self.commits[i]).is_some_and(|r| is_tip[r as usize]))
710            .collect();
711        let mut chosen: Vec<usize> = tip_idx.iter().copied().take(max).collect();
712        if chosen.len() < max {
713            let room = max - chosen.len();
714            let rest: Vec<usize> = (0..self.len()).filter(|i| !tip_idx.contains(i)).collect();
715            if !rest.is_empty() {
716                let stride = rest.len().div_ceil(room).max(1);
717                chosen.extend(rest.iter().step_by(stride).take(room));
718            }
719        }
720        chosen.sort_unstable();
721        chosen.dedup();
722        ReachTable {
723            commits: chosen.iter().map(|&i| self.commits[i]).collect(),
724            bitmaps: chosen.iter().map(|&i| self.bitmaps[i].clone()).collect(),
725        }
726    }
727}
728
729/// **Carry a sealed-spelling table across a renumbering** — kept for the sealed
730/// builders and the tests that speak hex; the live store uses
731/// [`ReachTable::remapped`] with one remap table. `keep` decides which commits
732/// survive. `None` means the carry cannot be exact (a set bit's object is gone).
733pub fn carry_reach(
734    old: &[ReachEntry],
735    old_space: &OrdinalSpace<'_>,
736    new_space: &OrdinalSpace<'_>,
737    keep: impl Fn(&str) -> bool,
738) -> Option<Vec<ReachEntry>> {
739    if old.is_empty() || old_space.is_empty() {
740        return None;
741    }
742    let remap: Vec<u32> = (0..old_space.len() as u32)
743        .map(|o| old_space.oid(o).and_then(|oid| new_space.ordinal_of_raw(oid)).unwrap_or(NONE))
744        .collect();
745    let mut out = Vec::with_capacity(old.len());
746    for e in old {
747        if !keep(&e.commit) {
748            continue;
749        }
750        let mut mapped = Vec::with_capacity(e.bitmap.len() as usize);
751        for b in e.bitmap.iter() {
752            match remap.get(b as usize).copied() {
753                Some(n) if n != NONE => mapped.push(n),
754                _ => return None,
755            }
756        }
757        mapped.sort_unstable();
758        let bitmap = RoaringBitmap::from_sorted_iter(mapped).ok()?;
759        out.push(ReachEntry { commit: e.commit.clone(), bitmap });
760    }
761    Some(out)
762}
763
764/// What the builder needs to know about every object in the archive.
765pub struct ObjectFacts<'a> {
766    /// oid ⇄ ordinal — the bitmap space.
767    pub ordinal: OrdinalSpace<'a>,
768    /// Tree payloads, by raw oid. Blobs are not needed (they are leaves) and
769    /// are deliberately not asked for.
770    pub trees: &'a dyn TreeSource,
771    /// Raw oid width, needed to walk tree records.
772    pub oid_len: usize,
773}
774
775/// **Every object under `root` that `acc` does not already hold** — git's
776/// `fill_bitmap_tree` with `should_include_obj`.
777///
778/// A set bit means that object and, for a tree, everything under it, is already
779/// accounted for: either by a parent's closure the caller started `acc` from, or
780/// by an earlier tree in this same walk. Descending anyway would be correct and
781/// would make the whole exercise O(commits × tree). Iterative: a deep source
782/// tree is not a stack-depth bet, and a corrupt cycle terminates because the
783/// revisited oid's bit is already set.
784pub(crate) fn fill_tree(root: u32, facts: &ObjectFacts<'_>, acc: &mut RoaringBitmap) {
785    fill_tree_counted(root, facts, acc, &mut BuildCount::default());
786}
787
788/// What a build spent, for the opt-in report (`ZNIPPY_REACH_TIMING=1`):
789/// counters, not clocks, except the one clock around the tree reads.
790#[derive(Default)]
791struct BuildCount {
792    trees_read: u64,
793    tree_bytes: u64,
794    entries: u64,
795    tree_read: std::time::Duration,
796}
797
798fn fill_tree_counted(root: u32, facts: &ObjectFacts<'_>, acc: &mut RoaringBitmap, count: &mut BuildCount) {
799    let mut stack: Vec<u32> = vec![root];
800    while let Some(o) = stack.pop() {
801        if !acc.insert(o) {
802            continue;
803        }
804        let Some(oid) = facts.ordinal.oid(o) else { continue };
805        let t0 = std::time::Instant::now();
806        let payload = facts.trees.tree(oid);
807        count.tree_read += t0.elapsed();
808        count.trees_read += 1;
809        if let Some(payload) = payload {
810            count.tree_bytes += payload.len() as u64;
811            for e in tree_entries(&payload, facts.oid_len) {
812                count.entries += 1;
813                // An entry the archive does not hold (a gitlink, a thin base
814                // outside) has no ordinal and is not selected for emission.
815                if let Some(c) = facts.ordinal.ordinal_of_raw(e.oid) {
816                    stack.push(c);
817                }
818            }
819        }
820    }
821}
822
823/// Tips first, then an even sample through the rest, capped at `max_commits`.
824fn select_rows(graph: &OrdGraph, policy: ReachPolicy) -> Vec<u32> {
825    let n = graph.len();
826    let mut has_child = vec![false; n];
827    for ps in &graph.parents {
828        for &p in ps {
829            has_child[p as usize] = true;
830        }
831    }
832    let mut chosen: Vec<u32> = (0..n as u32).filter(|&i| !has_child[i as usize]).collect();
833    chosen.truncate(policy.max_commits);
834    if chosen.len() < policy.max_commits {
835        let room = policy.max_commits - chosen.len();
836        let rest: Vec<u32> = (0..n as u32).filter(|&i| has_child[i as usize]).collect();
837        if !rest.is_empty() {
838            let stride = rest.len().div_ceil(room).max(1);
839            chosen.extend(rest.iter().step_by(stride).take(room));
840        }
841    }
842    chosen.sort_unstable();
843    chosen.dedup();
844    chosen
845}
846
847/// **Build the table from scratch**, parents before children, each bitmap
848/// started from its parents' and completed by [`fill_tree`].
849///
850/// Memory is the frontier: a commit's bitmap lives only while a child still
851/// needs it (or it was selected). The last child to need a parent takes the
852/// parent's bitmap by move; every other child clones it. Rows that are not in
853/// the ordinal space (no oid) still get a bitmap so their children inherit the
854/// history, but they are never selected.
855pub fn build_reach_table(graph: &OrdGraph, facts: &ObjectFacts<'_>, policy: ReachPolicy) -> ReachTable {
856    let n = graph.len();
857    if n == 0 || policy.max_commits == 0 {
858        return ReachTable::default();
859    }
860    let selected = select_rows(graph, policy);
861    let mut keep = vec![false; n];
862    for &r in &selected {
863        keep[r as usize] = graph.ords[r as usize] != NONE;
864    }
865    let mut pending: Vec<u32> = vec![0; n];
866    for ps in &graph.parents {
867        for &p in ps {
868            pending[p as usize] += 1;
869        }
870    }
871    let mut live: Vec<Option<RoaringBitmap>> = (0..n).map(|_| None).collect();
872    let report = std::env::var_os(crate::arms::ENV_REACH_TIMING).is_some();
873    let started = std::time::Instant::now();
874    let mut count = BuildCount::default();
875    let mut live_peak = 0usize;
876    let mut live_now = 0usize;
877    for i in 0..n {
878        let ps = &graph.parents[i];
879        let mut bm: Option<RoaringBitmap> = None;
880        for &p in ps {
881            let p = p as usize;
882            pending[p] -= 1;
883            let last = pending[p] == 0 && !keep[p];
884            if last && live[p].is_some() {
885                live_now -= 1;
886            }
887            match (&mut bm, last) {
888                (None, true) => bm = live[p].take(),
889                (None, false) => bm = live[p].clone(),
890                (Some(acc), true) => {
891                    if let Some(pb) = live[p].take() {
892                        *acc |= pb;
893                    }
894                }
895                (Some(acc), false) => {
896                    if let Some(pb) = &live[p] {
897                        *acc |= pb;
898                    }
899                }
900            }
901        }
902        let mut bm = bm.unwrap_or_default();
903        if graph.ords[i] != NONE {
904            bm.insert(graph.ords[i]);
905        }
906        if graph.tree[i] != NONE {
907            fill_tree_counted(graph.tree[i], facts, &mut bm, &mut count);
908        }
909        if pending[i] > 0 || keep[i] {
910            live[i] = Some(bm);
911            live_now += 1;
912            live_peak = live_peak.max(live_now);
913        }
914    }
915    if report {
916        eprintln!(
917            "reach build: {} commits, {} objects, {} entries; {} trees read ({} MB, {} entries) in {:.2} s of {:.2} s; live bitmaps peak {live_peak}, end {live_now}",
918            n,
919            facts.ordinal.len(),
920            selected.len(),
921            count.trees_read,
922            count.tree_bytes >> 20,
923            count.entries,
924            count.tree_read.as_secs_f64(),
925            started.elapsed().as_secs_f64(),
926        );
927    }
928    let mut out = ReachTable::default();
929    for &r in &selected {
930        if !keep[r as usize] {
931            continue;
932        }
933        // `expect`, not `unwrap_or_default`: a missing bitmap here means the
934        // retention rule let a kept row be dropped, and an EMPTY bitmap would
935        // be a silent under-send — a clone served one object per branch.
936        let bitmap = live[r as usize].take().unwrap_or_else(|| {
937            panic!("row {r} was selected for a reachability bitmap and its bitmap is not live at the end of the build")
938        });
939        out.commits.push(graph.ords[r as usize]);
940        out.bitmaps.push(bitmap);
941    }
942    out
943}
944
945/// Give every `EXTEND_STRIDE`-th new commit an entry besides the new tips, so a
946/// later walk down a long pushed branch is bounded by this many commits' trees.
947const EXTEND_STRIDE: usize = 64;
948
949/// **Extend a table after a push**: entries for the new tips (and a sample of
950/// the other new commits), each computed by the bounded walk down to the
951/// entries that already exist. Nothing already in the table is touched or
952/// recomputed; the cost is the pushed commits' trees, whatever the repository's
953/// size. `new_rows` must be in parents-before-children order (the caller takes
954/// them off the generation-ordered graph).
955pub fn extend_reach_table(
956    table: &ReachTable,
957    graph: &OrdGraph,
958    facts: &ObjectFacts<'_>,
959    new_rows: &[u32],
960    policy: ReachPolicy,
961) -> ReachTable {
962    if new_rows.is_empty() || policy.max_commits == 0 {
963        return table.clone();
964    }
965    let mut out = table.clone();
966    let mut by_row = out.by_row(graph);
967    let mut has_child = vec![false; graph.len()];
968    for ps in &graph.parents {
969        for &p in ps {
970            has_child[p as usize] = true;
971        }
972    }
973    for (k, &r) in new_rows.iter().enumerate() {
974        let row = r as usize;
975        if graph.ords[row] == NONE || by_row[row] != NONE {
976            continue;
977        }
978        if has_child[row] && k % EXTEND_STRIDE != EXTEND_STRIDE - 1 {
979            continue;
980        }
981        let mut bm = RoaringBitmap::new();
982        {
983            let view = ReachView { table: &out, by_row: &by_row, graph };
984            accumulate(r, &view, facts, &mut bm);
985        }
986        by_row[row] = out.len() as u32;
987        out.commits.push(graph.ords[row]);
988        out.bitmaps.push(bm);
989    }
990    if out.len() > policy.max_commits.saturating_mul(2) {
991        out = out.pruned(graph, policy.max_commits);
992    }
993    out
994}
995
996/// The table, its row index and the graph, paired by the fold that built them
997/// so a lookup can never mix a table with another fold's rows.
998#[derive(Clone, Copy)]
999pub struct ReachView<'a> {
1000    pub table: &'a ReachTable,
1001    pub by_row: &'a [u32],
1002    pub graph: &'a OrdGraph,
1003}
1004
1005impl<'a> ReachView<'a> {
1006    /// The stored bitmap of the commit at `row`, if it has one.
1007    #[inline]
1008    pub fn bitmap_of_row(&self, row: u32) -> Option<&'a RoaringBitmap> {
1009        match self.by_row.get(row as usize).copied() {
1010            Some(i) if i != NONE => self.table.bitmaps.get(i as usize),
1011            _ => None,
1012        }
1013    }
1014}
1015
1016/// **The bounded walk: OR into `acc` everything reachable from the commit at
1017/// `row`, stopping at every commit that already has a bitmap.**
1018///
1019/// A stored bitmap is closed under reachability, so reaching one accounts for
1020/// the entire history behind it; what the walk covers is only what has been
1021/// pushed since the table last covered it, whatever the repository's size.
1022/// This is git's `add_to_include_set` rule.
1023///
1024/// The two phases are an ordering, not a style: the commit walk runs to
1025/// completion before a single tree is opened, so that every stored bitmap the
1026/// request will OR in is already in `acc` when the tree walk's skip rule runs.
1027/// Get it wrong and the first unbitmapped commit costs a walk of every tree in
1028/// the repository.
1029pub fn accumulate(row: u32, view: &ReachView<'_>, facts: &ObjectFacts<'_>, acc: &mut RoaringBitmap) {
1030    // ── phase 1: the commit walk, to completion ────────────────────────────
1031    let mut unbitmapped: Vec<u32> = Vec::new();
1032    let mut seen = RoaringBitmap::new();
1033    let mut stack: Vec<u32> = vec![row];
1034    while let Some(r) = stack.pop() {
1035        if !seen.insert(r) {
1036            continue;
1037        }
1038        // **Already covered.** `acc` only ever holds closed sets — stored
1039        // bitmaps, and the results of completed walks — so a commit whose bit
1040        // is set has its whole history in `acc` and there is nothing to walk.
1041        // This is what makes a clone's 1 006 wants (most of them tags on old,
1042        // unbitmapped commits) cost nothing once the branch tips are in: the
1043        // caller ORs the bitmapped wants first, then walks the rest.
1044        let o = view.graph.ords[r as usize];
1045        if o != NONE && acc.contains(o) {
1046            continue;
1047        }
1048        if let Some(bm) = view.bitmap_of_row(r) {
1049            *acc |= bm;
1050            continue;
1051        }
1052        unbitmapped.push(r);
1053        if let Some(ps) = view.graph.parents.get(r as usize) {
1054            stack.extend(ps.iter().copied());
1055        }
1056    }
1057    // ── phase 2: the trees, with the skip rule ─────────────────────────────
1058    for r in unbitmapped {
1059        let r = r as usize;
1060        if view.graph.ords[r] != NONE {
1061            acc.insert(view.graph.ords[r]);
1062        }
1063        if view.graph.tree[r] != NONE {
1064            fill_tree(view.graph.tree[r], facts, acc);
1065        }
1066    }
1067}
1068
1069/// Which objects a repack must hold to make the reachability pass exact.
1070///
1071/// Only commits and trees carry pointers; blobs are leaves. The builder
1072/// therefore needs tree payloads and nothing else, which is why
1073/// [`ObjectFacts::trees`] exists and there is no `blobs` field.
1074pub fn needs_payload(kind: GitObjectKind) -> bool {
1075    matches!(kind, GitObjectKind::Commit | GitObjectKind::Tree)
1076}
1077
1078/// **The sealed builder**: hex nodes in, hex entries out, over a sorted space.
1079/// Decodes each node once into an [`OrdGraph`] and runs [`build_reach_table`].
1080pub fn build_reach(commits: &[CommitNode], facts: &ObjectFacts<'_>, policy: ReachPolicy) -> Vec<ReachEntry> {
1081    let graph = OrdGraph::from_nodes(commits, &facts.ordinal);
1082    let table = build_reach_table(&graph, facts, policy);
1083    table.to_entries(&facts.ordinal, None).unwrap_or_default()
1084}
1085
1086pub fn build_reach_batch(entries: &[ReachEntry]) -> Result<RecordBatch> {
1087    use znippy_common::arrow::array::UInt64Builder;
1088    let n = entries.len();
1089    let mut oid_b = StringBuilder::with_capacity(n, n * 64);
1090    let mut card_b = UInt64Builder::with_capacity(n);
1091    let mut bm_b = BinaryBuilder::with_capacity(n, n * 128);
1092    for e in entries {
1093        oid_b.append_value(&e.commit);
1094        card_b.append_value(e.bitmap.len());
1095        let mut buf = Vec::new();
1096        e.bitmap
1097            .serialize_into(&mut buf)
1098            .map_err(|err| anyhow!("reach bitmap serialize: {err}"))?;
1099        bm_b.append_value(&buf);
1100    }
1101    RecordBatch::try_new(
1102        reach_schema(),
1103        vec![Arc::new(oid_b.finish()), Arc::new(card_b.finish()), Arc::new(bm_b.finish())],
1104    )
1105    .map_err(|e| anyhow!("reach batch: {e}"))
1106}
1107
1108pub fn decode_reach(bytes: &[u8]) -> Result<Vec<ReachEntry>> {
1109    // Through the IPC guard: the section comes out of an archive.
1110    let reader = znippy_common::ipc_guard::stream_reader(bytes)
1111        .map_err(|e| anyhow!("reach reader: {e}"))?;
1112    let mut out = Vec::new();
1113    for batch in reader {
1114        let batch = batch.map_err(|e| anyhow!("reach batch read: {e}"))?;
1115        let oids = batch
1116            .column_by_name("commit_oid")
1117            .and_then(|c| c.as_any().downcast_ref::<StringArray>())
1118            .ok_or_else(|| anyhow!("reach: no `commit_oid` column"))?;
1119        let bms = batch
1120            .column_by_name("bitmap")
1121            .and_then(|c| c.as_any().downcast_ref::<BinaryArray>())
1122            .ok_or_else(|| anyhow!("reach: no `bitmap` column"))?;
1123        for i in 0..batch.num_rows() {
1124            let bitmap = RoaringBitmap::deserialize_from(bms.value(i))
1125                .map_err(|e| anyhow!("reach bitmap deserialize: {e}"))?;
1126            out.push(ReachEntry { commit: oids.value(i).to_string(), bitmap });
1127        }
1128    }
1129    Ok(out)
1130}
1131
1132/// Read the reachability bitmaps out of a sealed archive. `Ok(None)` when absent.
1133pub fn read_reach(archive: &Path) -> Result<Option<Vec<ReachEntry>>> {
1134    match read_reserved_section_bytes(archive, GUNNAR_REACH_MODULE)? {
1135        Some(b) => Ok(Some(decode_reach(&b)?)),
1136        None => Ok(None),
1137    }
1138}
1139
1140// ── the live table's persisted form ─────────────────────────────────────────
1141
1142fn live_schema() -> Arc<Schema> {
1143    Arc::new(Schema::new(vec![
1144        Field::new("commit_ordinal", DataType::UInt32, false),
1145        Field::new("bitmap", DataType::Binary, false),
1146    ]))
1147}
1148
1149/// The live table as Arrow IPC stream bytes: `(commit_ordinal, bitmap)`.
1150pub fn encode_live_table(table: &ReachTable) -> Result<Vec<u8>> {
1151    use znippy_common::arrow::array::UInt32Builder;
1152    let n = table.len();
1153    let mut ord_b = UInt32Builder::with_capacity(n);
1154    let mut bm_b = BinaryBuilder::with_capacity(n, n * 128);
1155    for (i, &o) in table.commits.iter().enumerate() {
1156        ord_b.append_value(o);
1157        let mut buf = Vec::new();
1158        table.bitmaps[i]
1159            .serialize_into(&mut buf)
1160            .map_err(|err| anyhow!("reach bitmap serialize: {err}"))?;
1161        bm_b.append_value(&buf);
1162    }
1163    let batch = RecordBatch::try_new(live_schema(), vec![Arc::new(ord_b.finish()), Arc::new(bm_b.finish())])
1164        .map_err(|e| anyhow!("live reach batch: {e}"))?;
1165    let mut buf = Vec::new();
1166    {
1167        let mut w = znippy_common::arrow::ipc::writer::StreamWriter::try_new(&mut buf, &live_schema())
1168            .map_err(|e| anyhow!("live reach writer: {e}"))?;
1169        w.write(&batch).map_err(|e| anyhow!("live reach write: {e}"))?;
1170        w.finish().map_err(|e| anyhow!("live reach finish: {e}"))?;
1171    }
1172    Ok(buf)
1173}
1174
1175/// The inverse of [`encode_live_table`], through the IPC guard: hostile bytes
1176/// are an `Err`, never a panic or an allocation abort. `space_len` bounds the
1177/// ordinals: an entry naming one outside the space is refused.
1178pub fn decode_live_table(bytes: &[u8], space_len: u32) -> Result<ReachTable> {
1179    use znippy_common::arrow::array::UInt32Array;
1180    let reader = znippy_common::ipc_guard::stream_reader(bytes).map_err(|e| anyhow!("live reach reader: {e}"))?;
1181    let mut out = ReachTable::default();
1182    for batch in reader {
1183        let batch = batch.map_err(|e| anyhow!("live reach batch read: {e}"))?;
1184        let ords = batch
1185            .column_by_name("commit_ordinal")
1186            .and_then(|c| c.as_any().downcast_ref::<UInt32Array>())
1187            .ok_or_else(|| anyhow!("live reach: no `commit_ordinal` column"))?;
1188        let bms = batch
1189            .column_by_name("bitmap")
1190            .and_then(|c| c.as_any().downcast_ref::<BinaryArray>())
1191            .ok_or_else(|| anyhow!("live reach: no `bitmap` column"))?;
1192        for i in 0..batch.num_rows() {
1193            let o = ords.value(i);
1194            if o >= space_len {
1195                return Err(anyhow!("live reach: commit ordinal {o} is outside the space ({space_len})"));
1196            }
1197            let bitmap = RoaringBitmap::deserialize_from(bms.value(i))
1198                .map_err(|e| anyhow!("live reach bitmap deserialize: {e}"))?;
1199            if bitmap.max().is_some_and(|m| m >= space_len) {
1200                return Err(anyhow!("live reach: a bitmap names an ordinal outside the space ({space_len})"));
1201            }
1202            out.commits.push(o);
1203            out.bitmaps.push(bitmap);
1204        }
1205    }
1206    Ok(out)
1207}
1208
1209#[cfg(test)]
1210mod carry_tests {
1211    use super::*;
1212
1213    fn space(oids: &[u8]) -> Vec<u8> {
1214        // 1-byte "oids" are enough for the arithmetic; the space is sorted.
1215        let mut v = oids.to_vec();
1216        v.sort_unstable();
1217        v
1218    }
1219
1220    /// The carry renumbers every bit through the remap, drops an entry whose
1221    /// commit is gone, and refuses when a set bit's object is gone.
1222    #[test]
1223    fn a_carry_renumbers_bits_drops_dead_commits_and_refuses_a_missing_object() {
1224        let old_raw = space(&[10, 20, 30, 40]);
1225        let old = OrdinalSpace::new(&old_raw, 1);
1226        // Two oids inserted below 30 and one above 40: ordinals shift by 2 and 2.
1227        let new_raw = space(&[10, 15, 20, 25, 30, 40, 50]);
1228        let new = OrdinalSpace::new(&new_raw, 1);
1229        let entries = vec![
1230            ReachEntry { commit: "keep".into(), bitmap: [0u32, 2, 3].into_iter().collect() },
1231            ReachEntry { commit: "gone".into(), bitmap: [1u32].into_iter().collect() },
1232        ];
1233        let carried = carry_reach(&entries, &old, &new, |c| c == "keep").expect("exact carry");
1234        assert_eq!(carried.len(), 1, "the dead commit's entry is dropped, not carried");
1235        assert_eq!(carried[0].commit, "keep");
1236        let bits: Vec<u32> = carried[0].bitmap.iter().collect();
1237        assert_eq!(bits, vec![0, 4, 5], "10→0, 30→4, 40→5 in the new numbering");
1238
1239        // An object removed from the space under a kept entry: refuse the carry.
1240        let shrunk_raw = space(&[10, 20, 40]);
1241        let shrunk = OrdinalSpace::new(&shrunk_raw, 1);
1242        assert!(carry_reach(&entries, &old, &shrunk, |_| true).is_none(), "30 is gone; a bitmap naming it must not survive");
1243        // Nothing to carry is not a carry.
1244        assert!(carry_reach(&[], &old, &new, |_| true).is_none());
1245    }
1246}
1247
1248#[cfg(test)]
1249mod table_tests {
1250    use super::*;
1251
1252    fn oid(i: u32) -> [u8; 20] {
1253        let mut o = [0u8; 20];
1254        // Spread the two fanout bytes so the prefix table does real work.
1255        o[0] = (i.wrapping_mul(2654435761) >> 24) as u8;
1256        o[1] = (i.wrapping_mul(40503) >> 8) as u8;
1257        o[16..20].copy_from_slice(&i.to_be_bytes());
1258        o
1259    }
1260
1261    fn sorted_concat(ids: &[u32]) -> Vec<u8> {
1262        let mut v: Vec<[u8; 20]> = ids.iter().map(|&i| oid(i)).collect();
1263        v.sort_unstable();
1264        v.dedup();
1265        v.concat()
1266    }
1267
1268    /// **Stable ordinals**: a follow appends behind the base and moves nothing;
1269    /// every oid is found both ways through the fanout and the appended index;
1270    /// a removed oid refuses the follow; the persisted form replays exactly.
1271    /// Seen RED with `fanout[p+1]` off by one (misses in the last bucket) and
1272    /// with `extra_sorted` left stale after a follow (appended oids absent).
1273    #[test]
1274    fn an_ordinal_table_appends_and_never_renumbers() {
1275        let first: Vec<u32> = (0..3000).collect();
1276        let mut t = OrdinalTable::from_sorted(sorted_concat(&first), 20);
1277        assert_eq!(t.len(), 3000);
1278        let v = t.view();
1279        for o in 0..3000u32 {
1280            let raw = v.oid(o).unwrap();
1281            assert_eq!(v.ordinal_of_raw(raw), Some(o), "base inverse at {o}");
1282        }
1283        let before: Vec<[u8; 20]> = (0..3000u32).map(|o| v.oid(o).unwrap().try_into().unwrap()).collect();
1284
1285        // Push 1: 7 new oids.
1286        let second: Vec<u32> = (0..3007).collect();
1287        let r = t.follow(&sorted_concat(&second)).expect("a superset follows");
1288        assert_eq!(r, 3000..3007);
1289        assert_eq!(t.extra_len(), 7);
1290        // Nothing moved.
1291        let v = t.view();
1292        for (o, raw) in before.iter().enumerate() {
1293            assert_eq!(v.oid(o as u32), Some(raw.as_slice()), "ordinal {o} moved");
1294            assert_eq!(v.ordinal_of_raw(raw), Some(o as u32));
1295        }
1296        for o in 3000..3007u32 {
1297            let raw = v.oid(o).unwrap();
1298            assert_eq!(v.ordinal_of_raw(raw), Some(o), "appended inverse at {o}");
1299        }
1300        // Push 2: no change is an empty range; then 5 more.
1301        assert_eq!(t.follow(&sorted_concat(&second)), Some(3007..3007));
1302        let third: Vec<u32> = (0..3012).collect();
1303        assert_eq!(t.follow(&sorted_concat(&third)), Some(3007..3012));
1304        let v = t.view();
1305        assert_eq!(v.len(), 3012);
1306        for o in 0..3012u32 {
1307            assert_eq!(v.ordinal_of_raw(v.oid(o).unwrap()), Some(o));
1308        }
1309        assert_eq!(v.ordinal_of_raw(&oid(999_999)), None, "an absent oid is None, not a neighbour");
1310        assert_eq!(v.ordinal_of_raw(&[0u8; 19]), None);
1311        assert_eq!(v.oid(3012), None);
1312
1313        // The persisted form: (full sorted now, extra in arrival order).
1314        let replay = OrdinalTable::from_parts(&sorted_concat(&third), t.extra_raw(), 20).expect("replays");
1315        let (a, b) = (t.view(), replay.view());
1316        for o in 0..3012u32 {
1317            assert_eq!(a.oid(o), b.oid(o), "replayed ordinal {o} differs");
1318        }
1319        // An extra the store no longer holds does not replay.
1320        let shrunk: Vec<u32> = (0..3011).collect();
1321        assert!(OrdinalTable::from_parts(&sorted_concat(&shrunk), t.extra_raw(), 20).is_none());
1322
1323        // A removal under the table refuses the follow (a gc, a retain).
1324        let mut gone: Vec<u32> = (0..3012).collect();
1325        gone.remove(1500);
1326        assert!(t.follow(&sorted_concat(&gone)).is_none(), "an oid the table numbers is gone");
1327        let mut gone_extra: Vec<u32> = (0..3012).collect();
1328        gone_extra.remove(3005);
1329        assert!(t.follow(&sorted_concat(&gone_extra)).is_none(), "an appended oid is gone");
1330
1331        // The remap to a sorted space over the same oids is a permutation.
1332        let sorted = sorted_concat(&third);
1333        let target = OrdinalSpace::new(&sorted, 20);
1334        let remap = t.remap_to(&target);
1335        let mut seen: Vec<u32> = remap.clone();
1336        seen.sort_unstable();
1337        assert_eq!(seen, (0..3012u32).collect::<Vec<_>>());
1338        for (o, &n) in remap.iter().enumerate() {
1339            assert_eq!(t.view().oid(o as u32), target.oid(n));
1340        }
1341    }
1342
1343    /// The live table round-trips through its IPC form, and hostile bytes are
1344    /// an `Err` (the ipc guard), including an ordinal outside the space.
1345    #[test]
1346    fn the_live_table_roundtrips_and_refuses_hostile_bytes() {
1347        let t = ReachTable {
1348            commits: vec![5, 9],
1349            bitmaps: vec![[1u32, 5].into_iter().collect(), [0u32, 1, 5, 9].into_iter().collect()],
1350        };
1351        let bytes = encode_live_table(&t).unwrap();
1352        assert_eq!(decode_live_table(&bytes, 10).unwrap(), t);
1353        assert!(decode_live_table(&bytes, 9).is_err(), "ordinal 9 is outside a space of 9");
1354        assert!(decode_live_table(b"not arrow at all, not even close to a stream", 10).is_err());
1355        let mut torn = bytes.clone();
1356        torn.truncate(bytes.len() / 2);
1357        assert!(decode_live_table(&torn, 10).is_err());
1358    }
1359}
1360
1361#[cfg(test)]
1362mod tests {
1363    use super::*;
1364    use crate::graph::assign_generations;
1365
1366    fn hexid(c: char) -> String {
1367        std::iter::repeat_n(c, 40).collect()
1368    }
1369
1370    /// A flat, sorted, fixed-width ordinal buffer from oids **already in
1371    /// ordinal order** — the shape [`OrdinalSpace`] borrows.
1372    fn ordinal_raw(oids_in_order: &[&str]) -> Vec<u8> {
1373        let mut v = Vec::new();
1374        for o in oids_in_order {
1375            v.extend_from_slice(&hex::decode(o).unwrap());
1376        }
1377        v
1378    }
1379
1380    /// Returns (commits, facts-owned-data).
1381    #[allow(clippy::type_complexity)]
1382    fn tiny_repo() -> (Vec<CommitNode>, Vec<u8>, HashMap<String, Vec<u8>>) {
1383        let blob1 = hexid('1');
1384        let blob2 = hexid('2');
1385        let tree1 = hexid('3');
1386        let tree2 = hexid('4');
1387        let c1 = hexid('5');
1388        let c2 = hexid('6');
1389
1390        let mut trees: HashMap<String, Vec<u8>> = HashMap::new();
1391        let mut t1 = Vec::new();
1392        t1.extend_from_slice(b"100644 a\0");
1393        t1.extend_from_slice(&hex::decode(&blob1).unwrap());
1394        trees.insert(tree1.clone(), t1);
1395
1396        let mut t2 = Vec::new();
1397        t2.extend_from_slice(b"100644 a\0");
1398        t2.extend_from_slice(&hex::decode(&blob1).unwrap());
1399        t2.extend_from_slice(b"100644 b\0");
1400        t2.extend_from_slice(&hex::decode(&blob2).unwrap());
1401        trees.insert(tree2.clone(), t2);
1402
1403        // Ordinal = oid-lexicographic rank: hexid('1') < … < hexid('6'), so the
1404        // sorted raw buffer is simply these six in this order.
1405        let ordinal = ordinal_raw(&[&blob1, &blob2, &tree1, &tree2, &c1, &c2]);
1406
1407        let commits = assign_generations(vec![
1408            CommitNode {
1409                oid: c1.clone(),
1410                parents: vec![],
1411                tree: Some(hexid('3')),
1412                committer_time: Some(1),
1413                generation: 0,
1414            },
1415            CommitNode {
1416                oid: c2.clone(),
1417                parents: vec![c1.clone()],
1418                tree: Some(hexid('4')),
1419                committer_time: Some(2),
1420                generation: 0,
1421            },
1422        ]);
1423        (commits, ordinal, trees)
1424    }
1425
1426    #[test]
1427    fn bitmap_contains_exactly_the_reachable_objects() {
1428        let (commits, ordinal, trees) = tiny_repo();
1429        let facts = ObjectFacts { ordinal: OrdinalSpace::new(&ordinal, 20), trees: &trees, oid_len: 20 };
1430        let entries = build_reach(&commits, &facts, ReachPolicy { max_commits: 16 });
1431
1432        let by_commit: HashMap<&str, &RoaringBitmap> =
1433            entries.iter().map(|e| (e.commit.as_str(), &e.bitmap)).collect();
1434
1435        // c1 reaches: blob1(0), tree1(2), c1(4).
1436        let b1: Vec<u32> = by_commit[hexid('5').as_str()].iter().collect();
1437        assert_eq!(b1, vec![0, 2, 4], "c1 must not reach blob2/tree2/c2");
1438
1439        // c2 reaches everything.
1440        let b2: Vec<u32> = by_commit[hexid('6').as_str()].iter().collect();
1441        assert_eq!(b2, vec![0, 1, 2, 3, 4, 5]);
1442    }
1443
1444    #[test]
1445    fn want_minus_have_is_an_andnot() {
1446        let (commits, ordinal, trees) = tiny_repo();
1447        let facts = ObjectFacts { ordinal: OrdinalSpace::new(&ordinal, 20), trees: &trees, oid_len: 20 };
1448        let entries = build_reach(&commits, &facts, ReachPolicy { max_commits: 16 });
1449        let by: HashMap<&str, &RoaringBitmap> =
1450            entries.iter().map(|e| (e.commit.as_str(), &e.bitmap)).collect();
1451
1452        let want = by[hexid('6').as_str()].clone();
1453        let have = by[hexid('5').as_str()].clone();
1454        let delta: Vec<u32> = (want - have).iter().collect();
1455        // Exactly the objects the second commit introduced: blob2(1), tree2(3), c2(5).
1456        assert_eq!(delta, vec![1, 3, 5]);
1457    }
1458
1459    #[test]
1460    fn selection_takes_tips_first_and_respects_the_cap() {
1461        let (commits, ordinal, trees) = tiny_repo();
1462        let facts = ObjectFacts { ordinal: OrdinalSpace::new(&ordinal, 20), trees: &trees, oid_len: 20 };
1463        let entries = build_reach(&commits, &facts, ReachPolicy { max_commits: 1 });
1464        assert_eq!(entries.len(), 1);
1465        assert_eq!(entries[0].commit, hexid('6'), "the tip, not the root");
1466    }
1467
1468    #[test]
1469    fn batch_roundtrips_through_arrow_ipc() {
1470        let (commits, ordinal, trees) = tiny_repo();
1471        let facts = ObjectFacts { ordinal: OrdinalSpace::new(&ordinal, 20), trees: &trees, oid_len: 20 };
1472        let entries = build_reach(&commits, &facts, ReachPolicy::default());
1473        let batch = build_reach_batch(&entries).unwrap();
1474        let mut buf = Vec::new();
1475        {
1476            let mut w = znippy_common::arrow::ipc::writer::StreamWriter::try_new(
1477                &mut buf,
1478                &reach_schema(),
1479            )
1480            .unwrap();
1481            w.write(&batch).unwrap();
1482            w.finish().unwrap();
1483        }
1484        let back = decode_reach(&buf).unwrap();
1485        assert_eq!(back, entries);
1486    }
1487
1488    #[test]
1489    fn a_corrupt_tree_cycle_terminates() {
1490        // t -> t (self-referential): must not hang or blow the stack.
1491        let t = hexid('a');
1492        let mut payload = Vec::new();
1493        payload.extend_from_slice(b"40000 self\0");
1494        payload.extend_from_slice(&hex::decode(&t).unwrap());
1495        let trees: HashMap<String, Vec<u8>> = [(t.clone(), payload)].into_iter().collect();
1496        let ordinal = ordinal_raw(&[&t, &hexid('b')]);
1497        let facts = ObjectFacts { ordinal: OrdinalSpace::new(&ordinal, 20), trees: &trees, oid_len: 20 };
1498        let commits = assign_generations(vec![CommitNode {
1499            oid: hexid('b'),
1500            parents: vec![],
1501            tree: Some(t),
1502            committer_time: None,
1503            generation: 0,
1504        }]);
1505        let entries = build_reach(&commits, &facts, ReachPolicy::default());
1506        assert_eq!(entries.len(), 1);
1507        let bits: Vec<u32> = entries[0].bitmap.iter().collect();
1508        assert_eq!(bits, vec![0, 1]);
1509    }
1510
1511    /// **The binary search is the exact inverse of the slice, and it is the same
1512    /// answer a `HashMap<String, u32>` over the sorted list would give.**
1513    #[test]
1514    fn ordinal_space_is_the_inverse_of_the_slice() {
1515        // 32 distinct sha1-width oids, deliberately built OUT of order so the
1516        // sort is doing real work, then sorted into the flat space.
1517        let mut oids: Vec<[u8; 20]> = (0..32u8)
1518            .map(|i| {
1519                let mut o = [0u8; 20];
1520                o[0] = i.wrapping_mul(37);
1521                o[19] = i;
1522                o
1523            })
1524            .collect();
1525        oids.sort_unstable();
1526        oids.dedup();
1527        let mut raw = Vec::new();
1528        for o in &oids {
1529            raw.extend_from_slice(o);
1530        }
1531        let space = OrdinalSpace::new(&raw, 20);
1532        assert_eq!(space.len(), oids.len());
1533        for (o, oid) in oids.iter().enumerate() {
1534            assert_eq!(space.oid(o as u32), Some(oid.as_slice()));
1535            assert_eq!(space.ordinal_of_raw(oid), Some(o as u32));
1536            assert_eq!(space.ordinal_of_hex(&hex::encode(oid)), Some(o as u32));
1537        }
1538        // Absent, wrong width, and out of range are all None — not a neighbour.
1539        assert_eq!(space.ordinal_of_raw(&[0xffu8; 20]), None);
1540        assert_eq!(space.ordinal_of_raw(&[0u8; 19]), None);
1541        assert_eq!(space.oid(oids.len() as u32), None);
1542        assert_eq!(space.ordinal_of_hex("not-hex"), None);
1543    }
1544
1545    /// **The view owns nothing.** `OrdinalSpace` is a `Copy` view of borrowed
1546    /// slices, so building one at open allocates zero bytes — the reason the
1547    /// hex `Vec<String>` and the `HashMap<String, u32>` were removed. It grew
1548    /// from one slice to four (the fanout and the appended part came with the
1549    /// stable live space); red if an owned field creeps in or `Copy` is lost.
1550    #[test]
1551    fn ordinal_space_is_a_borrowed_view() {
1552        assert_eq!(
1553            std::mem::size_of::<OrdinalSpace<'_>>(),
1554            4 * std::mem::size_of::<&[u8]>() + std::mem::size_of::<usize>(),
1555            "OrdinalSpace grew an owned field — it must stay a borrow"
1556        );
1557        fn assert_copy<T: Copy>() {}
1558        assert_copy::<OrdinalSpace<'_>>();
1559    }
1560
1561    // ── property tests: bitmap = graph walk, on random DAGs ─────────────────
1562
1563    /// A deterministic xorshift, so a failing seed is reproducible.
1564    struct Rng(u64);
1565    impl Rng {
1566        fn next(&mut self) -> u64 {
1567            let mut x = self.0;
1568            x ^= x << 13;
1569            x ^= x >> 7;
1570            x ^= x << 17;
1571            self.0 = x;
1572            x
1573        }
1574        fn below(&mut self, n: usize) -> usize {
1575            (self.next() % n.max(1) as u64) as usize
1576        }
1577    }
1578
1579    /// A synthetic repository with a real object graph: nested trees, shared
1580    /// blobs, merges and unrelated roots.
1581    struct Dag {
1582        nodes: Vec<CommitNode>,
1583        /// Every object: oid → (kind, payload). Trees and commits carry
1584        /// pointers; blobs are leaves.
1585        objects: HashMap<Vec<u8>, (GitObjectKind, Vec<u8>)>,
1586        sorted: Vec<u8>,
1587        trees: HashMap<Vec<u8>, Vec<u8>>,
1588    }
1589
1590    fn fake_oid(rng: &mut Rng) -> Vec<u8> {
1591        let mut o = vec![0u8; 20];
1592        for b in o.iter_mut() {
1593            *b = rng.next() as u8;
1594        }
1595        o
1596    }
1597
1598    fn random_dag(seed: u64, commits: usize) -> Dag {
1599        let mut rng = Rng(seed | 1);
1600        let mut objects: HashMap<Vec<u8>, (GitObjectKind, Vec<u8>)> = HashMap::new();
1601        let mut blobs: Vec<Vec<u8>> = Vec::new();
1602        let mut subtrees: Vec<Vec<u8>> = Vec::new();
1603        let mut trees: HashMap<Vec<u8>, Vec<u8>> = HashMap::new();
1604        let new_tree = |rng: &mut Rng,
1605                            objects: &mut HashMap<Vec<u8>, (GitObjectKind, Vec<u8>)>,
1606                            blobs: &mut Vec<Vec<u8>>,
1607                            subtrees: &[Vec<u8>],
1608                            trees: &mut HashMap<Vec<u8>, Vec<u8>>|
1609         -> Vec<u8> {
1610            let mut payload = Vec::new();
1611            let entries = 1 + rng.below(4);
1612            for e in 0..entries {
1613                // Reuse a blob 50 % of the time, else make one.
1614                let blob = if !blobs.is_empty() && rng.below(2) == 0 {
1615                    blobs[rng.below(blobs.len())].clone()
1616                } else {
1617                    let b = fake_oid(rng);
1618                    objects.insert(b.clone(), (GitObjectKind::Blob, vec![e as u8]));
1619                    blobs.push(b.clone());
1620                    b
1621                };
1622                payload.extend_from_slice(format!("100644 f{e}\0").as_bytes());
1623                payload.extend_from_slice(&blob);
1624            }
1625            if !subtrees.is_empty() && rng.below(3) == 0 {
1626                let st = &subtrees[rng.below(subtrees.len())];
1627                payload.extend_from_slice(b"40000 d\0");
1628                payload.extend_from_slice(st);
1629            }
1630            if rng.below(7) == 0 {
1631                // A gitlink: a commit in another repository, not in this space.
1632                payload.extend_from_slice(b"160000 sub\0");
1633                payload.extend_from_slice(&fake_oid(rng));
1634            }
1635            let t = fake_oid(rng);
1636            objects.insert(t.clone(), (GitObjectKind::Tree, payload.clone()));
1637            trees.insert(t.clone(), payload);
1638            t
1639        };
1640        let mut nodes: Vec<CommitNode> = Vec::new();
1641        for i in 0..commits {
1642            let tree = new_tree(&mut rng, &mut objects, &mut blobs, &subtrees, &mut trees);
1643            if rng.below(2) == 0 {
1644                subtrees.push(tree.clone());
1645            }
1646            let mut parents = Vec::new();
1647            if i > 0 && rng.below(5) != 0 {
1648                parents.push(nodes[rng.below(i)].oid.clone());
1649                if i > 1 && rng.below(4) == 0 {
1650                    parents.push(nodes[rng.below(i)].oid.clone());
1651                }
1652            }
1653            let oid = fake_oid(&mut rng);
1654            let mut payload = format!("tree {}\n", hex::encode(&tree)).into_bytes();
1655            for p in &parents {
1656                payload.extend_from_slice(format!("parent {p}\n").as_bytes());
1657            }
1658            objects.insert(oid.clone(), (GitObjectKind::Commit, payload));
1659            nodes.push(CommitNode {
1660                oid: hex::encode(&oid),
1661                parents,
1662                tree: Some(hex::encode(&tree)),
1663                committer_time: Some(i as i64),
1664                generation: 0,
1665            });
1666        }
1667        let mut all: Vec<&Vec<u8>> = objects.keys().collect();
1668        all.sort_unstable();
1669        let sorted: Vec<u8> = all.iter().flat_map(|o| o.iter().copied()).collect();
1670        Dag { nodes: assign_generations(nodes), objects, sorted, trees }
1671    }
1672
1673    /// The oracle: a plain object walk from `tip`, over the object map.
1674    fn walk_closure(dag: &Dag, tip: &[u8]) -> std::collections::BTreeSet<Vec<u8>> {
1675        let mut seen = std::collections::BTreeSet::new();
1676        let mut stack = vec![tip.to_vec()];
1677        while let Some(o) = stack.pop() {
1678            let Some((kind, payload)) = dag.objects.get(&o) else { continue };
1679            if !seen.insert(o.clone()) {
1680                continue;
1681            }
1682            match kind {
1683                GitObjectKind::Commit => {
1684                    for line in payload.split(|b| *b == b'\n') {
1685                        if let Some(h) = line.strip_prefix(b"tree ").or_else(|| line.strip_prefix(b"parent ")) {
1686                            stack.push(hex::decode(h).unwrap());
1687                        }
1688                    }
1689                }
1690                GitObjectKind::Tree => {
1691                    for e in tree_entries(payload, 20) {
1692                        stack.push(e.oid.to_vec());
1693                    }
1694                }
1695                _ => {}
1696            }
1697        }
1698        seen
1699    }
1700
1701    fn bits_as_oids(bm: &RoaringBitmap, space: &OrdinalSpace<'_>) -> std::collections::BTreeSet<Vec<u8>> {
1702        bm.iter().map(|o| space.oid(o).unwrap().to_vec()).collect()
1703    }
1704
1705    /// **Every stored bitmap equals the graph walk, at every cap, on random
1706    /// DAGs** — the full builder, and the walk-covered answer for every commit
1707    /// with or without an entry.
1708    #[test]
1709    fn the_full_table_equals_a_graph_walk_on_random_dags() {
1710        for seed in 1..=24u64 {
1711            let n = 1 + (seed as usize * 7) % 60;
1712            let dag = random_dag(seed, n);
1713            let space = OrdinalSpace::new(&dag.sorted, 20);
1714            let facts = ObjectFacts { ordinal: space, trees: &dag.trees, oid_len: 20 };
1715            let graph = OrdGraph::from_nodes(&dag.nodes, &space);
1716            for cap in [usize::MAX, 1, 3, 7] {
1717                let table = build_reach_table(&graph, &facts, ReachPolicy { max_commits: cap });
1718                let by_row = table.by_row(&graph);
1719                let view = ReachView { table: &table, by_row: &by_row, graph: &graph };
1720                for (i, &o) in table.commits.iter().enumerate() {
1721                    let oracle = walk_closure(&dag, space.oid(o).unwrap());
1722                    assert_eq!(bits_as_oids(&table.bitmaps[i], &space), oracle, "seed {seed} cap {cap}: stored entry {i}");
1723                }
1724                // Every commit, bitmapped or not, through the bounded walk.
1725                for row in 0..graph.len() as u32 {
1726                    let mut acc = RoaringBitmap::new();
1727                    accumulate(row, &view, &facts, &mut acc);
1728                    let oracle = walk_closure(&dag, space.oid(graph.ords[row as usize]).unwrap());
1729                    assert_eq!(bits_as_oids(&acc, &space), oracle, "seed {seed} cap {cap}: walk from row {row}");
1730                }
1731            }
1732        }
1733    }
1734
1735    /// **A table extended after a "push" equals a table built from scratch**,
1736    /// on random DAGs split into an old and a new part, over a stable
1737    /// (appended) ordinal space: entries for the new tips are exact, old
1738    /// entries are untouched, and the walk from every commit still equals the
1739    /// oracle.
1740    #[test]
1741    fn an_extended_table_equals_a_graph_walk_on_random_dags() {
1742        for seed in 1..=24u64 {
1743            let n = 4 + (seed as usize * 11) % 70;
1744            let dag = random_dag(seed * 7919, n);
1745            // The old part: the first k commits (in generation order they are
1746            // closed under parents only if we take a prefix of the ORIGINAL
1747            // creation order, so take the objects reachable from the first k
1748            // tips of creation order instead).
1749            let k = 1 + (seed as usize) % (n - 1);
1750            let old_nodes: Vec<CommitNode> = dag.nodes.iter().filter(|c| c.committer_time.unwrap() < k as i64).cloned().collect();
1751            let mut old_objs: std::collections::BTreeSet<Vec<u8>> = std::collections::BTreeSet::new();
1752            for c in &old_nodes {
1753                old_objs.extend(walk_closure(&dag, &hex::decode(&c.oid).unwrap()));
1754            }
1755            let old_sorted: Vec<u8> = old_objs.iter().flat_map(|o| o.iter().copied()).collect();
1756            let mut table_space = OrdinalTable::from_sorted(old_sorted, 20);
1757            let old_graph = OrdGraph::from_nodes(&old_nodes, &table_space.view());
1758            let facts_old = ObjectFacts { ordinal: table_space.view(), trees: &dag.trees, oid_len: 20 };
1759            let policy = ReachPolicy { max_commits: 5 };
1760            let old_table = build_reach_table(&old_graph, &facts_old, policy);
1761
1762            // The push: the rest arrives; the space appends.
1763            let appended = table_space.follow(&dag.sorted).expect("a superset follows");
1764            let space = table_space.view();
1765            let graph = OrdGraph::from_nodes(&dag.nodes, &space);
1766            let facts = ObjectFacts { ordinal: space, trees: &dag.trees, oid_len: 20 };
1767            let new_rows: Vec<u32> = (0..graph.len() as u32).filter(|&r| graph.ords[r as usize] >= appended.start).collect();
1768            let table = extend_reach_table(&old_table, &graph, &facts, &new_rows, policy);
1769
1770            // Old entries untouched (an entry may be PRUNED once the table
1771            // outgrows twice the cap — never rewritten), new tips covered.
1772            assert!(table.len() <= 2 * policy.max_commits, "seed {seed}: the table outgrew twice the cap");
1773            for (i, &o) in old_table.commits.iter().enumerate() {
1774                if let Some(j) = table.commits.iter().position(|&c| c == o) {
1775                    assert_eq!(table.bitmaps[j], old_table.bitmaps[i], "seed {seed}: old entry rewritten");
1776                }
1777            }
1778            let by_row = table.by_row(&graph);
1779            // Every new tip has an entry, unless the cap forced a prune (then
1780            // at least `max` entries stand and the walk covers the rest).
1781            for r in graph.tip_rows() {
1782                if new_rows.contains(&r) && table.len() < policy.max_commits {
1783                    assert_ne!(by_row[r as usize], NONE, "seed {seed}: new tip row {r} has no entry");
1784                }
1785            }
1786            let view = ReachView { table: &table, by_row: &by_row, graph: &graph };
1787            for (i, &o) in table.commits.iter().enumerate() {
1788                let oracle = walk_closure(&dag, space.oid(o).unwrap());
1789                assert_eq!(bits_as_oids(&table.bitmaps[i], &space), oracle, "seed {seed}: extended entry {i}");
1790            }
1791            for row in 0..graph.len() as u32 {
1792                let mut acc = RoaringBitmap::new();
1793                accumulate(row, &view, &facts, &mut acc);
1794                let oracle = walk_closure(&dag, space.oid(graph.ords[row as usize]).unwrap());
1795                assert_eq!(bits_as_oids(&acc, &space), oracle, "seed {seed}: walk from row {row} after the extension");
1796            }
1797            // And the sealed spelling, renumbered into the sorted space, equals
1798            // a sealed build over the same nodes.
1799            let sorted_space = OrdinalSpace::new(&dag.sorted, 20);
1800            let remap = table_space.remap_to(&sorted_space);
1801            let sealed = table.to_entries(&sorted_space, Some(&remap)).expect("exact remap");
1802            for e in &sealed {
1803                let oracle = walk_closure(&dag, &hex::decode(&e.commit).unwrap());
1804                assert_eq!(bits_as_oids(&e.bitmap, &sorted_space), oracle, "seed {seed}: sealed entry {}", e.commit);
1805            }
1806        }
1807    }
1808}