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_child;
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 pub reachability_roots: Option<Vec<crate::objects::ObjectId>>,
110}
111
112impl Default for PackBitmapWriteOptions {
113 fn default() -> Self {
114 Self {
115 full_dag: true,
116 hash_cache: true,
117 lookup_table: false,
118 prefer_bitmap_tips: Vec::new(),
119 verify_reachability_closure: true,
120 reachability_roots: None,
121 }
122 }
123}
124
125#[derive(Debug, Error, PartialEq, Eq)]
127#[non_exhaustive]
128pub enum PackBitmapWriteError {
129 #[error("pack is not closed under reachability from tips")]
131 NotClosed {
132 missing: Vec<ObjectId>,
134 },
135 #[error("bitmap write requires a files-backed object database")]
137 NotFilesBacked,
138 #[error("invalid pack index path: {0}")]
140 InvalidPackPath(String),
141 #[error("I/O error: {0}")]
143 Io(String),
144 #[error("object read error: {0}")]
146 Object(String),
147}
148
149impl PackBitmapWriteError {
150 fn io(err: std::io::Error) -> Self {
151 Self::Io(err.to_string())
152 }
153
154 #[must_use]
156 pub fn not_closed_missing(&self) -> Option<&[ObjectId]> {
157 match self {
158 Self::NotClosed { missing } => Some(missing.as_slice()),
159 _ => None,
160 }
161 }
162}
163
164pub struct MidxBitmapWriter;
166
167impl MidxBitmapWriter {
168 pub fn write_for_midx_layer(
170 repo: &Repository,
171 midx_bytes: &[u8],
172 rid_order: &[u32],
173 midx_checksum: &[u8],
174 bitmap_path: &Path,
175 options: &PackBitmapWriteOptions,
176 ) -> Result<PathBuf, PackBitmapWriteError> {
177 let order = BitmapOrder::from_midx_bytes_and_rid_order(midx_bytes, rid_order)
178 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
179 write_bitmap_for_order(repo, &order, midx_checksum, bitmap_path, options)
180 }
181
182 pub fn write(
191 repo: &Repository,
192 _pack_dir: &Path,
193 midx_checksum: &[u8],
194 bitmap_path: &Path,
195 options: &PackBitmapWriteOptions,
196 _now: SystemTime,
197 ) -> Result<PathBuf, PackBitmapWriteError> {
198 if !repo.odb().uses_files_primary() {
199 return Err(PackBitmapWriteError::NotFilesBacked);
200 }
201 let objects_dir = repo
202 .odb()
203 .files_objects_dir()
204 .ok_or(PackBitmapWriteError::NotFilesBacked)?;
205 let order = BitmapOrder::load_midx(objects_dir)
206 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?
207 .ok_or_else(|| PackBitmapWriteError::InvalidPackPath("no MIDX loaded".into()))?;
208 write_bitmap_for_order(repo, &order, midx_checksum, bitmap_path, options)
209 }
210}
211
212pub struct PackBitmapWriter;
214
215impl PackBitmapWriter {
216 pub fn write(
225 repo: &Repository,
226 pack_idx_path: &Path,
227 options: &PackBitmapWriteOptions,
228 _now: SystemTime,
229 ) -> Result<PathBuf, PackBitmapWriteError> {
230 if !repo.odb().uses_files_primary() {
231 return Err(PackBitmapWriteError::NotFilesBacked);
232 }
233 let idx = read_pack_index(pack_idx_path)
234 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
235 let objects_dir = repo
236 .odb()
237 .files_objects_dir()
238 .ok_or(PackBitmapWriteError::NotFilesBacked)?;
239 ensure_rev_sidecar(&idx)?;
240 let order = BitmapOrder::load_pack(objects_dir, Arc::new(idx))
241 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
242 let pack_checksum = read_pack_trailer(
243 order.pack_index().as_ref().ok_or_else(|| {
244 PackBitmapWriteError::InvalidPackPath("pack index missing".into())
245 })?,
246 repo.odb().hash_algo().len(),
247 )?;
248 let bitmap_path = bitmap_path_for_index(pack_idx_path);
249 write_bitmap_for_order(repo, &order, &pack_checksum, &bitmap_path, options)
250 }
251}
252
253fn write_bitmap_for_order(
258 repo: &Repository,
259 order: &BitmapOrder,
260 header_checksum: &[u8],
261 bitmap_path: &Path,
262 options: &PackBitmapWriteOptions,
263) -> Result<PathBuf, PackBitmapWriteError> {
264 repo.odb().with_pack_read_context(|| {
265 write_bitmap_for_order_in_context(repo, order, header_checksum, bitmap_path, options)
266 })
267}
268
269fn write_bitmap_for_order_in_context(
270 repo: &Repository,
271 order: &BitmapOrder,
272 header_checksum: &[u8],
273 bitmap_path: &Path,
274 options: &PackBitmapWriteOptions,
275) -> Result<PathBuf, PackBitmapWriteError> {
276 let roots = match &options.reachability_roots {
277 Some(roots) => roots.clone(),
278 None => collect_referenced_object_roots(repo.git_dir())
279 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?,
280 };
281 let strict = options.verify_reachability_closure;
282 let shallow = load_shallow_boundaries(repo.git_dir());
284 let (type_bitmaps, commits) = build_type_bitmaps_and_commits(repo, order, shallow)?;
285 let tips = commit_tips_from_roots(repo, order, &commits, &roots, strict)?;
286 let needs_bitmap = tip_needs_flags(repo, &tips, &options.prefer_bitmap_tips)?;
287 let hash_cache_len = hash_cache_row_count(order);
288 let mut name_hashes = vec![0u32; hash_cache_len];
289 let mut selected = select_commits(&commits.rows, &tips, &needs_bitmap);
290 if !strict {
291 selected.retain(|c| order.position_of(c).is_some());
292 }
293 let commit_bitmaps =
294 build_commit_bitmaps(repo, order, &commits, &selected, &mut name_hashes, strict)?;
295 let (selected_sorted, xor_plan) = plan_xor_compression(&commits, &selected, &commit_bitmaps);
296 let bytes = assemble_bitmap_bytes(
297 order,
298 header_checksum,
299 options,
300 &type_bitmaps,
301 &selected_sorted,
302 &xor_plan,
303 &name_hashes,
304 )?;
305 write_bitmap_atomically(bitmap_path, &bytes)?;
306 repo.caches().bitmap_index().set(None);
307 Ok(bitmap_path.to_path_buf())
308}
309
310fn bitmap_path_for_index(idx_path: &Path) -> PathBuf {
311 let stem = idx_path
312 .file_stem()
313 .and_then(|s| s.to_str())
314 .unwrap_or("pack");
315 idx_path
316 .parent()
317 .map(|p| p.join(format!("{stem}.bitmap")))
318 .unwrap_or_else(|| PathBuf::from(format!("{stem}.bitmap")))
319}
320
321fn read_pack_trailer(idx: &PackIndex, hash_len: usize) -> Result<Vec<u8>, PackBitmapWriteError> {
322 let mut file = fs::File::open(&idx.pack_path).map_err(PackBitmapWriteError::io)?;
323 let len = file.metadata().map_err(PackBitmapWriteError::io)?.len();
324 let trailer_len = hash_len as u64;
325 if len < trailer_len {
326 return Err(PackBitmapWriteError::InvalidPackPath(
327 "pack file too short".into(),
328 ));
329 }
330 file.seek(SeekFrom::Start(len - trailer_len))
331 .map_err(PackBitmapWriteError::io)?;
332 let mut trailer = vec![0u8; hash_len];
333 file.read_exact(&mut trailer)
334 .map_err(PackBitmapWriteError::io)?;
335 Ok(trailer)
336}
337
338fn ensure_rev_sidecar(idx: &PackIndex) -> Result<(), PackBitmapWriteError> {
339 let rev_path = rev_path_for_index(&idx.idx_path);
340 if rev_path.is_file() && verify_pack_rev_file(&rev_path, idx).is_ok() {
341 return Ok(());
342 }
343 let hash_len = idx.hash_bytes();
344 let pack_checksum = read_pack_trailer(idx, hash_len)?;
345 let offsets: Vec<u64> = (0..idx.len())
346 .map(|i| idx.offset_at(i))
347 .collect::<crate::error::Result<Vec<_>>>()
348 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
349 let rev_bytes =
350 build_pack_rev_bytes_from_index_order_offsets_and_checksum(&offsets, &pack_checksum);
351 let tmp =
352 NamedTempFile::new_in(rev_path.parent().ok_or_else(|| {
353 PackBitmapWriteError::InvalidPackPath("rev path has no parent".into())
354 })?)
355 .map_err(PackBitmapWriteError::io)?;
356 tmp.as_file()
357 .write_all(&rev_bytes)
358 .map_err(PackBitmapWriteError::io)?;
359 tmp.persist(&rev_path)
360 .map_err(|e| PackBitmapWriteError::io(e.error))?;
361 Ok(())
362}
363
364fn write_bitmap_atomically(path: &Path, bytes: &[u8]) -> Result<(), PackBitmapWriteError> {
365 let parent = path
366 .parent()
367 .ok_or_else(|| PackBitmapWriteError::InvalidPackPath("bitmap path has no parent".into()))?;
368 let tmp = NamedTempFile::new_in(parent).map_err(PackBitmapWriteError::io)?;
369 tmp.as_file()
370 .write_all(bytes)
371 .map_err(PackBitmapWriteError::io)?;
372 tmp.persist(path)
373 .map_err(|e| PackBitmapWriteError::io(e.error))?;
374 Ok(())
375}
376
377#[derive(Clone)]
378struct CommitRow {
379 oid: ObjectId,
380 committer_date: i64,
381 tree: ObjectId,
382 parents: Vec<ObjectId>,
383}
384
385struct CommitTable {
389 rows: Vec<CommitRow>,
391 by_oid: HashMap<ObjectId, usize>,
392 shallow: HashSet<ObjectId>,
393}
394
395impl CommitTable {
396 fn new(mut rows: Vec<CommitRow>, shallow: HashSet<ObjectId>) -> Self {
397 rows.sort_by_key(|c| std::cmp::Reverse(c.committer_date));
398 for row in rows.iter_mut().filter(|row| shallow.contains(&row.oid)) {
399 row.parents.clear();
400 }
401 let by_oid = rows.iter().enumerate().map(|(i, c)| (c.oid, i)).collect();
402 Self {
403 rows,
404 by_oid,
405 shallow,
406 }
407 }
408
409 fn graft_parents(&self, oid: &ObjectId, parents: Vec<ObjectId>) -> Vec<ObjectId> {
411 if self.shallow.contains(oid) {
412 Vec::new()
413 } else {
414 parents
415 }
416 }
417
418 fn index_of(&self, oid: &ObjectId) -> Option<usize> {
419 self.by_oid.get(oid).copied()
420 }
421
422 fn get(&self, oid: &ObjectId) -> Option<&CommitRow> {
423 self.index_of(oid).map(|i| &self.rows[i])
424 }
425
426 fn date(&self, oid: &ObjectId) -> i64 {
428 self.get(oid).map_or(0, |c| c.committer_date)
429 }
430
431 fn parents(
433 &self,
434 repo: &Repository,
435 oid: &ObjectId,
436 ) -> Result<Cow<'_, [ObjectId]>, PackBitmapWriteError> {
437 if let Some(row) = self.get(oid) {
438 return Ok(Cow::Borrowed(&row.parents));
439 }
440 let obj = read_object(repo, oid)?;
441 let commit =
442 parse_commit(&obj.data).map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
443 Ok(Cow::Owned(self.graft_parents(oid, commit.parents)))
444 }
445}
446
447fn read_object(repo: &Repository, oid: &ObjectId) -> Result<Object, PackBitmapWriteError> {
448 repo.odb()
449 .read(oid)
450 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))
451}
452
453fn pack_entry_kinds(
459 repo: &Repository,
460 order: &BitmapOrder,
461) -> Result<Vec<Option<ObjectKind>>, PackBitmapWriteError> {
462 let (Some(idx), Some(objects_dir)) = (order.pack_index(), repo.odb().files_objects_dir())
463 else {
464 return Ok(Vec::new());
465 };
466 let Some(reader) = SingleLocalPackReader::open_for_pack_index(objects_dir, Arc::clone(&idx))
467 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?
468 else {
469 return Ok(Vec::new());
470 };
471 let offsets = (0..order.object_count())
472 .map(|pos| {
473 let row = order.storage_row_for_bitmap_rank(pos).ok_or_else(|| {
474 PackBitmapWriteError::Object("bitmap position out of range".into())
475 })?;
476 idx.offset_at(row as usize)
477 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))
478 })
479 .collect::<Result<Vec<u64>, _>>()?;
480 reader
481 .object_kinds_at_sorted_offsets(&offsets)
482 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))
483}
484
485fn build_type_bitmaps_and_commits(
490 repo: &Repository,
491 order: &BitmapOrder,
492 shallow: HashSet<ObjectId>,
493) -> Result<(TypeBitmaps, CommitTable), PackBitmapWriteError> {
494 let mut commits = Vec::new();
495 let mut commit_bm = Bitmap::new();
496 let mut trees = Bitmap::new();
497 let mut blobs = Bitmap::new();
498 let mut tags = Bitmap::new();
499 let header_kinds = pack_entry_kinds(repo, order)?;
500 let n = order.object_count();
501 for pos in 0..n {
502 let oid = order
503 .oid_at(pos)
504 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
505 let p = pos as usize;
506 let kind = match header_kinds.get(p).copied().flatten() {
507 Some(kind) => kind,
508 None => {
509 repo.odb()
510 .read_info(&oid)
511 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?
512 .kind
513 }
514 };
515 match kind {
516 ObjectKind::Commit => {
517 commit_bm.set(p);
518 let obj = read_object(repo, &oid)?;
519 let c = parse_commit(&obj.data)
520 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
521 let committer_date = parse_signature_times(&c.committer)
522 .map(|t| t.unix_seconds)
523 .unwrap_or(0);
524 commits.push(CommitRow {
525 oid,
526 committer_date,
527 tree: c.tree,
528 parents: c.parents,
529 });
530 }
531 ObjectKind::Tree => trees.set(p),
532 ObjectKind::Blob => blobs.set(p),
533 ObjectKind::Tag => tags.set(p),
534 }
535 }
536 Ok((
537 TypeBitmaps {
538 commits: EwahBitmap::from_bitmap(&commit_bm),
539 trees: EwahBitmap::from_bitmap(&trees),
540 blobs: EwahBitmap::from_bitmap(&blobs),
541 tags: EwahBitmap::from_bitmap(&tags),
542 },
543 CommitTable::new(commits, shallow),
544 ))
545}
546
547fn commit_tips_from_roots(
559 repo: &Repository,
560 order: &BitmapOrder,
561 commits: &CommitTable,
562 roots: &[ObjectId],
563 strict: bool,
564) -> Result<Vec<ObjectId>, PackBitmapWriteError> {
565 const MAX_REPORTED_MISSING: usize = 32;
566 let mut tips = Vec::new();
567 let mut missing = Vec::new();
568 let mut seen_trees = HashSet::new();
569 for &root in roots {
570 let mut oid = root;
571 loop {
572 if strict && order.position_of(&oid).is_none() {
573 missing.push(oid);
574 break;
575 }
576 if commits.get(&oid).is_some() {
577 tips.push(oid);
578 break;
579 }
580 let obj = read_object(repo, &oid)?;
581 match obj.kind {
582 ObjectKind::Commit => {
583 tips.push(oid);
584 break;
585 }
586 ObjectKind::Tag => {
587 oid = parse_tag(&obj.data)
588 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?
589 .object;
590 }
591 ObjectKind::Tree => {
592 if strict {
593 collect_missing_in_tree(repo, order, oid, &mut seen_trees, &mut missing)?;
594 }
595 break;
596 }
597 ObjectKind::Blob => break,
598 }
599 }
600 if missing.len() >= MAX_REPORTED_MISSING {
601 break;
602 }
603 }
604 if !missing.is_empty() {
605 missing.truncate(MAX_REPORTED_MISSING);
606 return Err(PackBitmapWriteError::NotClosed { missing });
607 }
608 tips.sort_by_key(|o| o.to_hex());
609 tips.dedup();
610 Ok(tips)
611}
612
613fn collect_missing_in_tree(
615 repo: &Repository,
616 order: &BitmapOrder,
617 tree: ObjectId,
618 seen: &mut HashSet<ObjectId>,
619 missing: &mut Vec<ObjectId>,
620) -> Result<(), PackBitmapWriteError> {
621 let hash_len = repo.odb().hash_algo().len();
622 let mut stack = vec![tree];
623 while let Some(tree_oid) = stack.pop() {
624 if !seen.insert(tree_oid) {
625 continue;
626 }
627 let obj = read_object(repo, &tree_oid)?;
628 for entry in TreeRefIter::with_oid_len(&obj.data, hash_len) {
629 let entry = entry.map_err(|e| PackBitmapWriteError::Object(e.to_string()))?;
630 let kind = TreeChild::from_mode(entry.mode);
631 if kind == TreeChild::Gitlink {
632 continue;
633 }
634 if order.position_of(&entry.oid).is_none() {
635 missing.push(entry.oid);
636 } else if kind == TreeChild::Tree {
637 stack.push(entry.oid);
638 }
639 }
640 }
641 Ok(())
642}
643
644#[derive(Debug, Clone, Copy, PartialEq, Eq)]
646enum TreeChild {
647 Tree,
648 Gitlink,
649 Blob,
650}
651
652impl TreeChild {
653 const TYPE_MASK: u32 = 0o170000;
654 const TREE: u32 = 0o040000;
655 const GITLINK: u32 = 0o160000;
656
657 fn from_mode(mode: u32) -> Self {
658 match mode & Self::TYPE_MASK {
659 Self::TREE => Self::Tree,
660 Self::GITLINK => Self::Gitlink,
661 _ => Self::Blob,
662 }
663 }
664}
665
666fn tip_needs_flags(
667 repo: &Repository,
668 tips: &[ObjectId],
669 prefer_patterns: &[String],
670) -> Result<HashSet<ObjectId>, PackBitmapWriteError> {
671 let mut needs = HashSet::new();
672 if prefer_patterns.is_empty() {
673 needs.extend(tips.iter().copied());
674 return Ok(needs);
675 }
676 for (name, oid) in refs::list_refs_for_repository(repo, "refs/")
677 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))?
678 {
679 let peeled = peel_to_commit(repo, oid)?;
680 let Some(commit) = peeled else { continue };
681 if prefer_patterns
682 .iter()
683 .any(|pat| ref_matches_pattern(&name, pat))
684 {
685 needs.insert(commit);
686 }
687 }
688 needs.extend(tips.iter().copied());
689 Ok(needs)
690}
691
692fn ref_matches_pattern(ref_name: &str, pattern: &str) -> bool {
693 if pattern.is_empty() {
694 return false;
695 }
696 if ref_name == pattern {
697 return true;
698 }
699 if pattern.ends_with('/') {
700 ref_name.starts_with(pattern) || format!("{ref_name}/").starts_with(pattern)
701 } else {
702 ref_name == pattern
703 || ref_name
704 .strip_prefix("refs/")
705 .is_some_and(|r| r == pattern || r.starts_with(&format!("{pattern}/")))
706 }
707}
708
709fn peel_to_commit(
710 repo: &Repository,
711 oid: ObjectId,
712) -> Result<Option<ObjectId>, PackBitmapWriteError> {
713 repo.odb()
714 .peel_to_commit(oid)
715 .map_err(|e| PackBitmapWriteError::Object(e.to_string()))
716}
717
718fn pick_window_commit(window: &[CommitRow], needs_bitmap: &HashSet<ObjectId>) -> ObjectId {
719 if window.len() == 1 {
720 return window[0].oid;
721 }
722 let mut chosen = window[window.len() - 1].oid;
723 for cm in window {
724 if needs_bitmap.contains(&cm.oid) {
725 return cm.oid;
726 }
727 if cm.parents.len() > 1 {
728 chosen = cm.oid;
729 }
730 }
731 chosen
732}
733
734fn select_commits(
735 commits: &[CommitRow],
736 tips: &[ObjectId],
737 needs_bitmap: &HashSet<ObjectId>,
738) -> Vec<ObjectId> {
739 let mut selected: HashSet<ObjectId> = tips.iter().copied().collect();
740 let nr = commits.len();
741 let plan = HistorySamplePlan::default();
742 if nr <= plan.recent_dense_end {
743 selected.extend(commits.iter().map(|c| c.oid));
744 } else {
745 let mut rank = 0usize;
746 while rank < nr {
747 let gap = plan.gap_before_window(rank);
748 if rank + gap >= nr {
749 break;
750 }
751 let window_end = rank + gap;
752 let window = &commits[rank..=window_end];
753 selected.insert(pick_window_commit(window, needs_bitmap));
754 rank = window_end + 1;
755 }
756 }
757 let mut list: Vec<ObjectId> = selected.into_iter().collect();
758 list.sort_by_key(|o| o.to_hex());
759 list
760}
761
762struct TypeBitmaps {
763 commits: EwahBitmap,
764 trees: EwahBitmap,
765 blobs: EwahBitmap,
766 tags: EwahBitmap,
767}
768
769fn build_commit_bitmaps(
770 repo: &Repository,
771 order: &BitmapOrder,
772 commits: &CommitTable,
773 selected: &[ObjectId],
774 name_hashes: &mut [u32],
775 strict_closure: bool,
776) -> Result<HashMap<ObjectId, Bitmap>, PackBitmapWriteError> {
777 let mut topo = selected.to_vec();
778 topo.extend(first_parent_fork_points(commits, selected));
779 topo.sort_by_key(|a| commits.date(a));
780 let mut fill = TreeBitmapFill {
781 repo,
782 order,
783 hash_len: repo.odb().hash_algo().len(),
784 name_hashes,
785 strict_closure,
786 };
787 let mut computed: HashMap<ObjectId, Bitmap> = HashMap::new();
788 for commit in topo {
789 let mut bm = seed_from_computed_ancestors(repo, commits, commit, &computed)?;
790 fill_commit_bitmap(&mut fill, commits, &mut bm, commit, &computed)?;
791 computed.insert(commit, bm);
792 }
793 Ok(computed)
794}
795
796fn first_parent_fork_points(commits: &CommitTable, selected: &[ObjectId]) -> Vec<ObjectId> {
804 let mut visited = vec![false; commits.rows.len()];
805 let mut children = vec![0u32; commits.rows.len()];
806 let mut is_selected = vec![false; commits.rows.len()];
807 let starts: Vec<usize> = selected
808 .iter()
809 .filter_map(|c| commits.index_of(c))
810 .collect();
811 for &i in &starts {
812 is_selected[i] = true;
813 }
814 for &start in &starts {
815 if std::mem::replace(&mut visited[start], true) {
816 continue;
817 }
818 let mut cur = start;
819 while let Some(parent) = commits.rows[cur]
820 .parents
821 .first()
822 .and_then(|p| commits.index_of(p))
823 {
824 children[parent] += 1;
825 if std::mem::replace(&mut visited[parent], true) {
826 break;
827 }
828 cur = parent;
829 }
830 }
831 let mut forks: Vec<ObjectId> = (0..commits.rows.len())
832 .filter(|&i| children[i] >= 2 && !is_selected[i])
833 .map(|i| commits.rows[i].oid)
834 .collect();
835 forks.sort_by_key(|o| o.to_hex());
836 forks
837}
838
839fn seed_from_computed_ancestors(
840 repo: &Repository,
841 commits: &CommitTable,
842 commit: ObjectId,
843 computed: &HashMap<ObjectId, Bitmap>,
844) -> Result<Bitmap, PackBitmapWriteError> {
845 let mut bm = Bitmap::new();
846 for &parent in commits.parents(repo, &commit)?.iter() {
847 let mut walk = parent;
848 loop {
849 if let Some(ancestor_bm) = computed.get(&walk) {
850 bm.or_assign(ancestor_bm);
851 break;
852 }
853 match commits.parents(repo, &walk)?.first().copied() {
854 Some(next) => walk = next,
855 None => break,
856 }
857 }
858 }
859 Ok(bm)
860}
861
862fn hash_cache_row_count(order: &BitmapOrder) -> usize {
863 order
864 .pack_index()
865 .map_or(order.object_count() as usize, |idx| idx.len())
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 = pack_name_hash_child(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}