1use std::borrow::Cow;
4use std::collections::{HashMap, HashSet, VecDeque};
5use std::fs;
6use std::io::{Read, Seek, SeekFrom, Write};
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::time::SystemTime;
10
11use tempfile::NamedTempFile;
12use thiserror::Error;
13
14use crate::ewah_bitmap::{Bitmap, EwahBitmap};
15use crate::gc::collect_referenced_object_roots;
16use crate::ident::parse_signature_times;
17use crate::objects::{parse_commit, parse_tag, Object, ObjectId, ObjectKind, TreeRefIter};
18use crate::pack::{read_pack_index, PackIndex, SingleLocalPackReader};
19use crate::pack_name_hash::pack_name_hash_extend;
20use crate::pack_rev::{
21 append_hashfile_checksum, build_pack_rev_bytes_from_index_order_offsets_and_checksum,
22 rev_path_for_index, verify_pack_rev_file,
23};
24use crate::refs;
25use crate::repo::Repository;
26use crate::shallow::load_shallow_boundaries;
27
28use super::order::BitmapOrder;
29
30const BITMAP_MAGIC: &[u8; 4] = b"BITM";
31const BITMAP_VERSION: u16 = 1;
32const BITMAP_OPT_FULL_DAG: u16 = 0x1;
33const BITMAP_OPT_HASH_CACHE: u16 = 0x4;
34const BITMAP_OPT_LOOKUP_TABLE: u16 = 0x10;
35const MAX_XOR_SEARCH: usize = 10;
36const XOR_ROW_NONE: u32 = 0xffff_ffff;
37
38#[derive(Debug, Clone, Copy)]
44struct HistorySamplePlan {
45 recent_dense_end: usize,
47 mid_history_end: usize,
49 mid_stride_base: usize,
50 mid_stride_step: usize,
51 far_stride_base: usize,
52 far_stride_step: usize,
53}
54
55impl Default for HistorySamplePlan {
56 fn default() -> Self {
57 Self {
58 recent_dense_end: 96,
59 mid_history_end: 4096,
60 mid_stride_base: 12,
61 mid_stride_step: 3,
62 far_stride_base: 384,
63 far_stride_step: 48,
64 }
65 }
66}
67
68impl HistorySamplePlan {
69 fn mid_stride(&self, rank: usize) -> usize {
70 debug_assert!(rank >= self.recent_dense_end && rank < self.mid_history_end);
71 let depth = rank - self.recent_dense_end;
72 self.mid_stride_base + (depth / 64) * self.mid_stride_step
73 }
74
75 fn far_stride(&self, rank: usize) -> usize {
76 debug_assert!(rank >= self.mid_history_end);
77 let depth = rank - self.mid_history_end;
78 self.far_stride_base + (depth / 32) * self.far_stride_step
79 }
80
81 fn gap_before_window(&self, rank: usize) -> usize {
82 if rank < self.recent_dense_end {
83 0
84 } else if rank < self.mid_history_end {
85 self.mid_stride(rank)
86 } else {
87 self.far_stride(rank)
88 }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct PackBitmapWriteOptions {
95 pub full_dag: bool,
97 pub hash_cache: bool,
99 pub lookup_table: bool,
101 pub prefer_bitmap_tips: Vec<String>,
103 pub verify_reachability_closure: bool,
105}
106
107impl Default for PackBitmapWriteOptions {
108 fn default() -> Self {
109 Self {
110 full_dag: true,
111 hash_cache: true,
112 lookup_table: false,
113 prefer_bitmap_tips: Vec::new(),
114 verify_reachability_closure: true,
115 }
116 }
117}
118
119#[derive(Debug, Error, PartialEq, Eq)]
121#[non_exhaustive]
122pub enum PackBitmapWriteError {
123 #[error("pack is not closed under reachability from tips")]
125 NotClosed {
126 missing: Vec<ObjectId>,
128 },
129 #[error("bitmap write requires a files-backed object database")]
131 NotFilesBacked,
132 #[error("invalid pack index path: {0}")]
134 InvalidPackPath(String),
135 #[error("I/O error: {0}")]
137 Io(String),
138 #[error("object read error: {0}")]
140 Object(String),
141}
142
143impl PackBitmapWriteError {
144 fn io(err: std::io::Error) -> Self {
145 Self::Io(err.to_string())
146 }
147
148 #[must_use]
150 pub fn not_closed_missing(&self) -> Option<&[ObjectId]> {
151 match self {
152 Self::NotClosed { missing } => Some(missing.as_slice()),
153 _ => None,
154 }
155 }
156}
157
158pub struct MidxBitmapWriter;
160
161impl MidxBitmapWriter {
162 pub fn write_for_midx_layer(
164 repo: &Repository,
165 midx_bytes: &[u8],
166 rid_order: &[u32],
167 midx_checksum: &[u8],
168 bitmap_path: &Path,
169 options: &PackBitmapWriteOptions,
170 ) -> Result<PathBuf, PackBitmapWriteError> {
171 let order = BitmapOrder::from_midx_bytes_and_rid_order(midx_bytes, rid_order)
172 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
173 write_bitmap_for_order(repo, &order, midx_checksum, bitmap_path, options)
174 }
175
176 pub fn write(
185 repo: &Repository,
186 _pack_dir: &Path,
187 midx_checksum: &[u8],
188 bitmap_path: &Path,
189 options: &PackBitmapWriteOptions,
190 _now: SystemTime,
191 ) -> Result<PathBuf, PackBitmapWriteError> {
192 if !repo.odb().uses_files_primary() {
193 return Err(PackBitmapWriteError::NotFilesBacked);
194 }
195 let objects_dir = repo
196 .odb()
197 .files_objects_dir()
198 .ok_or(PackBitmapWriteError::NotFilesBacked)?;
199 let order = BitmapOrder::load_midx(objects_dir)
200 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?
201 .ok_or_else(|| PackBitmapWriteError::InvalidPackPath("no MIDX loaded".into()))?;
202 write_bitmap_for_order(repo, &order, midx_checksum, bitmap_path, options)
203 }
204}
205
206pub struct PackBitmapWriter;
208
209impl PackBitmapWriter {
210 pub fn write(
219 repo: &Repository,
220 pack_idx_path: &Path,
221 options: &PackBitmapWriteOptions,
222 _now: SystemTime,
223 ) -> Result<PathBuf, PackBitmapWriteError> {
224 if !repo.odb().uses_files_primary() {
225 return Err(PackBitmapWriteError::NotFilesBacked);
226 }
227 let idx = read_pack_index(pack_idx_path)
228 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
229 let objects_dir = repo
230 .odb()
231 .files_objects_dir()
232 .ok_or(PackBitmapWriteError::NotFilesBacked)?;
233 ensure_rev_sidecar(&idx)?;
234 let order = BitmapOrder::load_pack(objects_dir, Arc::new(idx))
235 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
236 let pack_checksum = read_pack_trailer(
237 order.pack_index().as_ref().ok_or_else(|| {
238 PackBitmapWriteError::InvalidPackPath("pack index missing".into())
239 })?,
240 repo.odb().hash_algo().len(),
241 )?;
242 let bitmap_path = bitmap_path_for_index(pack_idx_path);
243 write_bitmap_for_order(repo, &order, &pack_checksum, &bitmap_path, options)
244 }
245}
246
247fn write_bitmap_for_order(
252 repo: &Repository,
253 order: &BitmapOrder,
254 header_checksum: &[u8],
255 bitmap_path: &Path,
256 options: &PackBitmapWriteOptions,
257) -> Result<PathBuf, PackBitmapWriteError> {
258 repo.odb().with_pack_read_context(|| {
259 write_bitmap_for_order_in_context(repo, order, header_checksum, bitmap_path, options)
260 })
261}
262
263fn write_bitmap_for_order_in_context(
264 repo: &Repository,
265 order: &BitmapOrder,
266 header_checksum: &[u8],
267 bitmap_path: &Path,
268 options: &PackBitmapWriteOptions,
269) -> Result<PathBuf, PackBitmapWriteError> {
270 let roots = collect_referenced_object_roots(repo.git_dir())
271 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
272 let strict = options.verify_reachability_closure;
273 let shallow = load_shallow_boundaries(repo.git_dir());
275 let (type_bitmaps, commits) = build_type_bitmaps_and_commits(repo, order, shallow)?;
276 let tips = commit_tips_from_roots(repo, order, &commits, &roots, strict)?;
277 let needs_bitmap = tip_needs_flags(repo, &tips, &options.prefer_bitmap_tips)?;
278 let hash_cache_len = hash_cache_row_count(order);
279 let mut name_hashes = vec![0u32; hash_cache_len];
280 let mut selected = select_commits(&commits.rows, &tips, &needs_bitmap);
281 if !strict {
282 selected.retain(|c| order.position_of(c).is_some());
283 }
284 let commit_bitmaps =
285 build_commit_bitmaps(repo, order, &commits, &selected, &mut name_hashes, strict)?;
286 let (selected_sorted, xor_plan) = plan_xor_compression(&commits, &selected, &commit_bitmaps);
287 let bytes = assemble_bitmap_bytes(
288 order,
289 header_checksum,
290 options,
291 &type_bitmaps,
292 &selected_sorted,
293 &xor_plan,
294 &name_hashes,
295 )?;
296 write_bitmap_atomically(bitmap_path, &bytes)?;
297 repo.caches().bitmap_index().set(None);
298 Ok(bitmap_path.to_path_buf())
299}
300
301fn bitmap_path_for_index(idx_path: &Path) -> PathBuf {
302 let stem = idx_path
303 .file_stem()
304 .and_then(|s| s.to_str())
305 .unwrap_or("pack");
306 idx_path
307 .parent()
308 .map(|p| p.join(format!("{stem}.bitmap")))
309 .unwrap_or_else(|| PathBuf::from(format!("{stem}.bitmap")))
310}
311
312fn read_pack_trailer(idx: &PackIndex, hash_len: usize) -> Result<Vec<u8>, PackBitmapWriteError> {
313 let mut file = fs::File::open(&idx.pack_path).map_err(PackBitmapWriteError::io)?;
314 let len = file.metadata().map_err(PackBitmapWriteError::io)?.len();
315 let trailer_len = hash_len as u64;
316 if len < trailer_len {
317 return Err(PackBitmapWriteError::InvalidPackPath(
318 "pack file too short".into(),
319 ));
320 }
321 file.seek(SeekFrom::Start(len - trailer_len))
322 .map_err(PackBitmapWriteError::io)?;
323 let mut trailer = vec![0u8; hash_len];
324 file.read_exact(&mut trailer)
325 .map_err(PackBitmapWriteError::io)?;
326 Ok(trailer)
327}
328
329fn ensure_rev_sidecar(idx: &PackIndex) -> Result<(), PackBitmapWriteError> {
330 let rev_path = rev_path_for_index(&idx.idx_path);
331 if rev_path.is_file() && verify_pack_rev_file(&rev_path, idx).is_ok() {
332 return Ok(());
333 }
334 let hash_len = idx.hash_bytes();
335 let pack_checksum = read_pack_trailer(idx, hash_len)?;
336 let offsets: Vec<u64> = (0..idx.len())
337 .map(|i| idx.offset_at(i))
338 .collect::<crate::error::Result<Vec<_>>>()
339 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
340 let rev_bytes =
341 build_pack_rev_bytes_from_index_order_offsets_and_checksum(&offsets, &pack_checksum);
342 let tmp =
343 NamedTempFile::new_in(rev_path.parent().ok_or_else(|| {
344 PackBitmapWriteError::InvalidPackPath("rev path has no parent".into())
345 })?)
346 .map_err(PackBitmapWriteError::io)?;
347 tmp.as_file()
348 .write_all(&rev_bytes)
349 .map_err(PackBitmapWriteError::io)?;
350 tmp.persist(&rev_path)
351 .map_err(|e| PackBitmapWriteError::io(e.error))?;
352 Ok(())
353}
354
355fn write_bitmap_atomically(path: &Path, bytes: &[u8]) -> Result<(), PackBitmapWriteError> {
356 let parent = path
357 .parent()
358 .ok_or_else(|| PackBitmapWriteError::InvalidPackPath("bitmap path has no parent".into()))?;
359 let tmp = NamedTempFile::new_in(parent).map_err(PackBitmapWriteError::io)?;
360 tmp.as_file()
361 .write_all(bytes)
362 .map_err(PackBitmapWriteError::io)?;
363 tmp.persist(path)
364 .map_err(|e| PackBitmapWriteError::io(e.error))?;
365 Ok(())
366}
367
368#[derive(Clone)]
369struct CommitRow {
370 oid: ObjectId,
371 committer_date: i64,
372 tree: ObjectId,
373 parents: Vec<ObjectId>,
374}
375
376struct CommitTable {
380 rows: Vec<CommitRow>,
382 by_oid: HashMap<ObjectId, usize>,
383 shallow: HashSet<ObjectId>,
384}
385
386impl CommitTable {
387 fn new(mut rows: Vec<CommitRow>, shallow: HashSet<ObjectId>) -> Self {
388 rows.sort_by_key(|c| std::cmp::Reverse(c.committer_date));
389 for row in rows.iter_mut().filter(|row| shallow.contains(&row.oid)) {
390 row.parents.clear();
391 }
392 let by_oid = rows.iter().enumerate().map(|(i, c)| (c.oid, i)).collect();
393 Self {
394 rows,
395 by_oid,
396 shallow,
397 }
398 }
399
400 fn graft_parents(&self, oid: &ObjectId, parents: Vec<ObjectId>) -> Vec<ObjectId> {
402 if self.shallow.contains(oid) {
403 Vec::new()
404 } else {
405 parents
406 }
407 }
408
409 fn index_of(&self, oid: &ObjectId) -> Option<usize> {
410 self.by_oid.get(oid).copied()
411 }
412
413 fn get(&self, oid: &ObjectId) -> Option<&CommitRow> {
414 self.index_of(oid).map(|i| &self.rows[i])
415 }
416
417 fn date(&self, oid: &ObjectId) -> i64 {
419 self.get(oid).map_or(0, |c| c.committer_date)
420 }
421
422 fn parents(
424 &self,
425 repo: &Repository,
426 oid: &ObjectId,
427 ) -> Result<Cow<'_, [ObjectId]>, PackBitmapWriteError> {
428 if let Some(row) = self.get(oid) {
429 return Ok(Cow::Borrowed(&row.parents));
430 }
431 let obj = read_object(repo, oid)?;
432 let commit =
433 parse_commit(&obj.data).map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
434 Ok(Cow::Owned(self.graft_parents(oid, commit.parents)))
435 }
436}
437
438fn read_object(repo: &Repository, oid: &ObjectId) -> Result<Object, PackBitmapWriteError> {
439 repo.odb()
440 .read(oid)
441 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))
442}
443
444fn pack_entry_kinds(
450 repo: &Repository,
451 order: &BitmapOrder,
452) -> Result<Vec<Option<ObjectKind>>, PackBitmapWriteError> {
453 let (Some(idx), Some(objects_dir)) = (order.pack_index(), repo.odb().files_objects_dir())
454 else {
455 return Ok(Vec::new());
456 };
457 let Some(reader) = SingleLocalPackReader::open_for_pack_index(objects_dir, Arc::clone(&idx))
458 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?
459 else {
460 return Ok(Vec::new());
461 };
462 let offsets = (0..order.object_count())
463 .map(|pos| {
464 let row = order.storage_row_for_bitmap_rank(pos).ok_or_else(|| {
465 PackBitmapWriteError::Object("bitmap position out of range".into())
466 })?;
467 idx.offset_at(row as usize)
468 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))
469 })
470 .collect::<Result<Vec<u64>, _>>()?;
471 reader
472 .object_kinds_at_sorted_offsets(&offsets)
473 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))
474}
475
476fn build_type_bitmaps_and_commits(
481 repo: &Repository,
482 order: &BitmapOrder,
483 shallow: HashSet<ObjectId>,
484) -> Result<(TypeBitmaps, CommitTable), PackBitmapWriteError> {
485 let mut commits = Vec::new();
486 let mut commit_bm = Bitmap::new();
487 let mut trees = Bitmap::new();
488 let mut blobs = Bitmap::new();
489 let mut tags = Bitmap::new();
490 let header_kinds = pack_entry_kinds(repo, order)?;
491 let n = order.object_count();
492 for pos in 0..n {
493 let oid = order
494 .oid_at(pos)
495 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
496 let p = pos as usize;
497 let kind = match header_kinds.get(p).copied().flatten() {
498 Some(kind) => kind,
499 None => {
500 repo.odb()
501 .read_info(&oid)
502 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?
503 .kind
504 }
505 };
506 match kind {
507 ObjectKind::Commit => {
508 commit_bm.set(p);
509 let obj = read_object(repo, &oid)?;
510 let c = parse_commit(&obj.data)
511 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
512 let committer_date = parse_signature_times(&c.committer)
513 .map(|t| t.unix_seconds)
514 .unwrap_or(0);
515 commits.push(CommitRow {
516 oid,
517 committer_date,
518 tree: c.tree,
519 parents: c.parents,
520 });
521 }
522 ObjectKind::Tree => trees.set(p),
523 ObjectKind::Blob => blobs.set(p),
524 ObjectKind::Tag => tags.set(p),
525 }
526 }
527 Ok((
528 TypeBitmaps {
529 commits: EwahBitmap::from_bitmap(&commit_bm),
530 trees: EwahBitmap::from_bitmap(&trees),
531 blobs: EwahBitmap::from_bitmap(&blobs),
532 tags: EwahBitmap::from_bitmap(&tags),
533 },
534 CommitTable::new(commits, shallow),
535 ))
536}
537
538fn commit_tips_from_roots(
550 repo: &Repository,
551 order: &BitmapOrder,
552 commits: &CommitTable,
553 roots: &[ObjectId],
554 strict: bool,
555) -> Result<Vec<ObjectId>, PackBitmapWriteError> {
556 const MAX_REPORTED_MISSING: usize = 32;
557 let mut tips = Vec::new();
558 let mut missing = Vec::new();
559 let mut seen_trees = HashSet::new();
560 for &root in roots {
561 let mut oid = root;
562 loop {
563 if strict && order.position_of(&oid).is_none() {
564 missing.push(oid);
565 break;
566 }
567 if commits.get(&oid).is_some() {
568 tips.push(oid);
569 break;
570 }
571 let obj = read_object(repo, &oid)?;
572 match obj.kind {
573 ObjectKind::Commit => {
574 tips.push(oid);
575 break;
576 }
577 ObjectKind::Tag => {
578 oid = parse_tag(&obj.data)
579 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?
580 .object;
581 }
582 ObjectKind::Tree => {
583 if strict {
584 collect_missing_in_tree(repo, order, oid, &mut seen_trees, &mut missing)?;
585 }
586 break;
587 }
588 ObjectKind::Blob => break,
589 }
590 }
591 if missing.len() >= MAX_REPORTED_MISSING {
592 break;
593 }
594 }
595 if !missing.is_empty() {
596 missing.truncate(MAX_REPORTED_MISSING);
597 return Err(PackBitmapWriteError::NotClosed { missing });
598 }
599 tips.sort_by_key(|o| o.to_hex());
600 tips.dedup();
601 Ok(tips)
602}
603
604fn collect_missing_in_tree(
606 repo: &Repository,
607 order: &BitmapOrder,
608 tree: ObjectId,
609 seen: &mut HashSet<ObjectId>,
610 missing: &mut Vec<ObjectId>,
611) -> Result<(), PackBitmapWriteError> {
612 let hash_len = repo.odb().hash_algo().len();
613 let mut stack = vec![tree];
614 while let Some(tree_oid) = stack.pop() {
615 if !seen.insert(tree_oid) {
616 continue;
617 }
618 let obj = read_object(repo, &tree_oid)?;
619 for entry in TreeRefIter::with_oid_len(&obj.data, hash_len) {
620 let entry = entry.map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
621 let kind = TreeChild::from_mode(entry.mode);
622 if kind == TreeChild::Gitlink {
623 continue;
624 }
625 if order.position_of(&entry.oid).is_none() {
626 missing.push(entry.oid);
627 } else if kind == TreeChild::Tree {
628 stack.push(entry.oid);
629 }
630 }
631 }
632 Ok(())
633}
634
635#[derive(Debug, Clone, Copy, PartialEq, Eq)]
637enum TreeChild {
638 Tree,
639 Gitlink,
640 Blob,
641}
642
643impl TreeChild {
644 const TYPE_MASK: u32 = 0o170000;
645 const TREE: u32 = 0o040000;
646 const GITLINK: u32 = 0o160000;
647
648 fn from_mode(mode: u32) -> Self {
649 match mode & Self::TYPE_MASK {
650 Self::TREE => Self::Tree,
651 Self::GITLINK => Self::Gitlink,
652 _ => Self::Blob,
653 }
654 }
655}
656
657fn tip_needs_flags(
658 repo: &Repository,
659 tips: &[ObjectId],
660 prefer_patterns: &[String],
661) -> Result<HashSet<ObjectId>, PackBitmapWriteError> {
662 let mut needs = HashSet::new();
663 if prefer_patterns.is_empty() {
664 needs.extend(tips.iter().copied());
665 return Ok(needs);
666 }
667 let git_dir = &repo.git_dir();
668 for (name, oid) in refs::list_refs(git_dir, "refs/")
669 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?
670 {
671 let peeled = peel_to_commit(repo, oid)?;
672 let Some(commit) = peeled else { continue };
673 if prefer_patterns
674 .iter()
675 .any(|pat| ref_matches_pattern(&name, pat))
676 {
677 needs.insert(commit);
678 }
679 }
680 needs.extend(tips.iter().copied());
681 Ok(needs)
682}
683
684fn ref_matches_pattern(ref_name: &str, pattern: &str) -> bool {
685 if pattern.is_empty() {
686 return false;
687 }
688 if ref_name == pattern {
689 return true;
690 }
691 if pattern.ends_with('/') {
692 ref_name.starts_with(pattern) || format!("{ref_name}/").starts_with(pattern)
693 } else {
694 ref_name == pattern
695 || ref_name
696 .strip_prefix("refs/")
697 .is_some_and(|r| r == pattern || r.starts_with(&format!("{pattern}/")))
698 }
699}
700
701fn peel_to_commit(
702 repo: &Repository,
703 oid: ObjectId,
704) -> Result<Option<ObjectId>, PackBitmapWriteError> {
705 repo.odb()
706 .peel_to_commit(oid)
707 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))
708}
709
710fn pick_window_commit(window: &[CommitRow], needs_bitmap: &HashSet<ObjectId>) -> ObjectId {
711 if window.len() == 1 {
712 return window[0].oid;
713 }
714 let mut chosen = window[window.len() - 1].oid;
715 for cm in window {
716 if needs_bitmap.contains(&cm.oid) {
717 return cm.oid;
718 }
719 if cm.parents.len() > 1 {
720 chosen = cm.oid;
721 }
722 }
723 chosen
724}
725
726fn select_commits(
727 commits: &[CommitRow],
728 tips: &[ObjectId],
729 needs_bitmap: &HashSet<ObjectId>,
730) -> Vec<ObjectId> {
731 let mut selected: HashSet<ObjectId> = tips.iter().copied().collect();
732 let nr = commits.len();
733 let plan = HistorySamplePlan::default();
734 if nr <= plan.recent_dense_end {
735 selected.extend(commits.iter().map(|c| c.oid));
736 } else {
737 let mut rank = 0usize;
738 while rank < nr {
739 let gap = plan.gap_before_window(rank);
740 if rank + gap >= nr {
741 break;
742 }
743 let window_end = rank + gap;
744 let window = &commits[rank..=window_end];
745 selected.insert(pick_window_commit(window, needs_bitmap));
746 rank = window_end + 1;
747 }
748 }
749 let mut list: Vec<ObjectId> = selected.into_iter().collect();
750 list.sort_by_key(|o| o.to_hex());
751 list
752}
753
754struct TypeBitmaps {
755 commits: EwahBitmap,
756 trees: EwahBitmap,
757 blobs: EwahBitmap,
758 tags: EwahBitmap,
759}
760
761fn build_commit_bitmaps(
762 repo: &Repository,
763 order: &BitmapOrder,
764 commits: &CommitTable,
765 selected: &[ObjectId],
766 name_hashes: &mut [u32],
767 strict_closure: bool,
768) -> Result<HashMap<ObjectId, Bitmap>, PackBitmapWriteError> {
769 let mut topo = selected.to_vec();
770 topo.extend(first_parent_fork_points(commits, selected));
771 topo.sort_by_key(|a| commits.date(a));
772 let mut fill = TreeBitmapFill {
773 repo,
774 order,
775 hash_len: repo.odb().hash_algo().len(),
776 name_hashes,
777 strict_closure,
778 };
779 let mut computed: HashMap<ObjectId, Bitmap> = HashMap::new();
780 for commit in topo {
781 let mut bm = seed_from_computed_ancestors(repo, commits, commit, &computed)?;
782 fill_commit_bitmap(&mut fill, commits, &mut bm, commit, &computed)?;
783 computed.insert(commit, bm);
784 }
785 Ok(computed)
786}
787
788fn first_parent_fork_points(commits: &CommitTable, selected: &[ObjectId]) -> Vec<ObjectId> {
796 let mut visited = vec![false; commits.rows.len()];
797 let mut children = vec![0u32; commits.rows.len()];
798 let mut is_selected = vec![false; commits.rows.len()];
799 let starts: Vec<usize> = selected
800 .iter()
801 .filter_map(|c| commits.index_of(c))
802 .collect();
803 for &i in &starts {
804 is_selected[i] = true;
805 }
806 for &start in &starts {
807 if std::mem::replace(&mut visited[start], true) {
808 continue;
809 }
810 let mut cur = start;
811 while let Some(parent) = commits.rows[cur]
812 .parents
813 .first()
814 .and_then(|p| commits.index_of(p))
815 {
816 children[parent] += 1;
817 if std::mem::replace(&mut visited[parent], true) {
818 break;
819 }
820 cur = parent;
821 }
822 }
823 let mut forks: Vec<ObjectId> = (0..commits.rows.len())
824 .filter(|&i| children[i] >= 2 && !is_selected[i])
825 .map(|i| commits.rows[i].oid)
826 .collect();
827 forks.sort_by_key(|o| o.to_hex());
828 forks
829}
830
831fn seed_from_computed_ancestors(
832 repo: &Repository,
833 commits: &CommitTable,
834 commit: ObjectId,
835 computed: &HashMap<ObjectId, Bitmap>,
836) -> Result<Bitmap, PackBitmapWriteError> {
837 let mut bm = Bitmap::new();
838 for &parent in commits.parents(repo, &commit)?.iter() {
839 let mut walk = parent;
840 loop {
841 if let Some(ancestor_bm) = computed.get(&walk) {
842 bm.or_assign(ancestor_bm);
843 break;
844 }
845 match commits.parents(repo, &walk)?.first().copied() {
846 Some(next) => walk = next,
847 None => break,
848 }
849 }
850 }
851 Ok(bm)
852}
853
854fn hash_cache_row_count(order: &BitmapOrder) -> usize {
855 order
856 .pack_index()
857 .map_or(order.object_count() as usize, |idx| idx.len())
858}
859
860fn child_name_hash(parent: Option<u32>, name: &[u8]) -> u32 {
862 match parent {
863 None => pack_name_hash_extend(0, name),
864 Some(hash) => pack_name_hash_extend(pack_name_hash_extend(hash, b"/"), name),
865 }
866}
867
868fn fill_commit_bitmap(
869 fill: &mut TreeBitmapFill<'_>,
870 commits: &CommitTable,
871 bitmap: &mut Bitmap,
872 tip: ObjectId,
873 computed: &HashMap<ObjectId, Bitmap>,
874) -> Result<(), PackBitmapWriteError> {
875 let mut queue: VecDeque<ObjectId> = VecDeque::new();
876 queue.push_back(tip);
877 while let Some(oid) = queue.pop_front() {
878 let Some((pos, _)) = fill.locate(&oid)? else {
879 continue;
880 };
881 let p = pos as usize;
882 if bitmap.get(p) {
883 continue;
884 }
885 if let Some(cached) = computed.get(&oid) {
886 bitmap.or_assign(cached);
887 continue;
888 }
889 bitmap.set(p);
890 let Some(row) = commits.get(&oid) else {
892 continue;
893 };
894 queue.extend(row.parents.iter().copied());
895 if let Some((tree_pos, tree_row)) = fill.locate(&row.tree)? {
896 fill.fill_tree(bitmap, row.tree, tree_pos, tree_row, None)?;
897 }
898 }
899 Ok(())
900}
901
902struct TreeBitmapFill<'a> {
904 repo: &'a Repository,
905 order: &'a BitmapOrder,
906 hash_len: usize,
907 name_hashes: &'a mut [u32],
908 strict_closure: bool,
909}
910
911impl TreeBitmapFill<'_> {
912 fn locate(&self, oid: &ObjectId) -> Result<Option<(u32, u32)>, PackBitmapWriteError> {
915 match self.order.locate(oid) {
916 Some(slot) => Ok(Some(slot)),
917 None if self.strict_closure => Err(PackBitmapWriteError::NotClosed {
918 missing: vec![*oid],
919 }),
920 None => Ok(None),
921 }
922 }
923
924 fn store_name_hash(&mut self, row: u32, hash: u32) {
925 if let Some(slot) = self.name_hashes.get_mut(row as usize) {
926 *slot = hash;
927 }
928 }
929
930 fn fill_tree(
937 &mut self,
938 bitmap: &mut Bitmap,
939 tree: ObjectId,
940 pos: u32,
941 row: u32,
942 name_hash: Option<u32>,
943 ) -> Result<(), PackBitmapWriteError> {
944 let p = pos as usize;
945 if bitmap.get(p) {
946 return Ok(());
947 }
948 bitmap.set(p);
949 if let Some(hash) = name_hash {
950 self.store_name_hash(row, hash);
951 }
952 let obj = read_object(self.repo, &tree)?;
953 for entry in TreeRefIter::with_oid_len(&obj.data, self.hash_len) {
954 let entry = entry.map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
955 let kind = TreeChild::from_mode(entry.mode);
956 if kind == TreeChild::Gitlink {
957 continue;
958 }
959 let Some((child_pos, child_row)) = self.locate(&entry.oid)? else {
960 continue;
961 };
962 let hash = child_name_hash(name_hash, entry.name);
963 if kind == TreeChild::Tree {
964 self.fill_tree(bitmap, entry.oid, child_pos, child_row, Some(hash))?;
965 } else {
966 bitmap.set(child_pos as usize);
967 self.store_name_hash(child_row, hash);
968 }
969 }
970 Ok(())
971 }
972}
973
974struct XorPlan {
975 xor_prev: Vec<Option<usize>>,
977 payload: Vec<EwahBitmap>,
978}
979
980fn plan_xor_compression(
981 commits: &CommitTable,
982 selected: &[ObjectId],
983 commit_bitmaps: &HashMap<ObjectId, Bitmap>,
984) -> (Vec<ObjectId>, XorPlan) {
985 let mut selected_sorted: Vec<ObjectId> = selected.to_vec();
986 selected_sorted.sort_by_key(|a| commits.date(a));
987 let n = selected_sorted.len();
988 let mut xor_prev = vec![None; n];
989 let mut payload = Vec::with_capacity(n);
990 for i in 0..n {
991 let base = &commit_bitmaps[&selected_sorted[i]];
992 let mut best_bm = base.clone();
993 let mut best_off = 0usize;
994 let mut best_words = best_bm.word_len();
995 for off in 1..=MAX_XOR_SEARCH.min(i) {
996 let mut candidate = base.clone();
997 candidate.xor_assign(&commit_bitmaps[&selected_sorted[i - off]]);
998 let words = candidate.word_len();
999 if words < best_words {
1000 best_bm = candidate;
1001 best_off = off;
1002 best_words = words;
1003 }
1004 }
1005 xor_prev[i] = if best_off == 0 {
1006 None
1007 } else {
1008 Some(i - best_off)
1009 };
1010 payload.push(EwahBitmap::from_bitmap(&best_bm));
1011 }
1012 (selected_sorted, XorPlan { xor_prev, payload })
1013}
1014
1015fn assemble_bitmap_bytes(
1016 order: &BitmapOrder,
1017 pack_checksum: &[u8],
1018 options: &PackBitmapWriteOptions,
1019 types: &TypeBitmaps,
1020 selected_sorted: &[ObjectId],
1021 xor_plan: &XorPlan,
1022 name_hashes: &[u32],
1023) -> Result<Vec<u8>, PackBitmapWriteError> {
1024 let hash_len = pack_checksum.len();
1025 let mut flags = 0u16;
1026 if options.full_dag {
1027 flags |= BITMAP_OPT_FULL_DAG;
1028 }
1029 if options.hash_cache {
1030 flags |= BITMAP_OPT_HASH_CACHE;
1031 }
1032 if options.lookup_table {
1033 flags |= BITMAP_OPT_LOOKUP_TABLE;
1034 }
1035 if flags & BITMAP_OPT_FULL_DAG == 0 {
1036 return Err(PackBitmapWriteError::InvalidPackPath(
1037 "FULL_DAG required".into(),
1038 ));
1039 }
1040
1041 let mut body = Vec::new();
1042 body.extend_from_slice(BITMAP_MAGIC);
1043 body.extend_from_slice(&BITMAP_VERSION.to_be_bytes());
1044 body.extend_from_slice(&flags.to_be_bytes());
1045 body.extend_from_slice(&(selected_sorted.len() as u32).to_be_bytes());
1046 body.extend_from_slice(pack_checksum);
1047
1048 types.commits.serialize(&mut body);
1049 types.trees.serialize(&mut body);
1050 types.blobs.serialize(&mut body);
1051 types.tags.serialize(&mut body);
1052
1053 let mut entry_offsets: Vec<u64> = Vec::new();
1054 for (i, commit) in selected_sorted.iter().enumerate() {
1055 let commit_pos =
1056 order
1057 .storage_row_for_oid(commit)
1058 .ok_or_else(|| PackBitmapWriteError::NotClosed {
1059 missing: vec![*commit],
1060 })?;
1061 if options.lookup_table {
1062 entry_offsets.push(body.len() as u64);
1063 }
1064 body.extend_from_slice(&commit_pos.to_be_bytes());
1065 let xor_off = xor_plan
1066 .xor_prev
1067 .get(i)
1068 .and_then(|opt| opt.map(|prev| i - prev))
1069 .unwrap_or(0);
1070 if xor_off > u8::MAX as usize || xor_off > MAX_XOR_SEARCH {
1071 return Err(PackBitmapWriteError::Object(
1072 "xor offset out of range".into(),
1073 ));
1074 }
1075 body.push(xor_off as u8);
1076 body.push(0); xor_plan
1078 .payload
1079 .get(i)
1080 .ok_or_else(|| PackBitmapWriteError::Object("missing xor payload".into()))?
1081 .serialize(&mut body);
1082 }
1083
1084 if options.lookup_table {
1085 write_lookup_table(&mut body, order, selected_sorted, &entry_offsets, xor_plan)?;
1086 }
1087
1088 if options.hash_cache {
1089 for &hash in name_hashes {
1090 body.extend_from_slice(&hash.to_be_bytes());
1091 }
1092 }
1093
1094 append_hashfile_checksum(&mut body, hash_len);
1095 Ok(body)
1096}
1097
1098fn write_lookup_table(
1099 body: &mut Vec<u8>,
1100 order: &BitmapOrder,
1101 selected: &[ObjectId],
1102 offsets: &[u64],
1103 xor_plan: &XorPlan,
1104) -> Result<(), PackBitmapWriteError> {
1105 let mut table_order: Vec<usize> = (0..selected.len()).collect();
1106 table_order.sort_by_key(|&i| order.storage_row_for_oid(&selected[i]).unwrap_or(u32::MAX));
1107 let mut inv = vec![0u32; selected.len()];
1108 for (rank, &i) in table_order.iter().enumerate() {
1109 inv[i] = u32::try_from(rank).unwrap_or(u32::MAX);
1110 }
1111 for &i in &table_order {
1112 let commit_pos = order.storage_row_for_oid(&selected[i]).unwrap_or(0);
1113 body.extend_from_slice(&commit_pos.to_be_bytes());
1114 body.extend_from_slice(&offsets[i].to_be_bytes());
1115 let xor_row = xor_plan
1116 .xor_prev
1117 .get(i)
1118 .copied()
1119 .flatten()
1120 .map(|prev| inv[prev])
1121 .unwrap_or(XOR_ROW_NONE);
1122 body.extend_from_slice(&xor_row.to_be_bytes());
1123 }
1124 Ok(())
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129 use super::*;
1130
1131 #[test]
1132 fn history_sample_plan_increases_stride_with_rank() {
1133 let plan = HistorySamplePlan::default();
1134 assert_eq!(plan.gap_before_window(10), 0);
1135 assert!(plan.gap_before_window(200) >= plan.mid_stride_base);
1136 assert!(plan.gap_before_window(10_000) >= plan.far_stride_base);
1137 }
1138
1139 #[test]
1140 fn tree_child_kind_comes_from_mode_bits() {
1141 assert_eq!(TreeChild::from_mode(0o040000), TreeChild::Tree);
1142 assert_eq!(TreeChild::from_mode(0o160000), TreeChild::Gitlink);
1143 for mode in [0o100644, 0o100755, 0o120000, 0o100664] {
1144 assert_eq!(TreeChild::from_mode(mode), TreeChild::Blob, "{mode:o}");
1145 }
1146 }
1147
1148 #[test]
1149 fn child_name_hash_matches_full_path_hash() {
1150 use crate::pack_name_hash::pack_name_hash;
1151 assert_eq!(
1152 child_name_hash(None, b"Cargo.toml"),
1153 pack_name_hash("Cargo.toml")
1154 );
1155 let src = child_name_hash(None, b"src");
1156 let cargo = child_name_hash(Some(src), b"cargo");
1157 assert_eq!(cargo, pack_name_hash("src/cargo"));
1158 assert_eq!(
1159 child_name_hash(Some(cargo), b"lib.rs"),
1160 pack_name_hash("src/cargo/lib.rs")
1161 );
1162 }
1163}