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_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/// 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}
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/// Failure while writing a pack reachability bitmap.
120#[derive(Debug, Error, PartialEq, Eq)]
121#[non_exhaustive]
122pub enum PackBitmapWriteError {
123    /// An object reachable from the chosen tips is missing from the pack index.
124    #[error("pack is not closed under reachability from tips")]
125    NotClosed {
126        /// Object ids missing from the pack or unreachable from the validated ref roots.
127        missing: Vec<ObjectId>,
128    },
129    /// The repository does not use a files-backed object store.
130    #[error("bitmap write requires a files-backed object database")]
131    NotFilesBacked,
132    /// Pack index or sidecar path is invalid.
133    #[error("invalid pack index path: {0}")]
134    InvalidPackPath(String),
135    /// Unexpected I/O failure.
136    #[error("I/O error: {0}")]
137    Io(String),
138    /// Object database or pack data could not be read.
139    #[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    /// Missing object ids when [`Self::NotClosed`] ended the write.
149    #[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
158/// Writes `.bitmap` sidecars for multi-pack-index files (MIDX checksum in header).
159pub struct MidxBitmapWriter;
160
161impl MidxBitmapWriter {
162    /// Write a reachability bitmap for a MIDX layer that was just serialized to `midx_bytes`.
163    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    /// Write a reachability bitmap for the tip MIDX under `pack_dir`.
177    ///
178    /// The bitmap header checksum must match the MIDX file checksum (`midx_checksum`).
179    ///
180    /// # Errors
181    ///
182    /// Returns [`PackBitmapWriteError::NotClosed`] when objects reachable from ref
183    /// tips are missing from the MIDX pseudo-pack order.
184    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
206/// Writes `.bitmap` sidecars for pack indexes produced by all-into-one repacks.
207pub struct PackBitmapWriter;
208
209impl PackBitmapWriter {
210    /// Write a reachability bitmap for the pack identified by `pack_idx_path`.
211    ///
212    /// When no `.rev` sidecar exists, one is created first so object order matches Git.
213    ///
214    /// # Errors
215    ///
216    /// Returns [`PackBitmapWriteError::NotClosed`] when the pack is not the full
217    /// reachability closure from the ref tips used for validation.
218    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
247/// Shared bitmap writer core (pack order or MIDX pseudo-pack order).
248///
249/// Runs inside the object database's pack read context: the writer reads every commit and
250/// tree, and the context also lets [`pack_entry_kinds`] map the pack directly.
251fn 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    // Shallow boundary commits have no parents for reachability, as in Git.
274    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
376/// Every commit in the bitmap order, parsed once so later phases never re-read commits.
377///
378/// Shallow boundary commits are recorded without parents.
379struct CommitTable {
380    /// Newest committer date first (stable on ties, so pack order breaks them).
381    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    /// `parents` of commit `oid`, or none when `oid` is a shallow boundary.
401    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    /// Committer date of `oid`, or `0` for commits outside the order.
418    fn date(&self, oid: &ObjectId) -> i64 {
419        self.get(oid).map_or(0, |c| c.committer_date)
420    }
421
422    /// Parents of `oid`, reading commits outside the order from the object database.
423    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
444/// Kind of each position of a single-pack `order`, read from pack entry headers.
445///
446/// Empty for MIDX orders or when the pack cannot be mapped directly; entries are `None` where
447/// the headers alone cannot tell (see
448/// [`SingleLocalPackReader::object_kinds_at_sorted_offsets`]).
449fn 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
476/// Type bitmaps for every position in `order`, plus the parsed commits.
477///
478/// Only commits are inflated; other objects' types come from pack entry headers where possible,
479/// and from [`crate::odb::Odb::read_info`] otherwise.
480fn 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
538/// Peel every root to the commit it names, sorted and deduplicated.
539///
540/// With `strict`, also checks that each root, every tag on its peel chain, and the full
541/// closure of roots that peel to a tree are in `order`. Closure under commits is checked by
542/// the strict bitmap walk instead: every tip is selected, and that walk fails on the first
543/// reachable object missing from the order.
544///
545/// # Errors
546///
547/// [`PackBitmapWriteError::NotClosed`] lists up to 32 missing objects;
548/// [`PackBitmapWriteError::Object`] reports unreadable or unparsable objects.
549fn 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
604/// Record objects reachable from `tree` (already known to be in `order`) that the order lacks.
605fn 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/// What a tree entry points at, decided by its mode alone (Git never stores the type).
636#[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
788/// Unselected commits where the first-parent chains of two or more selected commits meet.
789///
790/// A release branch tip and the next selected commit on the branch it forked from both reach
791/// the history below the fork point, and neither bitmap contains the other. Computing an
792/// (unwritten) bitmap at the fork lets both seed from it, so that history is walked once.
793/// Only first-parent edges are followed: side branches merged back are already covered by the
794/// selected commit below the merge.
795fn 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
860/// Name hash of the path `parent/name`, or of `name` alone for root tree entries.
861fn 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        // 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 = 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    /// 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
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}