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