Skip to main content

grit_lib/
pack_objects.rs

1//! Negotiation-driven pack-objects: enumerate wants minus haves, delta search, stream PACK v2.
2//!
3//! Holds object metadata (not payloads) during planning; reads object bytes from the
4//! [`Odb`] on demand when encoding.
5
6use crate::environment::RepositoryOptions;
7
8use std::collections::{HashMap, HashSet, VecDeque};
9use std::io::Write;
10use std::path::Path;
11
12use flate2::write::ZlibEncoder;
13use flate2::Compression;
14
15use crate::bitmap_walk::ReachableSet;
16use crate::delta_encode::{encode_prefix_extension_delta, DeltaIndex};
17use crate::error::{Error, Result};
18use crate::hash::HashingWriter;
19use crate::objects::{
20    empty_object_id_hash_set, oid_hash_map, oid_hash_set, oid_hash_set_from, parse_commit,
21    parse_tag, HashAlgo, ObjectId, ObjectInfo, ObjectKind, OidHashMap, OidHashSet, TreeRefIter,
22};
23use crate::odb::Odb;
24use crate::pack_object_select::{
25    append_included_tags, HaveBoundary, NameTrees, PackObjectSelection, TagCandidate,
26};
27use crate::parallel::{try_par_map_force, ParallelHashError};
28use crate::progress::CancelSignal;
29use crate::repo::Repository;
30use crate::rev_list::ObjectFilter;
31
32/// Options controlling pack-objects output (also used by [`build_pack`]).
33#[derive(Clone, Copy, Debug)]
34pub struct PackBuildOptions {
35    /// Build a thin pack: allow deltas whose base is reachable from the `haves`
36    /// but **not** emitted in the pack (`REF_DELTA` against a peer-held base).
37    pub thin: bool,
38    /// Emit delta-compressed objects instead of whole objects only.
39    pub delta: bool,
40    /// Candidate bases per object (Git `--window`). `0` disables new deltas.
41    pub window: usize,
42    /// Maximum delta chain depth (Git `--depth`). `0` stores whole objects.
43    pub max_depth: usize,
44    /// Prefer `OFS_DELTA` when the base precedes the target in the pack stream.
45    pub use_ofs_delta: bool,
46    /// Honor `pack.island` delta-island rules when selecting bases.
47    pub respect_islands: bool,
48    /// Reuse on-disk delta edges when both target and base are in this pack.
49    pub reuse_deltas: bool,
50    /// Reuse verbatim packed bytes for whole objects (CRC32-guarded v2 indexes).
51    pub reuse_objects: bool,
52    /// Use pack reachability bitmaps for object enumeration when present.
53    pub use_bitmaps: bool,
54    /// Soft cap, in bytes, on the payloads and delta indexes one delta-search
55    /// window holds (Git `--window-memory`; each search thread has a window).
56    /// `None` reads `pack.windowMemory`, which defaults to unlimited; `Some(0)`
57    /// is unlimited too.
58    pub window_memory: Option<u64>,
59}
60
61/// The delta-search window cap a served fetch uses when `pack.windowMemory`
62/// is not set: unlimited windows of large blobs would hold `window` of them,
63/// with their indexes, per search thread.
64pub const SERVE_WINDOW_MEMORY: u64 = 32 * 1024 * 1024;
65
66impl Default for PackBuildOptions {
67    fn default() -> Self {
68        Self {
69            thin: false,
70            delta: false,
71            window: 10,
72            max_depth: 50,
73            use_ofs_delta: true,
74            respect_islands: false,
75            reuse_deltas: true,
76            reuse_objects: true,
77            use_bitmaps: true,
78            window_memory: None,
79        }
80    }
81}
82
83impl PackBuildOptions {
84    /// `pack.window` and `pack.depth` from config, or Git defaults when unset.
85    #[must_use]
86    pub fn window_and_depth_from_config(cfg: Option<&crate::config::ConfigSet>) -> (usize, usize) {
87        cfg.map(|c| (c.pack_object_window(), c.pack_object_depth()))
88            .unwrap_or((10, crate::pack::DEFAULT_PACK_DEPTH))
89    }
90
91    /// Production pack settings for a local full copy (clone/fetch from disk).
92    #[must_use]
93    pub fn for_local_copy(cfg: Option<&crate::config::ConfigSet>) -> Self {
94        let (window, max_depth) = Self::window_and_depth_from_config(cfg);
95        Self {
96            thin: false,
97            delta: true,
98            window,
99            max_depth,
100            use_ofs_delta: true,
101            reuse_deltas: max_depth > 0,
102            reuse_objects: true,
103            use_bitmaps: true,
104            ..Default::default()
105        }
106    }
107
108    /// Copy stored pack entries (whole objects and deltas) without a new delta search
109    /// (`window` 0), for rebuilding a pack from objects that are already packed.
110    #[must_use]
111    pub fn for_repack_rewrite(cfg: Option<&crate::config::ConfigSet>) -> Self {
112        let max_depth = Self::window_and_depth_from_config(cfg).1;
113        Self {
114            thin: false,
115            delta: true,
116            window: 0,
117            max_depth,
118            use_ofs_delta: true,
119            reuse_deltas: max_depth > 0,
120            reuse_objects: true,
121            use_bitmaps: true,
122            ..Default::default()
123        }
124    }
125
126    /// Pack settings for answering a fetch (upload-pack).
127    ///
128    /// Window and depth follow `pack.window` / `pack.depth`. Whole objects and deltas
129    /// already in the repository's packs are copied as they are, and the delta search
130    /// only covers blobs that are not already stored as a delta, as Git does when serving.
131    /// Like Git, `pack.depth = 0` only disables new deltas: existing on-disk deltas are
132    /// still sent.
133    ///
134    /// # Parameters
135    /// - `cfg` — repository config, or `None` for Git's defaults.
136    /// - `thin` — build a thin pack; pass `true` only when the client negotiated
137    ///   `thin-pack` *and* holds objects (sent `have`s), since a thin pack against
138    ///   nothing gains nothing.
139    /// - `use_ofs_delta` — whether the client negotiated `ofs-delta`.
140    #[must_use]
141    pub fn for_serve(
142        cfg: Option<&crate::config::ConfigSet>,
143        thin: bool,
144        use_ofs_delta: bool,
145    ) -> Self {
146        let (window, max_depth) = Self::window_and_depth_from_config(cfg);
147        let window_memory = cfg
148            .map(crate::config::ConfigSet::pack_window_memory_bytes)
149            .filter(|&bytes| bytes > 0)
150            .unwrap_or(SERVE_WINDOW_MEMORY);
151        Self {
152            thin,
153            delta: true,
154            window,
155            max_depth,
156            use_ofs_delta,
157            reuse_deltas: true,
158            reuse_objects: true,
159            use_bitmaps: true,
160            window_memory: Some(window_memory),
161            ..Default::default()
162        }
163    }
164
165    /// Production pack settings for an incremental in-process local push.
166    ///
167    /// Uses a thin pack when the remote already has objects to delta against.
168    /// Initial pushes into an empty bare repo should use [`Self::for_local_copy`].
169    #[must_use]
170    pub fn for_local_push(cfg: Option<&crate::config::ConfigSet>) -> Self {
171        let (window, max_depth) = Self::window_and_depth_from_config(cfg);
172        Self {
173            thin: true,
174            delta: true,
175            window,
176            max_depth,
177            use_ofs_delta: true,
178            reuse_deltas: true,
179            reuse_objects: true,
180            use_bitmaps: true,
181            ..Default::default()
182        }
183    }
184}
185
186/// Extended options for [`PackObjects`].
187#[derive(Clone, Copy, Debug, Default)]
188pub struct PackObjectsOptions {
189    /// Core pack-objects behaviour flags.
190    pub build: PackBuildOptions,
191    /// Override parallel delta-search workers (`None` = resolve [`crate::parallel::ParallelOp::PackDeltaSearch`] / `pack.threads`).
192    pub search_threads: Option<usize>,
193    /// Override parallel pack-write workers (`None` = resolve [`crate::parallel::ParallelOp::PackWrite`] / `pack.threads` from the repo config attached to the [`Odb`]).
194    pub write_threads: Option<usize>,
195}
196
197impl From<PackBuildOptions> for PackObjectsOptions {
198    fn from(build: PackBuildOptions) -> Self {
199        Self {
200            build,
201            search_threads: None,
202            write_threads: None,
203        }
204    }
205}
206
207/// Counters reported after a successful pack write.
208#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
209pub struct PackStats {
210    /// Number of objects in the pack header.
211    pub object_count: u32,
212    /// Total bytes written (including header and trailing hash).
213    pub pack_bytes: u64,
214    /// Objects written as `OFS_DELTA` or `REF_DELTA`.
215    pub delta_count: u32,
216    /// Pack body bytes copied verbatim from an existing on-disk pack (reuse path).
217    pub verbatim_reuse_bytes: u64,
218    /// Objects emitted via verbatim pack reuse (whole or delta copy/rewrite).
219    pub verbatim_reuse_objects: u32,
220    /// Delta entries whose OFS base distance was rewritten for the output pack.
221    pub pack_ofs_fixup_objects: u32,
222    /// OFS deltas converted to REF deltas for thin packs.
223    pub pack_ref_from_ofs_objects: u32,
224}
225
226/// Progress hook for long pack builds.
227#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228pub enum PackProgress {
229    /// Enumeration finished (`total` objects selected).
230    Enumerated {
231        /// Number of objects selected for packing.
232        total: usize,
233    },
234    /// Delta planning finished.
235    PlannedDeltas,
236    /// An object was written (`written` of `total`).
237    WroteObject {
238        /// Objects written so far.
239        written: usize,
240        /// Total objects to write in this pack.
241        total: usize,
242    },
243}
244
245/// Streaming pack-objects builder over an [`Odb`].
246pub struct PackObjects<'a> {
247    odb: &'a Odb,
248    source_odb: &'a Odb,
249    opts: PackObjectsOptions,
250    wants: Vec<ObjectId>,
251    haves: Vec<ObjectId>,
252    have_shallow: OidHashSet,
253    source_shallow: OidHashSet,
254    progress: Option<Box<dyn FnMut(PackProgress)>>,
255}
256
257impl<'a> PackObjects<'a> {
258    /// Create a builder reading objects from `odb`.
259    #[must_use]
260    pub fn new(odb: &'a Odb, opts: PackObjectsOptions) -> Self {
261        Self {
262            odb,
263            source_odb: odb,
264            opts,
265            wants: Vec::new(),
266            haves: Vec::new(),
267            have_shallow: oid_hash_set(),
268            source_shallow: oid_hash_set(),
269            progress: None,
270        }
271    }
272
273    /// Read object payloads from a different ODB (local fetch: remote source, local haves).
274    #[must_use]
275    pub fn with_source_odb(mut self, source: &'a Odb) -> Self {
276        self.source_odb = source;
277        self
278    }
279
280    /// Objects that must be included in the pack.
281    #[must_use]
282    pub fn wants(mut self, wants: &[ObjectId]) -> Self {
283        self.wants.extend_from_slice(wants);
284        self
285    }
286
287    /// Objects already held by the peer (negotiation boundary).
288    #[must_use]
289    pub fn haves(mut self, haves: &[ObjectId]) -> Self {
290        self.haves.extend_from_slice(haves);
291        self
292    }
293
294    /// Shallow grafts on the have side (local fetch).
295    #[must_use]
296    pub fn have_shallow_grafts(mut self, grafts: &HashSet<ObjectId>) -> Self {
297        self.have_shallow = grafts.iter().copied().collect();
298        self
299    }
300
301    /// Shallow grafts on the source side (do not walk past on source).
302    #[must_use]
303    pub fn source_shallow_grafts(mut self, grafts: &HashSet<ObjectId>) -> Self {
304        self.source_shallow = grafts.iter().copied().collect();
305        self
306    }
307
308    /// Optional progress callback.
309    #[must_use]
310    pub fn progress<F>(mut self, f: F) -> Self
311    where
312        F: FnMut(PackProgress) + 'static,
313    {
314        self.progress = Some(Box::new(f));
315        self
316    }
317
318    fn fire(&mut self, ev: PackProgress) {
319        if let Some(ref mut f) = self.progress {
320            f(ev);
321        }
322    }
323
324    /// Write a PACK v2 stream to `w` and return statistics.
325    ///
326    /// # Errors
327    ///
328    /// Missing wants, corrupt objects, or I/O failures while writing.
329    pub fn write_to(self, w: &mut dyn Write) -> Result<PackStats> {
330        self.write_to_with_cancel(w, &CancelSignal::never())
331    }
332
333    /// [`Self::write_to`], stopping with [`Error::Cancelled`] once `stop` fires.
334    pub(crate) fn write_to_with_cancel(
335        mut self,
336        w: &mut dyn Write,
337        stop: &CancelSignal<'_>,
338    ) -> Result<PackStats> {
339        let opts = self.opts.build;
340        let have_shallow: HashSet<ObjectId> = self.have_shallow.iter().copied().collect();
341        let source_shallow: HashSet<ObjectId> = self.source_shallow.iter().copied().collect();
342        let peer_odb = (!std::ptr::eq(self.source_odb, self.odb)).then_some(self.odb);
343        let selection = crate::pack_object_select::enumerate_pack_objects_naming(
344            self.source_odb,
345            &self.wants,
346            &self.haves,
347            &crate::pack_object_select::PackEnumerateOptions {
348                filter: None,
349                shallow_grafts: &source_shallow,
350                have_shallow_grafts: Some(&have_shallow),
351                use_bitmaps: opts.use_bitmaps,
352                exclude_objects: None,
353                peer_odb,
354                have_boundary: HaveBoundary::CommitClosure,
355            },
356            NameTrees::for_delta_search(&opts),
357            false,
358            stop,
359        )?;
360        let send = selection.send;
361        self.fire(PackProgress::Enumerated { total: send.len() });
362        if !opts.delta {
363            let bytes = serialize_pack(self.source_odb, &send, &opts)?;
364            stop.poll()?;
365            w.write_all(&bytes).map_err(Error::Io)?;
366            let count = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
367            return Ok(PackStats {
368                object_count: count,
369                pack_bytes: bytes.len() as u64,
370                ..Default::default()
371            });
372        }
373        let plan = plan_deltas(
374            self.source_odb,
375            &send,
376            &selection.have_closure,
377            selection.peer_held.as_deref(),
378            &opts,
379            &selection.name_hashes,
380            self.opts.search_threads,
381            stop,
382        )?;
383        self.fire(PackProgress::PlannedDeltas);
384        crate::pack_delta::write_pack_with_deltas(
385            self.source_odb,
386            &plan.entries,
387            &opts,
388            self.opts.write_threads,
389            w,
390            None,
391            stop,
392            None,
393        )
394    }
395}
396
397/// Compute the full object closure reachable from `roots`, stopping descent into
398/// any object already present in `stop`.
399pub fn reachable_closure(
400    odb: &Odb,
401    roots: &[ObjectId],
402    stop: &HashSet<ObjectId>,
403    skip_missing: bool,
404    shallow_grafts: &HashSet<ObjectId>,
405) -> Result<HashSet<ObjectId>> {
406    let stop = oid_hash_set_from(stop);
407    let shallow_grafts = oid_hash_set_from(shallow_grafts);
408    let mut seen = oid_hash_set();
409    let order = collect_reachable_excluding(odb, roots, &stop, skip_missing, &shallow_grafts)?;
410    for oid in order {
411        seen.insert(oid);
412    }
413    Ok(seen.into_iter().collect())
414}
415
416/// Build a v2 packfile containing exactly the objects reachable from `wants`
417/// but **not** reachable from `haves`, de-duplicated.
418pub fn build_pack(
419    odb: &Odb,
420    wants: &[ObjectId],
421    haves: &[ObjectId],
422    opts: &PackBuildOptions,
423) -> Result<Vec<u8>> {
424    build_pack_with_shallow_filter_and_boundary(
425        odb,
426        wants,
427        haves,
428        empty_object_id_hash_set(),
429        empty_object_id_hash_set(),
430        None,
431        opts,
432        HaveBoundary::CommitClosure,
433    )
434}
435
436/// Like [`build_pack`], but uses [`HaveBoundary::TipsOnly`] for send-pack pushes.
437///
438/// Negotiated have tips bound the want-side walk lazily instead of pre-walking
439/// their full commit closure before enumeration. Collects
440/// [`write_pack_for_push`] into memory; pushes stream with that instead.
441///
442/// # Errors
443///
444/// Same as [`build_pack`]: missing wants, corrupt objects, or pack write failures.
445pub fn build_pack_for_push(
446    odb: &Odb,
447    wants: &[ObjectId],
448    haves: &[ObjectId],
449    opts: &PackBuildOptions,
450) -> Result<Vec<u8>> {
451    let mut pack = Vec::new();
452    write_pack_for_push(odb, wants, haves, opts, &mut pack)?;
453    Ok(pack)
454}
455
456/// Stream the pack a push sends to `w`: the objects reachable from `wants` but not
457/// from `haves`, bounded by the have tips as in [`build_pack_for_push`].
458///
459/// Memory stays bounded by the object list rather than the pack size: on-disk
460/// deltas and whole objects are copied from the source packs batch by batch, and
461/// every phase shares one mapping of each source pack.
462///
463/// # Parameters
464///
465/// - `odb`: the object database the objects are read from.
466/// - `wants`: the new tips the remote should end up with.
467/// - `haves`: objects the remote already holds (its tips and `.have`s), all present in `odb`.
468/// - `opts`: delta, thin-pack and offset-delta settings for the remote.
469/// - `w`: where the PACK v2 stream, trailer included, is written.
470///
471/// # Returns
472///
473/// Object and byte counts of the pack written.
474///
475/// # Errors
476///
477/// Missing wants, corrupt objects, or [`Error::Io`] when writing to `w` fails.
478pub fn write_pack_for_push(
479    odb: &Odb,
480    wants: &[ObjectId],
481    haves: &[ObjectId],
482    opts: &PackBuildOptions,
483    w: &mut dyn Write,
484) -> Result<PackStats> {
485    write_pack_for_push_with_cancel(odb, wants, haves, opts, w, &CancelSignal::never())
486}
487
488/// [`write_pack_for_push`], stopping with [`Error::Cancelled`] once `stop` fires.
489pub(crate) fn write_pack_for_push_with_cancel(
490    odb: &Odb,
491    wants: &[ObjectId],
492    haves: &[ObjectId],
493    opts: &PackBuildOptions,
494    w: &mut dyn Write,
495    stop: &CancelSignal<'_>,
496) -> Result<PackStats> {
497    // One pack read context for the walk, the reuse scan and the write, so the
498    // source packs are mapped and indexed once rather than once per phase.
499    odb.with_pack_read_context(|| {
500        write_pack_with_shallow_and_filter(
501            odb,
502            wants,
503            haves,
504            empty_object_id_hash_set(),
505            empty_object_id_hash_set(),
506            None,
507            opts,
508            HaveBoundary::TipsOnly,
509            &[],
510            w,
511            stop,
512        )
513    })
514}
515
516/// Build a pack for upload-pack with optional shallow grafts and object filter.
517///
518/// When `filter` is set, objects are selected the way `pack-objects --filter` does:
519/// `wants` and the objects their tags point at are always sent.
520pub fn build_pack_with_shallow_and_filter(
521    odb: &Odb,
522    wants: &[ObjectId],
523    haves: &[ObjectId],
524    have_shallow: &HashSet<ObjectId>,
525    source_shallow: &HashSet<ObjectId>,
526    filter: Option<&ObjectFilter>,
527    opts: &PackBuildOptions,
528) -> Result<Vec<u8>> {
529    build_pack_with_shallow_filter_and_boundary(
530        odb,
531        wants,
532        haves,
533        have_shallow,
534        source_shallow,
535        filter,
536        opts,
537        HaveBoundary::CommitClosure,
538    )
539}
540
541#[allow(clippy::too_many_arguments)]
542fn build_pack_with_shallow_filter_and_boundary(
543    odb: &Odb,
544    wants: &[ObjectId],
545    haves: &[ObjectId],
546    have_shallow: &HashSet<ObjectId>,
547    source_shallow: &HashSet<ObjectId>,
548    filter: Option<&ObjectFilter>,
549    opts: &PackBuildOptions,
550    have_boundary: HaveBoundary,
551) -> Result<Vec<u8>> {
552    let mut pack = Vec::new();
553    write_pack_with_shallow_and_filter(
554        odb,
555        wants,
556        haves,
557        have_shallow,
558        source_shallow,
559        filter,
560        opts,
561        have_boundary,
562        &[],
563        &mut pack,
564        &CancelSignal::never(),
565    )?;
566    Ok(pack)
567}
568
569/// Streaming form of [`build_pack_with_shallow_and_filter`]; `include_tags` are added
570/// when the objects they peel to are selected.
571#[allow(clippy::too_many_arguments)]
572fn write_pack_with_shallow_and_filter(
573    odb: &Odb,
574    wants: &[ObjectId],
575    haves: &[ObjectId],
576    have_shallow: &HashSet<ObjectId>,
577    source_shallow: &HashSet<ObjectId>,
578    filter: Option<&ObjectFilter>,
579    opts: &PackBuildOptions,
580    have_boundary: HaveBoundary,
581    include_tags: &[TagCandidate],
582    w: &mut dyn Write,
583    stop: &CancelSignal<'_>,
584) -> Result<PackStats> {
585    let mut selection = crate::pack_object_select::enumerate_pack_objects_naming(
586        odb,
587        wants,
588        haves,
589        &crate::pack_object_select::PackEnumerateOptions {
590            filter,
591            shallow_grafts: source_shallow,
592            have_shallow_grafts: Some(have_shallow),
593            use_bitmaps: opts.use_bitmaps,
594            exclude_objects: None,
595            peer_odb: None,
596            have_boundary,
597        },
598        NameTrees::for_delta_search(opts),
599        false,
600        stop,
601    )?;
602    append_included_tags(odb, &mut selection.send, include_tags)?;
603    write_selected_pack(odb, &selection, opts, w, stop)
604}
605
606/// Write a pack holding exactly `selection.send`, searching for deltas when `opts`
607/// asks for them (thin bases from `selection.have_closure`).
608fn write_selected_pack(
609    odb: &Odb,
610    selection: &PackObjectSelection,
611    opts: &PackBuildOptions,
612    w: &mut dyn Write,
613    stop: &CancelSignal<'_>,
614) -> Result<PackStats> {
615    if !opts.delta {
616        let bytes = serialize_pack(odb, &selection.send, opts)?;
617        stop.poll()?;
618        w.write_all(&bytes).map_err(Error::Io)?;
619        return Ok(PackStats {
620            object_count: u32::try_from(selection.send.len()).unwrap_or(u32::MAX),
621            pack_bytes: bytes.len() as u64,
622            ..PackStats::default()
623        });
624    }
625    let plan = plan_deltas(
626        odb,
627        &selection.send,
628        &selection.have_closure,
629        selection.peer_held.as_deref(),
630        opts,
631        &selection.name_hashes,
632        None,
633        stop,
634    )?;
635    crate::pack_delta::write_pack_with_deltas(odb, &plan.entries, opts, None, w, None, stop, None)
636}
637
638/// Stream a pack for upload-pack: pack reuse when bitmaps allow, otherwise pack-objects.
639///
640/// The whole build runs in the odb's pack read context, so planning and the pack-write
641/// workers share one set of pack caches. Filtered (partial clone) packs stream too.
642///
643/// # Errors
644///
645/// Missing wants, corrupt objects, or I/O failures while writing to `w`.
646#[allow(clippy::too_many_arguments)]
647pub fn write_upload_pack_stream(
648    repo: &Repository,
649    wants: &[ObjectId],
650    haves: &[ObjectId],
651    source_shallow: &HashSet<ObjectId>,
652    have_shallow: &HashSet<ObjectId>,
653    filter: Option<&ObjectFilter>,
654    opts: &PackBuildOptions,
655    progress: &mut dyn FnMut(PackProgress),
656    w: &mut dyn Write,
657) -> Result<PackStats> {
658    write_upload_pack_stream_with_cancel(
659        repo,
660        wants,
661        haves,
662        source_shallow,
663        have_shallow,
664        filter,
665        &[],
666        opts,
667        progress,
668        w,
669        &CancelSignal::never(),
670    )
671}
672
673/// [`write_upload_pack_stream`], stopping object enumeration when `stop` fires, that
674/// also packs each of `include_tags` whose peeled object is among the objects sent
675/// (`include-tag`), on the walk and reuse paths alike.
676#[allow(clippy::too_many_arguments)]
677pub(crate) fn write_upload_pack_stream_with_cancel(
678    repo: &Repository,
679    wants: &[ObjectId],
680    haves: &[ObjectId],
681    source_shallow: &HashSet<ObjectId>,
682    have_shallow: &HashSet<ObjectId>,
683    filter: Option<&ObjectFilter>,
684    include_tags: &[TagCandidate],
685    opts: &PackBuildOptions,
686    progress: &mut dyn FnMut(PackProgress),
687    w: &mut dyn Write,
688    stop: &CancelSignal<'_>,
689) -> Result<PackStats> {
690    repo.odb().with_pack_read_context(|| {
691        write_upload_pack_stream_in_context(
692            repo,
693            wants,
694            haves,
695            source_shallow,
696            have_shallow,
697            filter,
698            include_tags,
699            opts,
700            progress,
701            w,
702            stop,
703        )
704    })
705}
706
707#[allow(clippy::too_many_arguments)]
708fn write_upload_pack_stream_in_context(
709    repo: &Repository,
710    wants: &[ObjectId],
711    haves: &[ObjectId],
712    source_shallow: &HashSet<ObjectId>,
713    have_shallow: &HashSet<ObjectId>,
714    filter: Option<&ObjectFilter>,
715    include_tags: &[TagCandidate],
716    opts: &PackBuildOptions,
717    progress: &mut dyn FnMut(PackProgress),
718    w: &mut dyn Write,
719    stop: &CancelSignal<'_>,
720) -> Result<PackStats> {
721    if opts.use_bitmaps && filter.is_none() {
722        if let Some(stats) = crate::pack_reuse::try_write_streaming_pack_including_tags(
723            repo,
724            wants,
725            haves,
726            source_shallow,
727            have_shallow,
728            filter,
729            include_tags,
730            opts,
731            progress,
732            w,
733            stop,
734        )? {
735            return Ok(stats);
736        }
737    }
738    // The client's history only has to be painted down to where the want walk meets
739    // it, not pre-walked to its roots.
740    write_pack_with_shallow_and_filter(
741        repo.odb(),
742        wants,
743        haves,
744        have_shallow,
745        source_shallow,
746        filter,
747        opts,
748        HaveBoundary::TipsOnly,
749        include_tags,
750        w,
751        stop,
752    )
753}
754
755type PackIndexRows = Vec<(ObjectId, u64, u32)>;
756
757/// Objects selected for an upload-pack response from a repository with promisor
758/// remotes, with the blobs it does not store set aside.
759///
760/// Always uses the object walk: bitmaps and verbatim pack reuse cannot tell that a
761/// blob is missing. See [`write_upload_pack_selection`].
762#[allow(clippy::too_many_arguments)]
763pub(crate) fn select_upload_pack_objects_probing(
764    repo: &Repository,
765    wants: &[ObjectId],
766    haves: &[ObjectId],
767    source_shallow: &HashSet<ObjectId>,
768    have_shallow: &HashSet<ObjectId>,
769    filter: Option<&ObjectFilter>,
770    opts: &PackBuildOptions,
771) -> Result<(
772    PackObjectSelection,
773    Vec<crate::pack_object_select::MissingBlob>,
774)> {
775    repo.odb().with_pack_read_context(|| {
776        crate::pack_object_select::enumerate_pack_objects_probing(
777            repo.odb(),
778            wants,
779            haves,
780            &crate::pack_object_select::PackEnumerateOptions {
781                filter,
782                shallow_grafts: source_shallow,
783                have_shallow_grafts: Some(have_shallow),
784                use_bitmaps: false,
785                exclude_objects: None,
786                peer_odb: None,
787                have_boundary: HaveBoundary::TipsOnly,
788            },
789            NameTrees::for_delta_search(opts),
790            &CancelSignal::never(),
791        )
792    })
793}
794
795/// Stream the pack for a selection made by [`select_upload_pack_objects_probing`]
796/// (after its missing blobs were omitted or fetched).
797pub(crate) fn write_upload_pack_selection(
798    repo: &Repository,
799    selection: &PackObjectSelection,
800    opts: &PackBuildOptions,
801    w: &mut dyn Write,
802    stop: &CancelSignal<'_>,
803) -> Result<PackStats> {
804    repo.odb()
805        .with_pack_read_context(|| write_selected_pack(repo.odb(), selection, opts, w, stop))
806}
807
808fn write_pack_header(w: &mut dyn Write, count: u32) -> Result<()> {
809    w.write_all(b"PACK").map_err(Error::Io)?;
810    w.write_all(&2u32.to_be_bytes()).map_err(Error::Io)?;
811    w.write_all(&count.to_be_bytes()).map_err(Error::Io)?;
812    Ok(())
813}
814
815/// Stream a delta pack from a plan, optionally applying bitmap verbatim reuse.
816pub(crate) fn write_delta_pack_stream(
817    odb: &Odb,
818    plan: &DeltaPlan,
819    opts: &PackBuildOptions,
820    w: &mut dyn Write,
821    reuse: Option<&mut crate::pack_reuse::StreamingReuse>,
822) -> Result<PackStats> {
823    let algo = odb.hash_algo();
824    let mut out = HashingWriter::new(algo, w);
825    let reuse_stats = if let Some(reuse) = reuse {
826        let (_, body_bytes) = write_delta_plan_stream_body(&mut out, odb, plan, opts, Some(reuse))?;
827        out.finish().map_err(Error::Io)?;
828        let pack_bytes = 12u64 + body_bytes + algo.len() as u64;
829        let stats = reuse.take_stats();
830        (pack_bytes, stats)
831    } else {
832        let (_, body_bytes) = write_delta_plan_stream_body(&mut out, odb, plan, opts, None)?;
833        out.finish().map_err(Error::Io)?;
834        (
835            12u64 + body_bytes + algo.len() as u64,
836            crate::pack_reuse::ReuseWriteStats::default(),
837        )
838    };
839    let delta_count = plan.entries.iter().filter(|e| e.base.is_some()).count() as u32;
840    Ok(PackStats {
841        object_count: u32::try_from(plan.entries.len())
842            .map_err(|_| Error::CorruptObject("pack object count exceeds u32".to_owned()))?,
843        pack_bytes: reuse_stats.0,
844        delta_count,
845        verbatim_reuse_bytes: reuse_stats.1.verbatim_reuse_bytes,
846        verbatim_reuse_objects: reuse_stats.1.verbatim_reuse_objects,
847        pack_ofs_fixup_objects: reuse_stats.1.pack_ofs_fixup_objects,
848        pack_ref_from_ofs_objects: reuse_stats.1.pack_ref_from_ofs_objects,
849    })
850}
851
852fn write_delta_plan_stream_body(
853    w: &mut dyn Write,
854    odb: &Odb,
855    plan: &DeltaPlan,
856    opts: &PackBuildOptions,
857    reuse: Option<&mut crate::pack_reuse::StreamingReuse>,
858) -> Result<(PackIndexRows, u64)> {
859    let count = u32::try_from(plan.entries.len())
860        .map_err(|_| Error::CorruptObject("pack object count exceeds u32".to_owned()))?;
861    write_pack_header(w, count)?;
862    let mut index_rows = Vec::with_capacity(plan.entries.len());
863    let body_bytes = write_planned_entries(w, odb, plan, opts, 12, reuse, &mut index_rows)?;
864    Ok((index_rows, body_bytes))
865}
866
867pub(crate) const PACK_STREAM_COPY_CHUNK: usize = 64 * 1024;
868
869pub(crate) struct CrcWriter<'a, W: Write + ?Sized> {
870    inner: &'a mut W,
871    crc: crc32fast::Hasher,
872}
873
874impl<'a, W: Write + ?Sized> CrcWriter<'a, W> {
875    pub(crate) fn new(inner: &'a mut W, prefix: &[u8]) -> Self {
876        let mut crc = crc32fast::Hasher::new();
877        crc.update(prefix);
878        Self { inner, crc }
879    }
880
881    pub(crate) fn finalize(self) -> u32 {
882        self.crc.finalize()
883    }
884}
885
886impl<W: Write + ?Sized> Write for CrcWriter<'_, W> {
887    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
888        let n = self.inner.write(buf)?;
889        self.crc.update(&buf[..n]);
890        Ok(n)
891    }
892
893    fn flush(&mut self) -> std::io::Result<()> {
894        self.inner.flush()
895    }
896}
897
898struct ByteCounter<'a, W: Write + ?Sized> {
899    inner: &'a mut W,
900    n: u64,
901}
902
903impl<W: Write + ?Sized> Write for ByteCounter<'_, W> {
904    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
905        let n = self.inner.write(buf)?;
906        self.n += n as u64;
907        Ok(n)
908    }
909
910    fn flush(&mut self) -> std::io::Result<()> {
911        self.inner.flush()
912    }
913}
914
915pub(crate) fn write_all_chunked(w: &mut dyn Write, data: &[u8]) -> Result<()> {
916    for chunk in data.chunks(PACK_STREAM_COPY_CHUNK) {
917        w.write_all(chunk).map_err(Error::Io)?;
918    }
919    Ok(())
920}
921
922fn write_planned_entries(
923    w: &mut dyn Write,
924    odb: &Odb,
925    plan: &DeltaPlan,
926    opts: &PackBuildOptions,
927    pack_body_start: u64,
928    mut reuse: Option<&mut crate::pack_reuse::StreamingReuse>,
929    index_rows: &mut Vec<(ObjectId, u64, u32)>,
930) -> Result<u64> {
931    let algo = odb.hash_algo();
932    let compression = odb.pack_compression()?;
933    let mut oid_to_offset: OidHashMap<u64> = oid_hash_map();
934    let mut body_written = 0u64;
935
936    for entry in &plan.entries {
937        let start = pack_body_start + body_written;
938        let mut chunk = Vec::new();
939        if entry.base.is_none() {
940            let mut crc_w = CrcWriter {
941                inner: w,
942                crc: crc32fast::Hasher::new(),
943            };
944            let wrote = {
945                let mut counter = ByteCounter {
946                    inner: &mut crc_w,
947                    n: 0,
948                };
949                if let Some(reuse) = reuse.as_mut() {
950                    if let Some(n) = reuse.try_write_whole(&mut counter, &entry.oid, opts)? {
951                        n
952                    } else {
953                        write_whole_pack_object_to_odb(
954                            &mut counter,
955                            odb,
956                            entry.oid,
957                            entry.kind,
958                            opts,
959                        )?
960                    }
961                } else {
962                    write_whole_pack_object_to_odb(&mut counter, odb, entry.oid, entry.kind, opts)?
963                }
964            };
965            let crc = crc_w.finalize();
966            index_rows.push((entry.oid, start, crc));
967            body_written += wrote;
968            oid_to_offset.insert(entry.oid, start);
969            continue;
970        }
971        let Some(base_oid) = entry.base else {
972            continue;
973        };
974        let in_pack_offset = oid_to_offset.get(&base_oid).copied();
975        if let Some((delta_size, pack_bytes, zlib_start, zlib_end)) = &entry.reused_delta_zlib {
976            let delta_len = usize::try_from(*delta_size)
977                .map_err(|_| Error::CorruptObject("reused delta size exceeds usize".to_owned()))?;
978            if let Some(base_off) = in_pack_offset.filter(|_| opts.use_ofs_delta) {
979                let dist = start.checked_sub(base_off).ok_or_else(|| {
980                    Error::CorruptObject("ofs-delta distance underflow".to_owned())
981                })?;
982                encode_pack_object_header(&mut chunk, 6, delta_len);
983                encode_ofs_delta_distance(&mut chunk, dist);
984                if let Some(reuse) = reuse.as_mut() {
985                    reuse.record_ofs_fixup();
986                }
987            } else {
988                encode_pack_object_header(&mut chunk, 7, delta_len);
989                chunk.extend_from_slice(base_oid.as_bytes());
990            }
991            chunk.extend_from_slice(&pack_bytes[*zlib_start..*zlib_end]);
992            if let Some(reuse) = reuse.as_mut() {
993                reuse.record_verbatim_zlib((zlib_end - zlib_start) as u64);
994            }
995        } else {
996            let computed;
997            let (delta_len, zlib): (usize, &[u8]) = match &entry.searched_delta {
998                Some((len, zlib)) => (*len, zlib),
999                None => {
1000                    let target_data = odb.read(&entry.oid)?.data;
1001                    let base_data = odb.read(&base_oid)?.data;
1002                    let delta = if target_data.starts_with(&base_data)
1003                        && target_data.len() > base_data.len()
1004                    {
1005                        encode_prefix_extension_delta(&base_data, &target_data)?
1006                    } else {
1007                        DeltaIndex::new(&base_data)
1008                            .encode(&target_data, 0)
1009                            .ok_or_else(|| {
1010                                Error::CorruptObject(
1011                                    "failed to encode pack delta for object".into(),
1012                                )
1013                            })?
1014                    };
1015                    let mut buf = Vec::new();
1016                    write_zlib(&mut buf, &delta, compression)?;
1017                    computed = buf;
1018                    (delta.len(), &computed)
1019                }
1020            };
1021            if let Some(base_off) = in_pack_offset.filter(|_| opts.use_ofs_delta) {
1022                let dist = start.checked_sub(base_off).ok_or_else(|| {
1023                    Error::CorruptObject("ofs-delta distance underflow".to_owned())
1024                })?;
1025                encode_pack_object_header(&mut chunk, 6, delta_len);
1026                encode_ofs_delta_distance(&mut chunk, dist);
1027            } else {
1028                encode_pack_object_header(&mut chunk, 7, delta_len);
1029                if base_oid.as_bytes().len() != algo.len() {
1030                    return Err(Error::CorruptObject(
1031                        "ref-delta base oid width mismatch".to_owned(),
1032                    ));
1033                }
1034                chunk.extend_from_slice(base_oid.as_bytes());
1035            }
1036            chunk.extend_from_slice(zlib);
1037        }
1038        w.write_all(&chunk).map_err(Error::Io)?;
1039        index_rows.push((entry.oid, start, crc32fast::hash(&chunk)));
1040        body_written += chunk.len() as u64;
1041        oid_to_offset.insert(entry.oid, start);
1042    }
1043    Ok(body_written)
1044}
1045
1046/// Serialize a precomputed object list into a pack (used by [`build_pack`] and filtered bundles).
1047///
1048/// # Errors
1049///
1050/// Same as [`build_pack`].
1051pub fn build_pack_from_send_list(
1052    odb: &Odb,
1053    send: &[ObjectId],
1054    have_closure: &HashSet<ObjectId>,
1055    opts: &PackBuildOptions,
1056    blob_name_hashes: &HashMap<ObjectId, u32>,
1057) -> Result<Vec<u8>> {
1058    build_pack_from_send_list_with_parallel_threads(
1059        odb,
1060        send,
1061        have_closure,
1062        opts,
1063        blob_name_hashes,
1064        None,
1065        None,
1066    )
1067}
1068
1069/// Like [`build_pack`] with an explicit pack-write worker override for tests and embedders.
1070///
1071/// `write_threads` replaces config-derived `pack.threads` when set.
1072pub fn build_pack_with_write_threads(
1073    odb: &Odb,
1074    wants: &[ObjectId],
1075    haves: &[ObjectId],
1076    opts: &PackBuildOptions,
1077    write_threads: Option<usize>,
1078) -> Result<Vec<u8>> {
1079    build_pack_with_write_threads_full(odb, wants, haves, opts, write_threads)
1080        .map(|pack| pack.bytes)
1081}
1082
1083/// Like [`build_pack_with_write_threads`], retaining trusted pack index rows for local install.
1084pub(crate) fn build_pack_with_write_threads_full(
1085    odb: &Odb,
1086    wants: &[ObjectId],
1087    haves: &[ObjectId],
1088    opts: &PackBuildOptions,
1089    write_threads: Option<usize>,
1090) -> Result<SerializedDeltaPack> {
1091    let empty_shallow = empty_object_id_hash_set();
1092    let have_closure = reachable_closure(odb, haves, empty_shallow, true, empty_shallow)?;
1093    let selection = crate::pack_object_select::enumerate_pack_objects_naming(
1094        odb,
1095        wants,
1096        haves,
1097        &crate::pack_object_select::PackEnumerateOptions {
1098            filter: None,
1099            shallow_grafts: empty_shallow,
1100            have_shallow_grafts: Some(empty_shallow),
1101            use_bitmaps: opts.use_bitmaps,
1102            exclude_objects: Some(&have_closure),
1103            peer_odb: None,
1104            have_boundary: HaveBoundary::CommitClosure,
1105        },
1106        NameTrees::for_delta_search(opts),
1107        false,
1108        &CancelSignal::never(),
1109    )?;
1110    build_pack_from_send_list_full(
1111        odb,
1112        &selection.send,
1113        &selection.have_closure,
1114        opts,
1115        &selection.name_hashes,
1116        None,
1117        write_threads,
1118    )
1119}
1120
1121/// Like [`build_pack`] with explicit delta-search and pack-write worker overrides.
1122pub fn build_pack_with_parallel_threads(
1123    odb: &Odb,
1124    wants: &[ObjectId],
1125    haves: &[ObjectId],
1126    opts: &PackBuildOptions,
1127    search_threads: Option<usize>,
1128    write_threads: Option<usize>,
1129) -> Result<Vec<u8>> {
1130    let empty_shallow = empty_object_id_hash_set();
1131    let have_closure = reachable_closure(odb, haves, empty_shallow, true, empty_shallow)?;
1132    let selection = crate::pack_object_select::enumerate_pack_objects_naming(
1133        odb,
1134        wants,
1135        haves,
1136        &crate::pack_object_select::PackEnumerateOptions {
1137            filter: None,
1138            shallow_grafts: empty_shallow,
1139            have_shallow_grafts: Some(empty_shallow),
1140            use_bitmaps: opts.use_bitmaps,
1141            exclude_objects: Some(&have_closure),
1142            peer_odb: None,
1143            have_boundary: HaveBoundary::CommitClosure,
1144        },
1145        NameTrees::for_delta_search(opts),
1146        false,
1147        &CancelSignal::never(),
1148    )?;
1149    build_pack_from_send_list_with_parallel_threads(
1150        odb,
1151        &selection.send,
1152        &selection.have_closure,
1153        opts,
1154        &selection.name_hashes,
1155        search_threads,
1156        write_threads,
1157    )
1158}
1159
1160/// Serialize a precomputed object list with an explicit pack-write worker count.
1161pub fn build_pack_from_send_list_with_write_threads(
1162    odb: &Odb,
1163    send: &[ObjectId],
1164    have_closure: &HashSet<ObjectId>,
1165    opts: &PackBuildOptions,
1166    blob_name_hashes: &HashMap<ObjectId, u32>,
1167    write_threads: Option<usize>,
1168) -> Result<Vec<u8>> {
1169    build_pack_from_send_list_with_parallel_threads(
1170        odb,
1171        send,
1172        have_closure,
1173        opts,
1174        blob_name_hashes,
1175        None,
1176        write_threads,
1177    )
1178}
1179
1180/// Serialize a precomputed object list with explicit delta-search and write parallelism.
1181pub fn build_pack_from_send_list_with_parallel_threads(
1182    odb: &Odb,
1183    send: &[ObjectId],
1184    have_closure: &HashSet<ObjectId>,
1185    opts: &PackBuildOptions,
1186    blob_name_hashes: &HashMap<ObjectId, u32>,
1187    search_threads: Option<usize>,
1188    write_threads: Option<usize>,
1189) -> Result<Vec<u8>> {
1190    build_pack_from_send_list_full(
1191        odb,
1192        send,
1193        have_closure,
1194        opts,
1195        blob_name_hashes,
1196        search_threads,
1197        write_threads,
1198    )
1199    .map(|pack| pack.bytes)
1200}
1201
1202pub(crate) fn build_pack_from_send_list_full(
1203    odb: &Odb,
1204    send: &[ObjectId],
1205    have_closure: &HashSet<ObjectId>,
1206    opts: &PackBuildOptions,
1207    blob_name_hashes: &HashMap<ObjectId, u32>,
1208    search_threads: Option<usize>,
1209    write_threads: Option<usize>,
1210) -> Result<SerializedDeltaPack> {
1211    if !opts.delta {
1212        let bytes = serialize_pack(odb, send, opts)?;
1213        return Ok(SerializedDeltaPack {
1214            bytes,
1215            index_rows: Vec::new(),
1216            staging_path: None,
1217        });
1218    }
1219    let plan = plan_deltas(
1220        odb,
1221        send,
1222        have_closure,
1223        None,
1224        opts,
1225        blob_name_hashes,
1226        search_threads,
1227        &CancelSignal::never(),
1228    )?;
1229    crate::pack_delta::serialize_pack_with_deltas(odb, &plan.entries, opts, write_threads)
1230}
1231
1232/// Stream a pack of exactly `send` (objects the receiver lacks, none of which it
1233/// needs as a delta base) to `w`, stopping with [`Error::Cancelled`] once `stop` fires.
1234///
1235/// Reuses on-disk whole objects and deltas as `opts` allows, like
1236/// [`build_pack_from_send_list`], without holding the pack in memory.
1237pub(crate) fn write_pack_from_send_list(
1238    odb: &Odb,
1239    send: &[ObjectId],
1240    opts: &PackBuildOptions,
1241    name_hashes: &HashMap<ObjectId, u32>,
1242    stop: &CancelSignal<'_>,
1243    w: &mut dyn Write,
1244    mut on_objects: Option<&mut dyn FnMut(u64)>,
1245) -> Result<PackStats> {
1246    if !opts.delta {
1247        let bytes = serialize_pack(odb, send, opts)?;
1248        stop.poll()?;
1249        w.write_all(&bytes).map_err(Error::Io)?;
1250        if let Some(on) = on_objects.as_deref_mut() {
1251            on(send.len() as u64);
1252        }
1253        return Ok(PackStats {
1254            object_count: u32::try_from(send.len()).unwrap_or(u32::MAX),
1255            pack_bytes: bytes.len() as u64,
1256            ..PackStats::default()
1257        });
1258    }
1259    odb.with_pack_read_context(|| {
1260        read_object_infos_with_progress(odb, send, None, stop, &mut on_objects)?;
1261        stop.poll()?;
1262        let plan = plan_deltas(
1263            odb,
1264            send,
1265            &HashSet::new(),
1266            None,
1267            opts,
1268            name_hashes,
1269            None,
1270            stop,
1271        )?;
1272        stop.poll()?;
1273        crate::pack_delta::write_pack_with_deltas(
1274            odb,
1275            &plan.entries,
1276            opts,
1277            None,
1278            w,
1279            None,
1280            stop,
1281            on_objects,
1282        )
1283    })
1284}
1285
1286/// Stream a pack for a local transfer to `w`: enumerate haves on `have_odb`, read
1287/// payloads from `source_odb`, and stop with [`Error::Cancelled`] once `stop` fires.
1288#[allow(clippy::too_many_arguments)]
1289pub(crate) fn write_pack_for_local_fetch(
1290    source_odb: &Odb,
1291    wants: &[ObjectId],
1292    have_odb: &Odb,
1293    haves: &[ObjectId],
1294    source_shallow: &HashSet<ObjectId>,
1295    have_shallow: &HashSet<ObjectId>,
1296    opts: &PackBuildOptions,
1297    stop: &CancelSignal<'_>,
1298    w: &mut dyn Write,
1299) -> Result<PackStats> {
1300    PackObjects::new(have_odb, PackObjectsOptions::from(*opts))
1301        .with_source_odb(source_odb)
1302        .wants(wants)
1303        .haves(haves)
1304        .have_shallow_grafts(have_shallow)
1305        .source_shallow_grafts(source_shallow)
1306        .write_to_with_cancel(w, stop)
1307}
1308
1309fn collect_reachable_excluding(
1310    odb: &Odb,
1311    roots: &[ObjectId],
1312    exclude: &OidHashSet,
1313    skip_missing: bool,
1314    shallow_grafts: &OidHashSet,
1315) -> Result<Vec<ObjectId>> {
1316    let mut visited: OidHashSet = oid_hash_set();
1317    let mut ordered: Vec<ObjectId> = Vec::new();
1318    let mut queue: VecDeque<ObjectId> = VecDeque::new();
1319
1320    let enqueue = |oid: ObjectId,
1321                   queue: &mut VecDeque<ObjectId>,
1322                   visited: &mut OidHashSet,
1323                   ordered: &mut Vec<ObjectId>|
1324     -> bool {
1325        if !visited.insert(oid) {
1326            return false;
1327        }
1328        if !exclude.contains(&oid) {
1329            ordered.push(oid);
1330        }
1331        queue.push_back(oid);
1332        true
1333    };
1334
1335    for &root in roots {
1336        enqueue(root, &mut queue, &mut visited, &mut ordered);
1337    }
1338
1339    let oid_len = odb.hash_algo().len();
1340
1341    while let Some(oid) = queue.pop_front() {
1342        let obj = match odb.read(&oid) {
1343            Ok(o) => o,
1344            // A root/have absent from this odb cannot be traversed; with
1345            // `skip_missing` it simply contributes nothing (no descent), instead
1346            // of failing the whole pack build.
1347            Err(_) if skip_missing => continue,
1348            Err(e) => return Err(e),
1349        };
1350        match obj.kind {
1351            ObjectKind::Commit => {
1352                let commit = parse_commit(&obj.data)?;
1353                if !shallow_grafts.contains(&oid) {
1354                    for parent in commit.parents {
1355                        enqueue(parent, &mut queue, &mut visited, &mut ordered);
1356                    }
1357                }
1358                enqueue(commit.tree, &mut queue, &mut visited, &mut ordered);
1359            }
1360            ObjectKind::Tree => {
1361                for entry in TreeRefIter::with_oid_len(&obj.data, oid_len) {
1362                    let entry = entry?;
1363                    // Skip submodule (gitlink) entries: the commit they name
1364                    // lives in another object store and is not part of this pack.
1365                    if entry.mode == 0o160000 {
1366                        continue;
1367                    }
1368                    enqueue(entry.oid, &mut queue, &mut visited, &mut ordered);
1369                }
1370            }
1371            ObjectKind::Tag => {
1372                let tag = parse_tag(&obj.data)?;
1373                enqueue(tag.object, &mut queue, &mut visited, &mut ordered);
1374            }
1375            ObjectKind::Blob => {}
1376        }
1377    }
1378
1379    Ok(ordered)
1380}
1381
1382/// The pack object type code for a Git object kind (PACK v2 base types).
1383pub(crate) fn pack_type_code(kind: ObjectKind) -> u8 {
1384    match kind {
1385        ObjectKind::Commit => 1,
1386        ObjectKind::Tree => 2,
1387        ObjectKind::Blob => 3,
1388        ObjectKind::Tag => 4,
1389    }
1390}
1391
1392/// Append a PACK object header: 3-bit type + variable-length size (little-endian
1393/// 7-bit groups, MSB = continuation). Lifted from the CLI pack writer.
1394pub(crate) fn encode_pack_object_header(buf: &mut Vec<u8>, type_code: u8, payload_len: usize) {
1395    let mut size = payload_len;
1396    let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
1397    size >>= 4;
1398    if size > 0 {
1399        buf.push(first | 0x80);
1400        while size > 0 {
1401            let b = (size & 0x7f) as u8;
1402            size >>= 7;
1403            buf.push(if size > 0 { b | 0x80 } else { b });
1404        }
1405    } else {
1406        buf.push(first);
1407    }
1408}
1409
1410/// Serialize `oids` as a PACK v2 stream of whole (non-delta) objects, terminated
1411/// by the trailing pack checksum at the repository hash width.
1412fn serialize_pack(odb: &Odb, oids: &[ObjectId], opts: &PackBuildOptions) -> Result<Vec<u8>> {
1413    let mut buf = Vec::new();
1414    buf.extend_from_slice(b"PACK");
1415    buf.extend_from_slice(&2u32.to_be_bytes());
1416    let count = u32::try_from(oids.len())
1417        .map_err(|_| Error::CorruptObject("pack object count exceeds u32".to_owned()))?;
1418    buf.extend_from_slice(&count.to_be_bytes());
1419
1420    for oid in oids {
1421        let kind = odb.read_info(oid)?.kind;
1422        write_whole_pack_object_from_odb(&mut buf, odb, *oid, kind, opts)?;
1423    }
1424
1425    append_pack_trailer(&mut buf, odb.hash_algo());
1426    Ok(buf)
1427}
1428
1429/// Append one whole object from the ODB, streaming large blobs when needed.
1430fn write_whole_pack_object_from_odb(
1431    buf: &mut Vec<u8>,
1432    odb: &Odb,
1433    oid: ObjectId,
1434    kind: ObjectKind,
1435    opts: &PackBuildOptions,
1436) -> Result<()> {
1437    write_whole_pack_object_to_odb(buf, odb, oid, kind, opts).map(|_| ())
1438}
1439
1440/// Write one whole object from the ODB to `w`, reusing on-disk pack bytes or streaming
1441/// blobs at or above [`Odb::big_file_threshold`].
1442pub(crate) fn write_whole_pack_object_to_odb(
1443    w: &mut dyn Write,
1444    odb: &Odb,
1445    oid: ObjectId,
1446    kind: ObjectKind,
1447    opts: &PackBuildOptions,
1448) -> Result<u64> {
1449    if opts.reuse_objects && odb.hash_algo() == HashAlgo::Sha1 {
1450        if let Some((bytes, start, end)) =
1451            crate::pack::packed_full_object_range(odb.objects_dir(), &oid)?
1452        {
1453            write_all_chunked(w, &bytes[start..end])?;
1454            return Ok((end - start) as u64);
1455        }
1456    }
1457    let size = odb.read_info(&oid)?.size;
1458    let stream_from = odb.big_file_threshold()?;
1459    if kind == ObjectKind::Blob && size >= stream_from {
1460        let mut counter = ByteCounter { inner: w, n: 0 };
1461        odb.with_stream(&oid, |mut stream| {
1462            let payload_len = usize::try_from(stream.size).map_err(|_| {
1463                Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
1464            })?;
1465            encode_pack_object_header_to(&mut counter, pack_type_code(kind), payload_len)?;
1466            crate::odb::deflate_stream_to_writer(
1467                &mut stream.reader,
1468                stream.size,
1469                &mut counter,
1470                odb.pack_compression()?,
1471            )?;
1472            Ok(())
1473        })?
1474        .ok_or_else(|| Error::ObjectNotFound(oid.to_hex()))?;
1475        return Ok(counter.n);
1476    }
1477    let obj = odb.read(&oid)?;
1478    write_whole_pack_object_to(w, odb, oid, kind, &obj.data, opts)
1479}
1480
1481fn encode_pack_object_header_to(w: &mut dyn Write, type_code: u8, size: usize) -> Result<()> {
1482    let mut header = Vec::new();
1483    encode_pack_object_header(&mut header, type_code, size);
1484    w.write_all(&header).map_err(Error::Io)?;
1485    Ok(())
1486}
1487
1488/// Write one whole (non-delta) pack object, reusing on-disk bytes when allowed.
1489fn write_whole_pack_object(
1490    buf: &mut Vec<u8>,
1491    odb: &Odb,
1492    oid: ObjectId,
1493    kind: ObjectKind,
1494    data: &[u8],
1495    opts: &PackBuildOptions,
1496) -> Result<()> {
1497    write_whole_pack_object_to(buf, odb, oid, kind, data, opts).map(|_| ())
1498}
1499
1500fn write_whole_pack_object_to(
1501    w: &mut dyn Write,
1502    odb: &Odb,
1503    oid: ObjectId,
1504    kind: ObjectKind,
1505    data: &[u8],
1506    opts: &PackBuildOptions,
1507) -> Result<u64> {
1508    let mut counter = ByteCounter { inner: w, n: 0 };
1509    if opts.reuse_objects && odb.hash_algo() == HashAlgo::Sha1 {
1510        if let Some((bytes, start, end)) =
1511            crate::pack::packed_full_object_range(odb.objects_dir(), &oid)?
1512        {
1513            write_all_chunked(&mut counter, &bytes[start..end])?;
1514            return Ok(counter.n);
1515        }
1516    }
1517    encode_pack_object_header_to(&mut counter, pack_type_code(kind), data.len())?;
1518    write_zlib_to(&mut counter, data, odb.pack_compression()?)?;
1519    Ok(counter.n)
1520}
1521
1522/// Append the trailing pack checksum: the hash of everything written so far, at
1523/// the repository's hash width (SHA-1 → 20 bytes, SHA-256 → 32 bytes).
1524pub(crate) fn append_pack_trailer(buf: &mut Vec<u8>, algo: HashAlgo) {
1525    buf.extend_from_slice(&algo.checksum_digest(&*buf));
1526}
1527
1528/// Append `oid` from `odb` to an in-progress pack as one whole object (header + zlib body).
1529///
1530/// Returns the object's kind.
1531pub(crate) fn append_whole_object_from_odb(
1532    buf: &mut Vec<u8>,
1533    odb: &Odb,
1534    oid: ObjectId,
1535) -> Result<ObjectKind> {
1536    let obj = odb.read(&oid)?;
1537    write_whole_pack_object(
1538        buf,
1539        odb,
1540        oid,
1541        obj.kind,
1542        &obj.data,
1543        &PackBuildOptions::default(),
1544    )?;
1545    Ok(obj.kind)
1546}
1547
1548/// A single object to write, either whole or as a delta against a chosen base.
1549pub(crate) struct PlannedEntry {
1550    pub(crate) oid: ObjectId,
1551    pub(crate) kind: ObjectKind,
1552    /// `Some(base_oid)` when this entry is a delta against `base_oid`. The base may be
1553    /// an in-pack object or — for thin packs — an external base present only on the peer.
1554    pub(crate) base: Option<ObjectId>,
1555    /// Delta chosen by the delta search: uncompressed length and zlib-compressed
1556    /// instructions, ready to copy into the pack. When neither this nor
1557    /// [`Self::reused_delta_zlib`] is set for a delta, the writer computes it.
1558    pub(crate) searched_delta: Option<(usize, Vec<u8>)>,
1559    /// On-disk reuse: uncompressed size from the source pack header plus zlib
1560    /// bytes copied verbatim from a cached pack buffer (Git's `reuse_delta`).
1561    pub(crate) reused_delta_zlib:
1562        Option<(u64, std::sync::Arc<crate::pack::PackData>, usize, usize)>,
1563}
1564
1565/// The full delta plan: the ordered entries to emit. Payloads stay in the object
1566/// database and are read again when the pack is written.
1567pub(crate) struct DeltaPlan {
1568    pub(crate) entries: Vec<PlannedEntry>,
1569}
1570
1571/// A delta pack built in-process, with index rows that match the emitted stream.
1572pub(crate) struct SerializedDeltaPack {
1573    pub bytes: Vec<u8>,
1574    pub index_rows: Vec<(ObjectId, u64, u32)>,
1575    /// Pack bytes on disk when set ([`Self::bytes`] may be empty).
1576    pub staging_path: Option<std::path::PathBuf>,
1577}
1578
1579/// Kind and size of each of `oids`, in the same order, without inflating payloads.
1580fn read_object_infos(
1581    odb: &Odb,
1582    oids: &[ObjectId],
1583    search_threads: Option<usize>,
1584) -> Result<Vec<ObjectInfo>> {
1585    let mut none: Option<&mut dyn FnMut(u64)> = None;
1586    read_object_infos_with_progress(odb, oids, search_threads, &CancelSignal::never(), &mut none)
1587}
1588
1589fn read_object_infos_with_progress(
1590    odb: &Odb,
1591    oids: &[ObjectId],
1592    search_threads: Option<usize>,
1593    stop: &CancelSignal<'_>,
1594    on_objects: &mut Option<&mut dyn FnMut(u64)>,
1595) -> Result<Vec<ObjectInfo>> {
1596    let par = crate::pack_delta::pack_delta_search_parallel_options(odb, search_threads)?;
1597    let read = |oid: &ObjectId| odb.read_info(oid);
1598    let stride = crate::progress::OBJECT_PROGRESS_STRIDE as usize;
1599    if par.threads.get() <= 1 || oids.len() < 32 {
1600        let mut out = Vec::with_capacity(oids.len());
1601        for (i, oid) in oids.iter().enumerate() {
1602            stop.poll()?;
1603            out.push(read(oid)?);
1604            if (i + 1).is_multiple_of(stride) {
1605                if let Some(on) = on_objects.as_deref_mut() {
1606                    on(stride as u64);
1607                }
1608            }
1609        }
1610        let rem = oids.len() % stride;
1611        if rem != 0 {
1612            if let Some(on) = on_objects.as_deref_mut() {
1613                on(rem as u64);
1614            }
1615        }
1616        return Ok(out);
1617    }
1618    try_par_map_force(oids, par, read).map_err(|e| match e {
1619        ParallelHashError::Task(err) => err,
1620    })
1621}
1622
1623/// Reorder `entries` so every delta base that is in the pack precedes its targets.
1624///
1625/// Entries keep their relative order except that a not-yet-emitted base chain is
1626/// emitted, deepest base first, right before the first entry that needs it. Bases
1627/// outside the pack (thin-pack bases) are left alone. The entries are moved in place.
1628fn order_bases_first(entries: &mut [PlannedEntry]) {
1629    let n = entries.len();
1630    let bases: Vec<Option<usize>> = {
1631        let position: OidHashMap<usize> = entries
1632            .iter()
1633            .enumerate()
1634            .map(|(i, e)| (e.oid, i))
1635            .collect();
1636        entries
1637            .iter()
1638            .map(|e| e.base.and_then(|b| position.get(&b).copied()))
1639            .collect()
1640    };
1641    // `dest[i]`: the position entry `i` moves to.
1642    let mut dest = vec![usize::MAX; n];
1643    let mut next = 0usize;
1644    let mut chain = Vec::new();
1645    for i in 0..n {
1646        let mut cur = Some(i);
1647        while let Some(j) = cur {
1648            if dest[j] != usize::MAX || chain.len() > n {
1649                break;
1650            }
1651            chain.push(j);
1652            cur = bases[j];
1653        }
1654        while let Some(j) = chain.pop() {
1655            if dest[j] == usize::MAX {
1656                dest[j] = next;
1657                next += 1;
1658            }
1659        }
1660    }
1661    for i in 0..n {
1662        while dest[i] != i {
1663            let d = dest[i];
1664            entries.swap(i, d);
1665            dest.swap(i, d);
1666        }
1667    }
1668}
1669
1670/// Whether `objects_dir/pack` holds at least one `.pack` file.
1671pub(crate) fn objects_dir_has_local_packs(objects_dir: &Path) -> bool {
1672    let pack_dir = objects_dir.join("pack");
1673    if !pack_dir.is_dir() {
1674        return false;
1675    }
1676    std::fs::read_dir(&pack_dir)
1677        .ok()
1678        .into_iter()
1679        .flatten()
1680        .filter_map(|e| e.ok())
1681        .any(|e| e.path().extension().is_some_and(|ext| ext == "pack"))
1682}
1683
1684/// Select deltas for `send` and produce an ordered emit plan.
1685///
1686/// Only object kinds and sizes are read up front. New deltas come from the streaming
1687/// window search ([`crate::delta_search::search_deltas`]), which covers every object
1688/// kind, groups objects by kind, path name hash (`name_hashes`, for blobs and trees)
1689/// and size as Git does, and reads payloads from `odb` only while they are in the
1690/// window, so memory does not grow with the number or size of the objects packed.
1691/// Chains are depth-limited on [`crate::pack::apply_delta_depth_limit`]'s rule.
1692/// Correctness (re-indexability) is preserved because the chosen edges form no cycle,
1693/// even when on-disk deltas come from several packs, and every base is either in-pack
1694/// (emitted ahead of its targets) or, for thin packs, peer-held.
1695///
1696/// Two optional refinements:
1697///
1698/// * **Delta islands** (`opts.respect_islands`): when `pack.island` config marks
1699///   any ref, a target only deltas against a base in a compatible (superset)
1700///   island ([`crate::delta_islands::DeltaIslands::in_same_island`]). Islands
1701///   default to inactive (the prior behavior).
1702/// * **On-disk delta reuse** (`opts.reuse_deltas`): an existing
1703///   `REF_DELTA`/`OFS_DELTA` edge whose base is also in this pack is reused
1704///   verbatim ([`crate::pack::packed_ref_delta_reuse_slice`]) rather than
1705///   recomputed, still subject to island rules. Such objects and their bases are
1706///   neither targets nor bases of the search.
1707///
1708/// When `opts.thin`, trees and blobs may also delta against objects the receiver
1709/// holds, which are referenced via `REF_DELTA` and not emitted: the objects at the
1710/// same paths in the trees of the edge commits (Git's preferred bases, see
1711/// [`crate::pack_thin_bases::find_thin_bases`]), and for on-disk delta reuse any base
1712/// in `peer_held`. Edge commits are the parents of sent commits that `peer_commits`
1713/// (the selection's [`crate::pack_object_select::PackObjectSelection::have_closure`])
1714/// or `peer_held` contains.
1715#[allow(clippy::too_many_arguments)]
1716pub(crate) fn plan_deltas(
1717    odb: &Odb,
1718    send: &[ObjectId],
1719    peer_commits: &HashSet<ObjectId>,
1720    peer_held: Option<&ReachableSet>,
1721    opts: &PackBuildOptions,
1722    name_hashes: &HashMap<ObjectId, u32>,
1723    search_threads: Option<usize>,
1724    stop: &CancelSignal<'_>,
1725) -> Result<DeltaPlan> {
1726    let has_local_packs = objects_dir_has_local_packs(odb.objects_dir());
1727    let reuse_on_disk = opts.reuse_deltas && odb.hash_algo() == HashAlgo::Sha1 && has_local_packs;
1728    let search = opts.window > 0 && opts.max_depth > 0;
1729    stop.poll()?;
1730    let infos = read_object_infos(odb, send, search_threads)?;
1731    stop.poll()?;
1732    let in_pack: HashSet<ObjectId> = if reuse_on_disk || opts.thin || opts.respect_islands {
1733        send.iter().copied().collect()
1734    } else {
1735        HashSet::new()
1736    };
1737
1738    // Delta islands (`--delta-islands`): only loaded when requested AND a git dir
1739    // is attached to the odb. An inactive island set (no `pack.island` config, or
1740    // no matched ref) imposes no restriction, so the common case is unaffected.
1741    let islands = load_islands_for_pack(odb, &in_pack, opts);
1742
1743    // A thin pack may delta against what the receiver holds at the paths it changes.
1744    let peer_has = |oid: &ObjectId| peer_held.is_some_and(|held| held.contains(oid));
1745    let thin_bases = if opts.thin && (reuse_on_disk || search) {
1746        crate::pack_thin_bases::find_thin_bases(
1747            odb,
1748            send,
1749            &infos,
1750            &|oid| in_pack.contains(oid),
1751            &|oid| peer_commits.contains(oid) || peer_has(oid),
1752            name_hashes,
1753            opts.window.max(1),
1754        )?
1755    } else {
1756        crate::pack_thin_bases::ThinBases::default()
1757    };
1758
1759    // target oid -> base oid (the object `target` deltas against).
1760    let mut delta_to_base: OidHashMap<ObjectId> = oid_hash_map();
1761    // Deltas whose instruction stream is reused verbatim from an existing pack.
1762    let mut reused_zlib: HashMap<ObjectId, crate::pack::OnDiskReuseZlibEntry> = HashMap::new();
1763
1764    // On-disk delta reuse runs even with `--window=0` (no new deltas), matching Git pack-objects.
1765    if reuse_on_disk {
1766        // A thin pack keeps an on-disk delta whose base the receiver already holds; it is
1767        // written as `REF_DELTA` and completed by the receiver.
1768        let reusable_base = |oid: &ObjectId| {
1769            in_pack.contains(oid) || (opts.thin && (thin_bases.oids.contains(oid) || peer_has(oid)))
1770        };
1771        reused_zlib = crate::pack::collect_packed_delta_reuse_for_send(
1772            odb.objects_dir(),
1773            &in_pack,
1774            &reusable_base,
1775        )?;
1776        stop.poll()?;
1777        reused_zlib.retain(|t, (base, ..)| {
1778            // A stored delta against some other object the receiver holds is kept only
1779            // when the search has no base at the object's own path to do better with.
1780            let far_peer_base = !in_pack.contains(base) && !thin_bases.oids.contains(base);
1781            !(far_peer_base && search && thin_bases.targets.contains(t))
1782                && *base != *t
1783                && islands.in_same_island(t, base)
1784        });
1785        delta_to_base.extend(reused_zlib.iter().map(|(t, (base, ..))| (*t, *base)));
1786        // An empty blob is always stored whole.
1787        for (oid, info) in send.iter().zip(&infos) {
1788            if info.kind == ObjectKind::Blob && info.size == 0 && reused_zlib.remove(oid).is_some()
1789            {
1790                delta_to_base.remove(oid);
1791            }
1792        }
1793    }
1794
1795    // An object that is the base of a reused on-disk delta keeps its content as it is:
1796    // it may only become a delta against something the receiver holds (which cannot
1797    // close a cycle), and only with room left for the chains that depend on it. With
1798    // no such bases it stays whole (or keeps its own on-disk delta).
1799    let reuse_dependents: OidHashMap<usize> = {
1800        let mut longest: OidHashMap<usize> = oid_hash_map();
1801        for target in reused_zlib.keys() {
1802            let mut cur = *target;
1803            let mut depth = 0usize;
1804            while let Some(base) = delta_to_base.get(&cur) {
1805                depth += 1;
1806                cur = *base;
1807                if depth > delta_to_base.len() {
1808                    break;
1809                }
1810            }
1811            let entry = longest.entry(cur).or_insert(0);
1812            *entry = (*entry).max(depth);
1813        }
1814        longest
1815    };
1816
1817    let mut searched: OidHashMap<(usize, Vec<u8>)> = oid_hash_map();
1818    if search {
1819        let found = search_new_deltas(
1820            odb,
1821            send,
1822            &infos,
1823            &thin_bases,
1824            |oid| {
1825                delta_to_base.contains_key(oid)
1826                    || (thin_bases.oids.is_empty() && reuse_dependents.contains_key(oid))
1827            },
1828            &reuse_dependents,
1829            name_hashes,
1830            &islands,
1831            opts,
1832            search_threads,
1833            has_local_packs,
1834            stop.flag(),
1835        )?;
1836        stop.poll()?;
1837        for (target, base, delta) in found {
1838            delta_to_base.insert(target, base);
1839            searched.insert(target, delta);
1840        }
1841    }
1842    drop((in_pack, thin_bases));
1843
1844    if opts.max_depth > 0 && !delta_to_base.is_empty() {
1845        // Cap chain length. After snipping, any removed target reverts to whole. The
1846        // edges are acyclic: reused ones come without cycles, and the search neither
1847        // retargets a reused delta nor deltas the root of a reused chain against
1848        // anything in the pack.
1849        let mut delta_std: HashMap<ObjectId, ObjectId> = delta_to_base.drain().collect();
1850        crate::pack::cap_delta_chain_depth(&mut delta_std, opts.max_depth);
1851        delta_to_base.extend(delta_std);
1852    }
1853
1854    // Emit in discovery order (commits before their trees/blobs), pulling each in-pack
1855    // delta base ahead of its first target so deltas can use the compact OFS_DELTA.
1856    // A delta whose target was snipped reverts to a whole object.
1857    let mut entries: Vec<PlannedEntry> = send
1858        .iter()
1859        .zip(&infos)
1860        .map(|(&oid, info)| {
1861            let base = delta_to_base.remove(&oid);
1862            let (searched_delta, reused_delta_zlib) = match base {
1863                Some(_) => (
1864                    searched.remove(&oid),
1865                    reused_zlib
1866                        .remove(&oid)
1867                        .map(|(_, size, bytes, start, end)| (size, bytes, start, end)),
1868                ),
1869                None => (None, None),
1870            };
1871            PlannedEntry {
1872                oid,
1873                kind: info.kind,
1874                base,
1875                searched_delta,
1876                reused_delta_zlib,
1877            }
1878        })
1879        .collect();
1880    drop((infos, delta_to_base, searched, reused_zlib));
1881    order_bases_first(&mut entries);
1882    Ok(DeltaPlan { entries })
1883}
1884
1885/// A delta chosen by the search: target, base, and (delta length, zlib-compressed delta).
1886type SearchedDelta = (ObjectId, ObjectId, (usize, Vec<u8>));
1887
1888/// Run the windowed delta search over the objects of `send` (with `infos` in the same
1889/// order) that `skip` leaves, trying only pairs in compatible delta islands.
1890///
1891/// `thin_bases` join the search as bases only: they are never targets and never
1892/// written, so a delta found against one names an object the receiver holds. An
1893/// object in `reuse_dependents` (the base of reused on-disk deltas, with the longest
1894/// chain that depends on it) is only paired with those bases, leaving room for that
1895/// chain under the depth limit.
1896#[allow(clippy::too_many_arguments)]
1897fn search_new_deltas(
1898    odb: &Odb,
1899    send: &[ObjectId],
1900    infos: &[ObjectInfo],
1901    thin_bases: &crate::pack_thin_bases::ThinBases,
1902    skip: impl Fn(&ObjectId) -> bool,
1903    reuse_dependents: &OidHashMap<usize>,
1904    name_hashes: &HashMap<ObjectId, u32>,
1905    islands: &crate::delta_islands::DeltaIslands,
1906    opts: &PackBuildOptions,
1907    search_threads: Option<usize>,
1908    has_local_packs: bool,
1909    stop: &std::sync::atomic::AtomicBool,
1910) -> Result<Vec<SearchedDelta>> {
1911    u32::try_from(send.len() + thin_bases.bases.len())
1912        .map_err(|_| Error::CorruptObject("too many objects for one pack".to_owned()))?;
1913    let ids: Vec<u32> = (0..send.len())
1914        .filter(|&i| infos[i].size > 0 && !skip(&send[i]))
1915        .map(|i| i as u32)
1916        .collect();
1917    // Pairs an earlier pack writer already compared inside one pack are skipped.
1918    let packed = if opts.reuse_deltas && has_local_packs {
1919        let oids: OidHashSet = ids.iter().map(|&i| send[i as usize]).collect();
1920        crate::pack::locate_packed_objects(odb.objects_dir(), &oids).ok()
1921    } else {
1922        None
1923    };
1924    let base_oids: Vec<ObjectId> = thin_bases.bases.iter().map(|b| b.oid).collect();
1925    let base_infos = read_object_infos(odb, &base_oids, search_threads)?;
1926    let mut candidates: Vec<crate::delta_search::DeltaCandidate> = ids
1927        .into_iter()
1928        .map(|id| {
1929            let oid = send[id as usize];
1930            let info = infos[id as usize];
1931            let slot = packed.as_ref().and_then(|p| p.get(&oid));
1932            crate::delta_search::DeltaCandidate {
1933                id,
1934                oid,
1935                kind: info.kind,
1936                size: info.size,
1937                name_hash: name_hashes.get(&oid).copied().unwrap_or(0),
1938                whole_in_pack: slot.filter(|s| s.whole).map(|s| s.pack),
1939                in_pack: slot.map(|s| s.pack),
1940                dependent_depth: reuse_dependents.get(&oid).copied().unwrap_or(0),
1941                base_only: false,
1942            }
1943        })
1944        .collect();
1945    drop(packed);
1946    candidates.extend(
1947        (send.len()..)
1948            .zip(thin_bases.bases.iter().zip(&base_infos))
1949            .filter(|(_, (base, info))| info.kind == base.kind && info.size > 0)
1950            .map(|(id, (base, info))| crate::delta_search::DeltaCandidate {
1951                id: id as u32,
1952                oid: base.oid,
1953                kind: info.kind,
1954                size: info.size,
1955                name_hash: base.name_hash,
1956                whole_in_pack: None,
1957                in_pack: None,
1958                dependent_depth: 0,
1959                base_only: true,
1960            }),
1961    );
1962    let oid_of = |id: u32| {
1963        let id = id as usize;
1964        send.get(id)
1965            .copied()
1966            .unwrap_or_else(|| base_oids[id - send.len()])
1967    };
1968    let window_memory = match opts.window_memory {
1969        Some(bytes) => bytes,
1970        None => crate::pack_delta::pack_config(odb)?.pack_window_memory_bytes(),
1971    };
1972    let threads = crate::pack_delta::pack_delta_search_parallel_options(odb, search_threads)?;
1973    let allowed = |target: &ObjectId, base: &ObjectId| {
1974        islands.in_same_island(target, base)
1975            && (!reuse_dependents.contains_key(target) || thin_bases.oids.contains(base))
1976    };
1977    let found = crate::delta_search::search_deltas(
1978        odb,
1979        candidates,
1980        crate::delta_search::DeltaSearchParams {
1981            window: opts.window,
1982            max_depth: opts.max_depth,
1983            window_memory,
1984            hash_len: odb.hash_algo().len(),
1985            compression: odb.pack_compression()?,
1986            threads: threads.threads.get(),
1987        },
1988        &allowed,
1989        stop,
1990    )?;
1991    Ok(found
1992        .into_iter()
1993        .map(|d| (oid_of(d.target), oid_of(d.base), (d.delta_len, d.zlib)))
1994        .collect())
1995}
1996
1997/// Load delta-island marks for the objects being packed, honoring
1998/// `opts.respect_islands`. Returns an inactive (no-op) island set when islands
1999/// are not requested, when the odb has no attached git directory, or when no
2000/// `pack.island` regex matches a ref — so callers can always consult the result
2001/// without a flag check.
2002fn load_islands_for_pack(
2003    odb: &Odb,
2004    in_pack: &HashSet<ObjectId>,
2005    opts: &PackBuildOptions,
2006) -> crate::delta_islands::DeltaIslands {
2007    if !opts.respect_islands {
2008        return crate::delta_islands::DeltaIslands::default();
2009    }
2010    let Some(git_dir) = odb.config_git_dir() else {
2011        return crate::delta_islands::DeltaIslands::default();
2012    };
2013    let Ok(repo) = crate::repo::Repository::open(git_dir, &RepositoryOptions::empty()) else {
2014        return crate::delta_islands::DeltaIslands::default();
2015    };
2016    let env = crate::environment::Environment::empty();
2017    let cfg = crate::config::ConfigSet::load(&env, Some(git_dir), true).unwrap_or_default();
2018    crate::delta_islands::load_delta_islands(&repo, &cfg, in_pack)
2019}
2020
2021/// zlib-deflate `data` and append it to `buf`.
2022fn write_zlib(buf: &mut Vec<u8>, data: &[u8], compression: Compression) -> Result<()> {
2023    write_zlib_to(buf, data, compression)
2024}
2025
2026fn write_zlib_to(w: &mut dyn Write, data: &[u8], compression: Compression) -> Result<()> {
2027    let mut enc = ZlibEncoder::new(w, compression);
2028    enc.write_all(data).map_err(Error::Io)?;
2029    enc.finish().map_err(Error::Io)?;
2030    Ok(())
2031}
2032
2033/// Encode an `OFS_DELTA` base distance (Git's offset varint). Lifted verbatim
2034/// from the CLI pack writer's `encode_git_ofs_delta_distance`.
2035pub(crate) fn encode_ofs_delta_distance(buf: &mut Vec<u8>, mut ofs: u64) {
2036    let mut dheader = [0u8; 32];
2037    let mut pos = dheader.len() - 1;
2038    dheader[pos] = (ofs & 0x7f) as u8;
2039    while {
2040        ofs >>= 7;
2041        ofs != 0
2042    } {
2043        pos -= 1;
2044        ofs -= 1;
2045        dheader[pos] = 0x80 | ((ofs & 0x7f) as u8);
2046    }
2047    buf.extend_from_slice(&dheader[pos..]);
2048}
2049
2050#[cfg(test)]
2051#[allow(clippy::unwrap_used, clippy::expect_used)]
2052pub(crate) mod tests {
2053    use super::*;
2054    use crate::pack_object_select::{enumerate_pack_objects, PackEnumerateOptions};
2055
2056    fn planned(n: u8, base: Option<u8>) -> PlannedEntry {
2057        let oid = |n: u8| ObjectId::from_bytes(&[n; 20]).unwrap();
2058        PlannedEntry {
2059            oid: oid(n),
2060            kind: ObjectKind::Blob,
2061            base: base.map(oid),
2062            searched_delta: None,
2063            reused_delta_zlib: None,
2064        }
2065    }
2066
2067    fn order_of(mut entries: Vec<PlannedEntry>) -> Vec<u8> {
2068        order_bases_first(&mut entries);
2069        entries.iter().map(|e| e.oid.as_bytes()[0]).collect()
2070    }
2071
2072    #[test]
2073    fn bases_are_ordered_before_their_targets() {
2074        // 1 -> 3 -> 5 chain discovered target-first; 9 is an out-of-pack (thin) base.
2075        let entries = vec![
2076            planned(1, Some(3)),
2077            planned(2, None),
2078            planned(3, Some(5)),
2079            planned(4, Some(9)),
2080            planned(5, None),
2081            planned(6, Some(2)),
2082        ];
2083        assert_eq!(order_of(entries), [5, 3, 1, 2, 4, 6]);
2084    }
2085
2086    #[test]
2087    fn base_cycles_do_not_hang_or_drop_entries() {
2088        let entries = vec![planned(1, Some(2)), planned(2, Some(1)), planned(3, None)];
2089        let mut order = order_of(entries);
2090        order.sort_unstable();
2091        assert_eq!(order, [1, 2, 3]);
2092    }
2093
2094    /// Enumerate everything reachable from `main` in a git-packed fixture.
2095    pub(crate) fn packed_selection(
2096        commits: usize,
2097    ) -> (
2098        tempfile::TempDir,
2099        Repository,
2100        crate::pack_object_select::PackObjectSelection,
2101    ) {
2102        let tmp = tempfile::tempdir().unwrap();
2103        let path = grit_test_support::git_packed_history(tmp.path(), commits);
2104        let tip = grit_test_support::git(&path, &["rev-parse", "main"]);
2105        let tip = ObjectId::from_hex(tip.trim()).unwrap();
2106        let repo = Repository::open(&path, &RepositoryOptions::empty()).unwrap();
2107        let empty = HashSet::new();
2108        let selection = enumerate_pack_objects(
2109            repo.odb(),
2110            &[tip],
2111            &[],
2112            &PackEnumerateOptions {
2113                filter: None,
2114                shallow_grafts: &empty,
2115                have_shallow_grafts: Some(&empty),
2116                use_bitmaps: false,
2117                exclude_objects: None,
2118                peer_odb: None,
2119                have_boundary: HaveBoundary::CommitClosure,
2120            },
2121        )
2122        .unwrap();
2123        (tmp, repo, selection)
2124    }
2125
2126    #[cfg_attr(
2127        not(feature = "full-tests"),
2128        ignore = "starts system git or is heavy; run with --features full-tests"
2129    )]
2130    #[test]
2131    fn serve_plan_reuses_on_disk_deltas() {
2132        let (_tmp, repo, selection) = packed_selection(80);
2133        let opts = PackBuildOptions::for_serve(None, false, true);
2134        let plan = plan_deltas(
2135            repo.odb(),
2136            &selection.send,
2137            &selection.have_closure,
2138            selection.peer_held.as_deref(),
2139            &opts,
2140            &selection.name_hashes,
2141            None,
2142            &CancelSignal::never(),
2143        )
2144        .unwrap();
2145
2146        let reused = plan
2147            .entries
2148            .iter()
2149            .filter(|e| e.reused_delta_zlib.is_some())
2150            .count();
2151        let trees_and_blobs = plan
2152            .entries
2153            .iter()
2154            .filter(|e| matches!(e.kind, ObjectKind::Tree | ObjectKind::Blob))
2155            .count();
2156        assert!(
2157            reused * 2 > trees_and_blobs,
2158            "most trees and blobs keep their on-disk delta ({reused} of {trees_and_blobs})"
2159        );
2160    }
2161
2162    /// A history of loose objects: a subdirectory of stable files plus two that change
2163    /// in every commit.
2164    fn loose_history(commits: usize) -> (tempfile::TempDir, Repository, ObjectId) {
2165        let tmp = tempfile::tempdir().unwrap();
2166        let dir = tmp.path();
2167        grit_test_support::git(dir, &["init", "-q", "-b", "main"]);
2168        std::fs::create_dir_all(dir.join("src")).unwrap();
2169        for i in 0..20 {
2170            std::fs::write(
2171                dir.join(format!("src/stable{i}.txt")),
2172                format!("stable {i}\n"),
2173            )
2174            .unwrap();
2175        }
2176        let mut lines: Vec<String> = (0..200).map(|i| format!("line {i} of the file")).collect();
2177        for c in 0..commits {
2178            lines[(c * 13) % 200] = format!("changed in commit {c}");
2179            std::fs::write(dir.join("src/file.txt"), lines.join("\n")).unwrap();
2180            std::fs::write(dir.join("src/other.txt"), format!("other {c}\n")).unwrap();
2181            grit_test_support::git(dir, &["add", "."]);
2182            grit_test_support::git(
2183                dir,
2184                &[
2185                    "-c",
2186                    "user.name=T",
2187                    "-c",
2188                    "user.email=t@e",
2189                    "commit",
2190                    "-qm",
2191                    "c",
2192                ],
2193            );
2194        }
2195        let tip = grit_test_support::git(dir, &["rev-parse", "main"]);
2196        let tip = ObjectId::from_hex(tip.trim()).unwrap();
2197        let repo = Repository::open(&dir.join(".git"), &RepositoryOptions::empty()).unwrap();
2198        (tmp, repo, tip)
2199    }
2200
2201    #[cfg_attr(
2202        not(feature = "full-tests"),
2203        ignore = "starts system git or is heavy; run with --features full-tests"
2204    )]
2205    #[test]
2206    fn loose_plan_deltifies_trees_and_blobs_with_compressed_search_output() {
2207        let (tmp, repo, tip) = loose_history(12);
2208        assert!(!objects_dir_has_local_packs(repo.odb().objects_dir()));
2209        let empty = HashSet::new();
2210        let opts = PackBuildOptions::for_local_copy(None);
2211        let selection = crate::pack_object_select::enumerate_pack_objects_naming(
2212            repo.odb(),
2213            &[tip],
2214            &[],
2215            &PackEnumerateOptions {
2216                filter: None,
2217                shallow_grafts: &empty,
2218                have_shallow_grafts: Some(&empty),
2219                use_bitmaps: false,
2220                exclude_objects: None,
2221                peer_odb: None,
2222                have_boundary: HaveBoundary::CommitClosure,
2223            },
2224            NameTrees::for_delta_search(&opts),
2225            false,
2226            &CancelSignal::never(),
2227        )
2228        .unwrap();
2229        let plan = plan_deltas(
2230            repo.odb(),
2231            &selection.send,
2232            &selection.have_closure,
2233            selection.peer_held.as_deref(),
2234            &opts,
2235            &selection.name_hashes,
2236            Some(1),
2237            &CancelSignal::never(),
2238        )
2239        .unwrap();
2240        let deltas_of = |kind| {
2241            plan.entries
2242                .iter()
2243                .filter(|e| e.kind == kind && e.base.is_some())
2244                .count()
2245        };
2246        assert!(deltas_of(ObjectKind::Blob) >= 10, "blob deltas");
2247        assert!(deltas_of(ObjectKind::Tree) >= 10, "subtree deltas");
2248        assert!(plan
2249            .entries
2250            .iter()
2251            .filter(|e| e.base.is_some())
2252            .all(|e| e.searched_delta.is_some()));
2253
2254        let pack = crate::pack_delta::serialize_pack_with_deltas(
2255            repo.odb(),
2256            &plan.entries,
2257            &opts,
2258            Some(2),
2259        )
2260        .unwrap();
2261        std::fs::write(tmp.path().join("plan.pack"), &pack.bytes).unwrap();
2262        grit_test_support::git(tmp.path(), &["index-pack", "--strict", "plan.pack"]);
2263    }
2264
2265    #[test]
2266    fn serve_options_follow_pack_config() {
2267        use crate::config::{ConfigFile, ConfigScope, ConfigSet};
2268        let file = ConfigFile::parse(
2269            Path::new(".git/config"),
2270            "[pack]\n\twindow = 4\n\tdepth = 0\n",
2271            ConfigScope::Local,
2272        )
2273        .unwrap();
2274        let mut cfg = ConfigSet::new();
2275        cfg.merge(&file);
2276        let opts = PackBuildOptions::for_serve(Some(&cfg), true, false);
2277        assert_eq!((opts.window, opts.max_depth), (4, 0));
2278        assert!(opts.thin && !opts.use_ofs_delta && opts.delta);
2279        assert!(
2280            opts.reuse_deltas,
2281            "like git upload-pack, depth 0 stops new deltas but still sends on-disk deltas"
2282        );
2283
2284        let defaults = PackBuildOptions::for_serve(None, false, true);
2285        assert_eq!(
2286            (defaults.window, defaults.max_depth),
2287            (10, crate::pack::DEFAULT_PACK_DEPTH)
2288        );
2289        assert!(defaults.reuse_deltas && defaults.reuse_objects && !defaults.thin);
2290    }
2291
2292    /// A merge being sent whose two receiver-held parents hold an old and the new
2293    /// `f.txt`; the new blob is also sent, so a held tree names a sent object at the
2294    /// path being changed.
2295    struct PeerHoldsSentBlob {
2296        dir: tempfile::TempDir,
2297        repo: Repository,
2298        old: ObjectId,
2299        new: ObjectId,
2300        /// The parents and everything they reach, in write order.
2301        peer_objects: Vec<ObjectId>,
2302        peer_commits: HashSet<ObjectId>,
2303        send: Vec<ObjectId>,
2304        name_hashes: HashMap<ObjectId, u32>,
2305    }
2306
2307    impl PeerHoldsSentBlob {
2308        fn new() -> Self {
2309            use crate::objects::{serialize_tree, TreeEntry};
2310            use crate::pack_name_hash::pack_name_hash;
2311            let dir = tempfile::TempDir::new().unwrap();
2312            let repo =
2313                Repository::init(dir.path(), &crate::repo::InitOptions::default()).expect("init");
2314            let odb = repo.odb();
2315            let tree = |entries: &[(&str, ObjectId)]| {
2316                let entries: Vec<TreeEntry> = entries
2317                    .iter()
2318                    .map(|&(name, oid)| TreeEntry {
2319                        mode: 0o100644,
2320                        name: name.as_bytes().to_vec(),
2321                        oid,
2322                    })
2323                    .collect();
2324                odb.write(ObjectKind::Tree, &serialize_tree(&entries))
2325                    .unwrap()
2326            };
2327            let commit = |tree: ObjectId, parents: &[ObjectId]| {
2328                let parents: String = parents
2329                    .iter()
2330                    .map(|p| format!("parent {}\n", p.to_hex()))
2331                    .collect();
2332                let body = format!(
2333                    "tree {}\n{parents}author A <a@example.com> 0 +0000\ncommitter A <a@example.com> 0 +0000\n\nc\n",
2334                    tree.to_hex()
2335                );
2336                odb.write(ObjectKind::Commit, body.as_bytes()).unwrap()
2337            };
2338            let lines: Vec<String> = (0..200).map(|i| format!("line {i} of f.txt")).collect();
2339            let old = odb
2340                .write(ObjectKind::Blob, lines.join("\n").as_bytes())
2341                .unwrap();
2342            let new = odb
2343                .write(
2344                    ObjectKind::Blob,
2345                    format!("{}\nmore\n", lines.join("\n")).as_bytes(),
2346                )
2347                .unwrap();
2348            let old_tree = tree(&[("f.txt", old)]);
2349            let new_tree = tree(&[("f.txt", new)]);
2350            let first = commit(old_tree, &[]);
2351            let second = commit(new_tree, &[]);
2352            let extra = odb.write(ObjectKind::Blob, b"extra\n").unwrap();
2353            let merge_tree = tree(&[("extra.txt", extra), ("f.txt", new)]);
2354            let merge = commit(merge_tree, &[first, second]);
2355            Self {
2356                dir,
2357                old,
2358                new,
2359                peer_objects: vec![old, new, old_tree, new_tree, first, second],
2360                peer_commits: HashSet::from([first, second]),
2361                send: vec![merge, merge_tree, new, extra],
2362                name_hashes: HashMap::from([
2363                    (new, pack_name_hash("f.txt")),
2364                    (extra, pack_name_hash("extra.txt")),
2365                ]),
2366                repo,
2367            }
2368        }
2369
2370        fn thin_plan(&self) -> DeltaPlan {
2371            plan_deltas(
2372                self.repo.odb(),
2373                &self.send,
2374                &self.peer_commits,
2375                None,
2376                &PackBuildOptions::for_serve(None, true, true),
2377                &self.name_hashes,
2378                Some(1),
2379                &CancelSignal::never(),
2380            )
2381            .unwrap()
2382        }
2383    }
2384
2385    #[test]
2386    fn thin_pack_never_deltas_a_sent_blob_against_itself() {
2387        let fx = PeerHoldsSentBlob::new();
2388        let plan = fx.thin_plan();
2389        assert_eq!(plan.entries.len(), fx.send.len());
2390        for entry in &plan.entries {
2391            assert_ne!(
2392                entry.base,
2393                Some(entry.oid),
2394                "a delta against itself never resolves"
2395            );
2396        }
2397        let new = plan.entries.iter().find(|e| e.oid == fx.new).unwrap();
2398        assert_eq!(new.base, Some(fx.old), "the other held version is the base");
2399    }
2400
2401    #[cfg_attr(
2402        not(feature = "full-tests"),
2403        ignore = "starts system git or is heavy; run with --features full-tests"
2404    )]
2405    #[test]
2406    fn thin_pack_with_held_sent_blob_completes_in_git() {
2407        let fx = PeerHoldsSentBlob::new();
2408        let plan = fx.thin_plan();
2409        let opts = PackBuildOptions::for_serve(None, true, true);
2410        let pack = crate::pack_delta::serialize_pack_with_deltas(
2411            fx.repo.odb(),
2412            &plan.entries,
2413            &opts,
2414            Some(1),
2415        )
2416        .unwrap();
2417
2418        let root = fx.dir.path();
2419        grit_test_support::git(root, &["init", "-q", "--bare", "peer"]);
2420        let peer = root.join("peer");
2421        for oid in &fx.peer_objects {
2422            let obj = fx.repo.odb().read(oid).unwrap();
2423            let written = grit_test_support::git_cmd(&[
2424                "hash-object",
2425                "-w",
2426                "--stdin",
2427                "-t",
2428                obj.kind.as_str(),
2429            ])
2430            .in_dir(&peer)
2431            .stdin(obj.data)
2432            .suc();
2433            assert_eq!(written.stdout.trim(), oid.to_hex());
2434        }
2435        grit_test_support::git_cmd(&["index-pack", "--strict", "--fix-thin", "--stdin"])
2436            .in_dir(&peer)
2437            .stdin(pack.bytes)
2438            .suc();
2439        let merge = fx.send[0].to_hex();
2440        grit_test_support::git(&peer, &["update-ref", "refs/heads/main", &merge]);
2441        grit_test_support::git(&peer, &["fsck", "--strict", "--no-dangling"]);
2442    }
2443}