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