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