Skip to main content

grit_lib/pack_bitmap/
writer.rs

1//! Write Git pack reachability bitmap (`.bitmap`) sidecars for a single pack index.
2
3use 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/// Rank-based spacing for history sampling (newest commit = rank 0).
39///
40/// Calibrated against system `git repack -adb` on a full [git.git](https://github.com/git/git)
41/// clone (~62k commits, ~321 bitmap entries — denser near HEAD, sparser in ancient history).
42/// This plan uses rank tiers and linear stride growth, not Git's window-index function.
43#[derive(Debug, Clone, Copy)]
44struct HistorySamplePlan {
45    /// Ranks in `[0, recent_dense_end)` are visited with stride zero (every window picks HEAD of slice).
46    recent_dense_end: usize,
47    /// Ranks below this use [`Self::mid_stride`].
48    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/// Options controlling pack bitmap generation (caller-supplied; not read from the environment).
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct PackBitmapWriteOptions {
95    /// Write `BITMAP_OPT_FULL_DAG` (required for Git-compatible readers).
96    pub full_dag: bool,
97    /// Include the v1 name-hash tail section.
98    pub hash_cache: bool,
99    /// Include the commit lookup table extension.
100    pub lookup_table: bool,
101    /// Ref name patterns (`refs/heads/`, …) that prefer tip commits inside each selection window.
102    pub prefer_bitmap_tips: Vec<String>,
103    /// When true, every object reachable from ref tips must appear in the bitmap order.
104    pub verify_reachability_closure: bool,
105    /// When set, validate reachability against these roots instead of re-reading refs.
106    ///
107    /// Maintenance passes the same roots it used to build a repack pack so a concurrent ref
108    /// update cannot make bitmap validation fail spuriously mid-gc.
109    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/// Failure while writing a pack reachability bitmap.
126#[derive(Debug, Error, PartialEq, Eq)]
127#[non_exhaustive]
128pub enum PackBitmapWriteError {
129    /// An object reachable from the chosen tips is missing from the pack index.
130    #[error("pack is not closed under reachability from tips")]
131    NotClosed {
132        /// Object ids missing from the pack or unreachable from the validated ref roots.
133        missing: Vec<ObjectId>,
134    },
135    /// The repository does not use a files-backed object store.
136    #[error("bitmap write requires a files-backed object database")]
137    NotFilesBacked,
138    /// Pack index or sidecar path is invalid.
139    #[error("invalid pack index path: {0}")]
140    InvalidPackPath(String),
141    /// Unexpected I/O failure.
142    #[error("I/O error: {0}")]
143    Io(String),
144    /// Object database or pack data could not be read.
145    #[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    /// Missing object ids when [`Self::NotClosed`] ended the write.
155    #[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
164/// Writes `.bitmap` sidecars for multi-pack-index files (MIDX checksum in header).
165pub struct MidxBitmapWriter;
166
167impl MidxBitmapWriter {
168    /// Write a reachability bitmap for a MIDX layer that was just serialized to `midx_bytes`.
169    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    /// Write a reachability bitmap for the tip MIDX under `pack_dir`.
183    ///
184    /// The bitmap header checksum must match the MIDX file checksum (`midx_checksum`).
185    ///
186    /// # Errors
187    ///
188    /// Returns [`PackBitmapWriteError::NotClosed`] when objects reachable from ref
189    /// tips are missing from the MIDX pseudo-pack order.
190    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
212/// Writes `.bitmap` sidecars for pack indexes produced by all-into-one repacks.
213pub struct PackBitmapWriter;
214
215impl PackBitmapWriter {
216    /// Write a reachability bitmap for the pack identified by `pack_idx_path`.
217    ///
218    /// When no `.rev` sidecar exists, one is created first so object order matches Git.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`PackBitmapWriteError::NotClosed`] when the pack is not the full
223    /// reachability closure from the ref tips used for validation.
224    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
253/// Shared bitmap writer core (pack order or MIDX pseudo-pack order).
254///
255/// Runs inside the object database's pack read context: the writer reads every commit and
256/// tree, and the context also lets [`pack_entry_kinds`] map the pack directly.
257fn 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    // Shallow boundary commits have no parents for reachability, as in Git.
283    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
385/// Every commit in the bitmap order, parsed once so later phases never re-read commits.
386///
387/// Shallow boundary commits are recorded without parents.
388struct CommitTable {
389    /// Newest committer date first (stable on ties, so pack order breaks them).
390    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    /// `parents` of commit `oid`, or none when `oid` is a shallow boundary.
410    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    /// Committer date of `oid`, or `0` for commits outside the order.
427    fn date(&self, oid: &ObjectId) -> i64 {
428        self.get(oid).map_or(0, |c| c.committer_date)
429    }
430
431    /// Parents of `oid`, reading commits outside the order from the object database.
432    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
453/// Kind of each position of a single-pack `order`, read from pack entry headers.
454///
455/// Empty for MIDX orders or when the pack cannot be mapped directly; entries are `None` where
456/// the headers alone cannot tell (see
457/// [`SingleLocalPackReader::object_kinds_at_sorted_offsets`]).
458fn 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
485/// Type bitmaps for every position in `order`, plus the parsed commits.
486///
487/// Only commits are inflated; other objects' types come from pack entry headers where possible,
488/// and from [`crate::odb::Odb::read_info`] otherwise.
489fn 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
547/// Peel every root to the commit it names, sorted and deduplicated.
548///
549/// With `strict`, also checks that each root, every tag on its peel chain, and the full
550/// closure of roots that peel to a tree are in `order`. Closure under commits is checked by
551/// the strict bitmap walk instead: every tip is selected, and that walk fails on the first
552/// reachable object missing from the order.
553///
554/// # Errors
555///
556/// [`PackBitmapWriteError::NotClosed`] lists up to 32 missing objects;
557/// [`PackBitmapWriteError::Object`] reports unreadable or unparsable objects.
558fn 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
613/// Record objects reachable from `tree` (already known to be in `order`) that the order lacks.
614fn 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/// What a tree entry points at, decided by its mode alone (Git never stores the type).
645#[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
796/// Unselected commits where the first-parent chains of two or more selected commits meet.
797///
798/// A release branch tip and the next selected commit on the branch it forked from both reach
799/// the history below the fork point, and neither bitmap contains the other. Computing an
800/// (unwritten) bitmap at the fork lets both seed from it, so that history is walked once.
801/// Only first-parent edges are followed: side branches merged back are already covered by the
802/// selected commit below the merge.
803fn 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        // Every commit in the order is in the table; anything else is not a commit.
891        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
902/// Walks trees into a commit bitmap, recording each object's path name hash.
903struct 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    /// Bitmap position and storage row of `oid`; `None` when the order lacks it and the
913    /// closure is not strict.
914    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    /// Set `tree` (at `pos` / `row`) and everything below it that `bitmap` lacks.
931    ///
932    /// Blobs are classified by tree entry mode and never read. A subtree whose bit is already
933    /// set (from this walk or an ancestor seed) is skipped without reading it. A blob's name
934    /// hash is overwritten by every newly walked tree that names it, so the walk order (oldest
935    /// selected commit first) leaves the newest path; a tree keeps the path it was first set by.
936    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    /// For each selected commit (sorted by date ascending), optional index of xor base.
976    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); // flags
1077        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}