1use 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
60pub const NONE: u32 = u32::MAX;
62
63#[derive(Debug, Clone, Copy)]
65pub struct ReachPolicy {
66 pub max_commits: usize,
71}
72
73impl Default for ReachPolicy {
74 fn default() -> Self {
75 Self { max_commits: 512 }
78 }
79}
80
81#[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
97pub trait TreeSource: Sync {
112 fn tree(&self, oid: &[u8]) -> Option<std::borrow::Cow<'_, [u8]>>;
113}
114
115impl 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
129const 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#[derive(Clone, Copy)]
163pub struct OrdinalSpace<'a> {
164 base: &'a [u8],
165 fanout: &'a [u32],
167 extra: &'a [u8],
168 extra_sorted: &'a [u32],
169 oid_len: usize,
170}
171
172impl<'a> OrdinalSpace<'a> {
173 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 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 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 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 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 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#[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 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 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 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 if v.ordinal_of_raw(o) != Some((t.base_len() + i) as u32) {
347 return None;
348 }
349 }
350 Some(t)
351 }
352
353 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 pub fn extra_len(&self) -> usize {
389 if self.oid_len == 0 { 0 } else { self.extra.len() / self.oid_len }
390 }
391
392 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 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 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 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 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
468struct 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#[derive(Clone, Default)]
514pub struct OrdGraph {
515 pub ords: Vec<u32>,
517 pub tree: Vec<u32>,
519 pub parents: Vec<Vec<u32>>,
521 pub generation: Vec<u32>,
523 commits: RoaringBitmap,
525 row_by_rank: Vec<u32>,
527}
528
529impl OrdGraph {
530 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 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 #[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 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#[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 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 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 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 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 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
729pub 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
764pub struct ObjectFacts<'a> {
766 pub ordinal: OrdinalSpace<'a>,
768 pub trees: &'a dyn TreeSource,
771 pub oid_len: usize,
773}
774
775pub(crate) fn fill_tree(root: u32, facts: &ObjectFacts<'_>, acc: &mut RoaringBitmap) {
785 fill_tree_counted(root, facts, acc, &mut BuildCount::default());
786}
787
788#[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 if let Some(c) = facts.ordinal.ordinal_of_raw(e.oid) {
816 stack.push(c);
817 }
818 }
819 }
820 }
821}
822
823fn 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
847pub 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 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
945const EXTEND_STRIDE: usize = 64;
948
949pub 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#[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 #[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
1016pub fn accumulate(row: u32, view: &ReachView<'_>, facts: &ObjectFacts<'_>, acc: &mut RoaringBitmap) {
1030 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 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 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
1069pub fn needs_payload(kind: GitObjectKind) -> bool {
1075 matches!(kind, GitObjectKind::Commit | GitObjectKind::Tree)
1076}
1077
1078pub 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 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
1132pub 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
1140fn 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
1149pub 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
1175pub 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 let mut v = oids.to_vec();
1216 v.sort_unstable();
1217 v
1218 }
1219
1220 #[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 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 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 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 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 #[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 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 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 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 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 let shrunk: Vec<u32> = (0..3011).collect();
1321 assert!(OrdinalTable::from_parts(&sorted_concat(&shrunk), t.extra_raw(), 20).is_none());
1322
1323 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 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 #[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 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 #[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 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 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 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 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 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 #[test]
1514 fn ordinal_space_is_the_inverse_of_the_slice() {
1515 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 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 #[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 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 struct Dag {
1582 nodes: Vec<CommitNode>,
1583 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 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 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 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 #[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 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 #[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 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 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 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 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 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}