1use crate::environment::RepositoryOptions;
7
8use std::collections::{HashMap, HashSet, VecDeque};
9use std::io::Write;
10use std::path::Path;
11
12use flate2::write::ZlibEncoder;
13use flate2::Compression;
14
15use crate::bitmap_walk::ReachableSet;
16use crate::delta_encode::{encode_prefix_extension_delta, DeltaIndex};
17use crate::error::{Error, Result};
18use crate::hash::HashingWriter;
19use crate::objects::{
20 empty_object_id_hash_set, oid_hash_map, oid_hash_set, oid_hash_set_from, parse_commit,
21 parse_tag, HashAlgo, ObjectId, ObjectInfo, ObjectKind, OidHashMap, OidHashSet, TreeRefIter,
22};
23use crate::odb::Odb;
24use crate::pack_object_select::{
25 append_included_tags, HaveBoundary, NameTrees, PackObjectSelection, TagCandidate,
26};
27use crate::parallel::{try_par_map_force, ParallelHashError};
28use crate::progress::CancelSignal;
29use crate::repo::Repository;
30use crate::rev_list::ObjectFilter;
31
32#[derive(Clone, Copy, Debug)]
34pub struct PackBuildOptions {
35 pub thin: bool,
38 pub delta: bool,
40 pub window: usize,
42 pub max_depth: usize,
44 pub use_ofs_delta: bool,
46 pub respect_islands: bool,
48 pub reuse_deltas: bool,
50 pub reuse_objects: bool,
52 pub use_bitmaps: bool,
54 pub window_memory: Option<u64>,
59}
60
61pub const SERVE_WINDOW_MEMORY: u64 = 32 * 1024 * 1024;
65
66impl Default for PackBuildOptions {
67 fn default() -> Self {
68 Self {
69 thin: false,
70 delta: false,
71 window: 10,
72 max_depth: 50,
73 use_ofs_delta: true,
74 respect_islands: false,
75 reuse_deltas: true,
76 reuse_objects: true,
77 use_bitmaps: true,
78 window_memory: None,
79 }
80 }
81}
82
83impl PackBuildOptions {
84 #[must_use]
86 pub fn window_and_depth_from_config(cfg: Option<&crate::config::ConfigSet>) -> (usize, usize) {
87 cfg.map(|c| (c.pack_object_window(), c.pack_object_depth()))
88 .unwrap_or((10, crate::pack::DEFAULT_PACK_DEPTH))
89 }
90
91 #[must_use]
93 pub fn for_local_copy(cfg: Option<&crate::config::ConfigSet>) -> Self {
94 let (window, max_depth) = Self::window_and_depth_from_config(cfg);
95 Self {
96 thin: false,
97 delta: true,
98 window,
99 max_depth,
100 use_ofs_delta: true,
101 reuse_deltas: max_depth > 0,
102 reuse_objects: true,
103 use_bitmaps: true,
104 ..Default::default()
105 }
106 }
107
108 #[must_use]
111 pub fn for_repack_rewrite(cfg: Option<&crate::config::ConfigSet>) -> Self {
112 let max_depth = Self::window_and_depth_from_config(cfg).1;
113 Self {
114 thin: false,
115 delta: true,
116 window: 0,
117 max_depth,
118 use_ofs_delta: true,
119 reuse_deltas: max_depth > 0,
120 reuse_objects: true,
121 use_bitmaps: true,
122 ..Default::default()
123 }
124 }
125
126 #[must_use]
141 pub fn for_serve(
142 cfg: Option<&crate::config::ConfigSet>,
143 thin: bool,
144 use_ofs_delta: bool,
145 ) -> Self {
146 let (window, max_depth) = Self::window_and_depth_from_config(cfg);
147 let window_memory = cfg
148 .map(crate::config::ConfigSet::pack_window_memory_bytes)
149 .filter(|&bytes| bytes > 0)
150 .unwrap_or(SERVE_WINDOW_MEMORY);
151 Self {
152 thin,
153 delta: true,
154 window,
155 max_depth,
156 use_ofs_delta,
157 reuse_deltas: true,
158 reuse_objects: true,
159 use_bitmaps: true,
160 window_memory: Some(window_memory),
161 ..Default::default()
162 }
163 }
164
165 #[must_use]
170 pub fn for_local_push(cfg: Option<&crate::config::ConfigSet>) -> Self {
171 let (window, max_depth) = Self::window_and_depth_from_config(cfg);
172 Self {
173 thin: true,
174 delta: true,
175 window,
176 max_depth,
177 use_ofs_delta: true,
178 reuse_deltas: true,
179 reuse_objects: true,
180 use_bitmaps: true,
181 ..Default::default()
182 }
183 }
184}
185
186#[derive(Clone, Copy, Debug, Default)]
188pub struct PackObjectsOptions {
189 pub build: PackBuildOptions,
191 pub search_threads: Option<usize>,
193 pub write_threads: Option<usize>,
195}
196
197impl From<PackBuildOptions> for PackObjectsOptions {
198 fn from(build: PackBuildOptions) -> Self {
199 Self {
200 build,
201 search_threads: None,
202 write_threads: None,
203 }
204 }
205}
206
207#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
209pub struct PackStats {
210 pub object_count: u32,
212 pub pack_bytes: u64,
214 pub delta_count: u32,
216 pub verbatim_reuse_bytes: u64,
218 pub verbatim_reuse_objects: u32,
220 pub pack_ofs_fixup_objects: u32,
222 pub pack_ref_from_ofs_objects: u32,
224}
225
226#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228pub enum PackProgress {
229 Enumerated {
231 total: usize,
233 },
234 PlannedDeltas,
236 WroteObject {
238 written: usize,
240 total: usize,
242 },
243}
244
245pub struct PackObjects<'a> {
247 odb: &'a Odb,
248 source_odb: &'a Odb,
249 opts: PackObjectsOptions,
250 wants: Vec<ObjectId>,
251 haves: Vec<ObjectId>,
252 have_shallow: OidHashSet,
253 source_shallow: OidHashSet,
254 progress: Option<Box<dyn FnMut(PackProgress)>>,
255}
256
257impl<'a> PackObjects<'a> {
258 #[must_use]
260 pub fn new(odb: &'a Odb, opts: PackObjectsOptions) -> Self {
261 Self {
262 odb,
263 source_odb: odb,
264 opts,
265 wants: Vec::new(),
266 haves: Vec::new(),
267 have_shallow: oid_hash_set(),
268 source_shallow: oid_hash_set(),
269 progress: None,
270 }
271 }
272
273 #[must_use]
275 pub fn with_source_odb(mut self, source: &'a Odb) -> Self {
276 self.source_odb = source;
277 self
278 }
279
280 #[must_use]
282 pub fn wants(mut self, wants: &[ObjectId]) -> Self {
283 self.wants.extend_from_slice(wants);
284 self
285 }
286
287 #[must_use]
289 pub fn haves(mut self, haves: &[ObjectId]) -> Self {
290 self.haves.extend_from_slice(haves);
291 self
292 }
293
294 #[must_use]
296 pub fn have_shallow_grafts(mut self, grafts: &HashSet<ObjectId>) -> Self {
297 self.have_shallow = grafts.iter().copied().collect();
298 self
299 }
300
301 #[must_use]
303 pub fn source_shallow_grafts(mut self, grafts: &HashSet<ObjectId>) -> Self {
304 self.source_shallow = grafts.iter().copied().collect();
305 self
306 }
307
308 #[must_use]
310 pub fn progress<F>(mut self, f: F) -> Self
311 where
312 F: FnMut(PackProgress) + 'static,
313 {
314 self.progress = Some(Box::new(f));
315 self
316 }
317
318 fn fire(&mut self, ev: PackProgress) {
319 if let Some(ref mut f) = self.progress {
320 f(ev);
321 }
322 }
323
324 pub fn write_to(self, w: &mut dyn Write) -> Result<PackStats> {
330 self.write_to_with_cancel(w, &CancelSignal::never())
331 }
332
333 pub(crate) fn write_to_with_cancel(
335 mut self,
336 w: &mut dyn Write,
337 stop: &CancelSignal<'_>,
338 ) -> Result<PackStats> {
339 let opts = self.opts.build;
340 let have_shallow: HashSet<ObjectId> = self.have_shallow.iter().copied().collect();
341 let source_shallow: HashSet<ObjectId> = self.source_shallow.iter().copied().collect();
342 let peer_odb = (!std::ptr::eq(self.source_odb, self.odb)).then_some(self.odb);
343 let selection = crate::pack_object_select::enumerate_pack_objects_naming(
344 self.source_odb,
345 &self.wants,
346 &self.haves,
347 &crate::pack_object_select::PackEnumerateOptions {
348 filter: None,
349 shallow_grafts: &source_shallow,
350 have_shallow_grafts: Some(&have_shallow),
351 use_bitmaps: opts.use_bitmaps,
352 exclude_objects: None,
353 peer_odb,
354 have_boundary: HaveBoundary::CommitClosure,
355 },
356 NameTrees::for_delta_search(&opts),
357 false,
358 stop,
359 )?;
360 let send = selection.send;
361 self.fire(PackProgress::Enumerated { total: send.len() });
362 if !opts.delta {
363 let bytes = serialize_pack(self.source_odb, &send, &opts)?;
364 stop.poll()?;
365 w.write_all(&bytes).map_err(Error::Io)?;
366 let count = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
367 return Ok(PackStats {
368 object_count: count,
369 pack_bytes: bytes.len() as u64,
370 ..Default::default()
371 });
372 }
373 let plan = plan_deltas(
374 self.source_odb,
375 &send,
376 &selection.have_closure,
377 selection.peer_held.as_deref(),
378 &opts,
379 &selection.name_hashes,
380 self.opts.search_threads,
381 stop,
382 )?;
383 self.fire(PackProgress::PlannedDeltas);
384 crate::pack_delta::write_pack_with_deltas(
385 self.source_odb,
386 &plan.entries,
387 &opts,
388 self.opts.write_threads,
389 w,
390 None,
391 stop,
392 None,
393 )
394 }
395}
396
397pub fn reachable_closure(
400 odb: &Odb,
401 roots: &[ObjectId],
402 stop: &HashSet<ObjectId>,
403 skip_missing: bool,
404 shallow_grafts: &HashSet<ObjectId>,
405) -> Result<HashSet<ObjectId>> {
406 let stop = oid_hash_set_from(stop);
407 let shallow_grafts = oid_hash_set_from(shallow_grafts);
408 let mut seen = oid_hash_set();
409 let order = collect_reachable_excluding(odb, roots, &stop, skip_missing, &shallow_grafts)?;
410 for oid in order {
411 seen.insert(oid);
412 }
413 Ok(seen.into_iter().collect())
414}
415
416pub fn build_pack(
419 odb: &Odb,
420 wants: &[ObjectId],
421 haves: &[ObjectId],
422 opts: &PackBuildOptions,
423) -> Result<Vec<u8>> {
424 build_pack_with_shallow_filter_and_boundary(
425 odb,
426 wants,
427 haves,
428 empty_object_id_hash_set(),
429 empty_object_id_hash_set(),
430 None,
431 opts,
432 HaveBoundary::CommitClosure,
433 )
434}
435
436pub fn build_pack_for_push(
446 odb: &Odb,
447 wants: &[ObjectId],
448 haves: &[ObjectId],
449 opts: &PackBuildOptions,
450) -> Result<Vec<u8>> {
451 let mut pack = Vec::new();
452 write_pack_for_push(odb, wants, haves, opts, &mut pack)?;
453 Ok(pack)
454}
455
456pub fn write_pack_for_push(
479 odb: &Odb,
480 wants: &[ObjectId],
481 haves: &[ObjectId],
482 opts: &PackBuildOptions,
483 w: &mut dyn Write,
484) -> Result<PackStats> {
485 write_pack_for_push_with_cancel(odb, wants, haves, opts, w, &CancelSignal::never())
486}
487
488pub(crate) fn write_pack_for_push_with_cancel(
490 odb: &Odb,
491 wants: &[ObjectId],
492 haves: &[ObjectId],
493 opts: &PackBuildOptions,
494 w: &mut dyn Write,
495 stop: &CancelSignal<'_>,
496) -> Result<PackStats> {
497 odb.with_pack_read_context(|| {
500 write_pack_with_shallow_and_filter(
501 odb,
502 wants,
503 haves,
504 empty_object_id_hash_set(),
505 empty_object_id_hash_set(),
506 None,
507 opts,
508 HaveBoundary::TipsOnly,
509 &[],
510 w,
511 stop,
512 )
513 })
514}
515
516pub fn build_pack_with_shallow_and_filter(
521 odb: &Odb,
522 wants: &[ObjectId],
523 haves: &[ObjectId],
524 have_shallow: &HashSet<ObjectId>,
525 source_shallow: &HashSet<ObjectId>,
526 filter: Option<&ObjectFilter>,
527 opts: &PackBuildOptions,
528) -> Result<Vec<u8>> {
529 build_pack_with_shallow_filter_and_boundary(
530 odb,
531 wants,
532 haves,
533 have_shallow,
534 source_shallow,
535 filter,
536 opts,
537 HaveBoundary::CommitClosure,
538 )
539}
540
541#[allow(clippy::too_many_arguments)]
542fn build_pack_with_shallow_filter_and_boundary(
543 odb: &Odb,
544 wants: &[ObjectId],
545 haves: &[ObjectId],
546 have_shallow: &HashSet<ObjectId>,
547 source_shallow: &HashSet<ObjectId>,
548 filter: Option<&ObjectFilter>,
549 opts: &PackBuildOptions,
550 have_boundary: HaveBoundary,
551) -> Result<Vec<u8>> {
552 let mut pack = Vec::new();
553 write_pack_with_shallow_and_filter(
554 odb,
555 wants,
556 haves,
557 have_shallow,
558 source_shallow,
559 filter,
560 opts,
561 have_boundary,
562 &[],
563 &mut pack,
564 &CancelSignal::never(),
565 )?;
566 Ok(pack)
567}
568
569#[allow(clippy::too_many_arguments)]
572fn write_pack_with_shallow_and_filter(
573 odb: &Odb,
574 wants: &[ObjectId],
575 haves: &[ObjectId],
576 have_shallow: &HashSet<ObjectId>,
577 source_shallow: &HashSet<ObjectId>,
578 filter: Option<&ObjectFilter>,
579 opts: &PackBuildOptions,
580 have_boundary: HaveBoundary,
581 include_tags: &[TagCandidate],
582 w: &mut dyn Write,
583 stop: &CancelSignal<'_>,
584) -> Result<PackStats> {
585 let mut selection = crate::pack_object_select::enumerate_pack_objects_naming(
586 odb,
587 wants,
588 haves,
589 &crate::pack_object_select::PackEnumerateOptions {
590 filter,
591 shallow_grafts: source_shallow,
592 have_shallow_grafts: Some(have_shallow),
593 use_bitmaps: opts.use_bitmaps,
594 exclude_objects: None,
595 peer_odb: None,
596 have_boundary,
597 },
598 NameTrees::for_delta_search(opts),
599 false,
600 stop,
601 )?;
602 append_included_tags(odb, &mut selection.send, include_tags)?;
603 write_selected_pack(odb, &selection, opts, w, stop)
604}
605
606fn write_selected_pack(
609 odb: &Odb,
610 selection: &PackObjectSelection,
611 opts: &PackBuildOptions,
612 w: &mut dyn Write,
613 stop: &CancelSignal<'_>,
614) -> Result<PackStats> {
615 if !opts.delta {
616 let bytes = serialize_pack(odb, &selection.send, opts)?;
617 stop.poll()?;
618 w.write_all(&bytes).map_err(Error::Io)?;
619 return Ok(PackStats {
620 object_count: u32::try_from(selection.send.len()).unwrap_or(u32::MAX),
621 pack_bytes: bytes.len() as u64,
622 ..PackStats::default()
623 });
624 }
625 let plan = plan_deltas(
626 odb,
627 &selection.send,
628 &selection.have_closure,
629 selection.peer_held.as_deref(),
630 opts,
631 &selection.name_hashes,
632 None,
633 stop,
634 )?;
635 crate::pack_delta::write_pack_with_deltas(odb, &plan.entries, opts, None, w, None, stop, None)
636}
637
638#[allow(clippy::too_many_arguments)]
647pub fn write_upload_pack_stream(
648 repo: &Repository,
649 wants: &[ObjectId],
650 haves: &[ObjectId],
651 source_shallow: &HashSet<ObjectId>,
652 have_shallow: &HashSet<ObjectId>,
653 filter: Option<&ObjectFilter>,
654 opts: &PackBuildOptions,
655 progress: &mut dyn FnMut(PackProgress),
656 w: &mut dyn Write,
657) -> Result<PackStats> {
658 write_upload_pack_stream_with_cancel(
659 repo,
660 wants,
661 haves,
662 source_shallow,
663 have_shallow,
664 filter,
665 &[],
666 opts,
667 progress,
668 w,
669 &CancelSignal::never(),
670 )
671}
672
673#[allow(clippy::too_many_arguments)]
677pub(crate) fn write_upload_pack_stream_with_cancel(
678 repo: &Repository,
679 wants: &[ObjectId],
680 haves: &[ObjectId],
681 source_shallow: &HashSet<ObjectId>,
682 have_shallow: &HashSet<ObjectId>,
683 filter: Option<&ObjectFilter>,
684 include_tags: &[TagCandidate],
685 opts: &PackBuildOptions,
686 progress: &mut dyn FnMut(PackProgress),
687 w: &mut dyn Write,
688 stop: &CancelSignal<'_>,
689) -> Result<PackStats> {
690 repo.odb().with_pack_read_context(|| {
691 write_upload_pack_stream_in_context(
692 repo,
693 wants,
694 haves,
695 source_shallow,
696 have_shallow,
697 filter,
698 include_tags,
699 opts,
700 progress,
701 w,
702 stop,
703 )
704 })
705}
706
707#[allow(clippy::too_many_arguments)]
708fn write_upload_pack_stream_in_context(
709 repo: &Repository,
710 wants: &[ObjectId],
711 haves: &[ObjectId],
712 source_shallow: &HashSet<ObjectId>,
713 have_shallow: &HashSet<ObjectId>,
714 filter: Option<&ObjectFilter>,
715 include_tags: &[TagCandidate],
716 opts: &PackBuildOptions,
717 progress: &mut dyn FnMut(PackProgress),
718 w: &mut dyn Write,
719 stop: &CancelSignal<'_>,
720) -> Result<PackStats> {
721 if opts.use_bitmaps && filter.is_none() {
722 if let Some(stats) = crate::pack_reuse::try_write_streaming_pack_including_tags(
723 repo,
724 wants,
725 haves,
726 source_shallow,
727 have_shallow,
728 filter,
729 include_tags,
730 opts,
731 progress,
732 w,
733 stop,
734 )? {
735 return Ok(stats);
736 }
737 }
738 write_pack_with_shallow_and_filter(
741 repo.odb(),
742 wants,
743 haves,
744 have_shallow,
745 source_shallow,
746 filter,
747 opts,
748 HaveBoundary::TipsOnly,
749 include_tags,
750 w,
751 stop,
752 )
753}
754
755type PackIndexRows = Vec<(ObjectId, u64, u32)>;
756
757#[allow(clippy::too_many_arguments)]
763pub(crate) fn select_upload_pack_objects_probing(
764 repo: &Repository,
765 wants: &[ObjectId],
766 haves: &[ObjectId],
767 source_shallow: &HashSet<ObjectId>,
768 have_shallow: &HashSet<ObjectId>,
769 filter: Option<&ObjectFilter>,
770 opts: &PackBuildOptions,
771) -> Result<(
772 PackObjectSelection,
773 Vec<crate::pack_object_select::MissingBlob>,
774)> {
775 repo.odb().with_pack_read_context(|| {
776 crate::pack_object_select::enumerate_pack_objects_probing(
777 repo.odb(),
778 wants,
779 haves,
780 &crate::pack_object_select::PackEnumerateOptions {
781 filter,
782 shallow_grafts: source_shallow,
783 have_shallow_grafts: Some(have_shallow),
784 use_bitmaps: false,
785 exclude_objects: None,
786 peer_odb: None,
787 have_boundary: HaveBoundary::TipsOnly,
788 },
789 NameTrees::for_delta_search(opts),
790 &CancelSignal::never(),
791 )
792 })
793}
794
795pub(crate) fn write_upload_pack_selection(
798 repo: &Repository,
799 selection: &PackObjectSelection,
800 opts: &PackBuildOptions,
801 w: &mut dyn Write,
802 stop: &CancelSignal<'_>,
803) -> Result<PackStats> {
804 repo.odb()
805 .with_pack_read_context(|| write_selected_pack(repo.odb(), selection, opts, w, stop))
806}
807
808fn write_pack_header(w: &mut dyn Write, count: u32) -> Result<()> {
809 w.write_all(b"PACK").map_err(Error::Io)?;
810 w.write_all(&2u32.to_be_bytes()).map_err(Error::Io)?;
811 w.write_all(&count.to_be_bytes()).map_err(Error::Io)?;
812 Ok(())
813}
814
815pub(crate) fn write_delta_pack_stream(
817 odb: &Odb,
818 plan: &DeltaPlan,
819 opts: &PackBuildOptions,
820 w: &mut dyn Write,
821 reuse: Option<&mut crate::pack_reuse::StreamingReuse>,
822) -> Result<PackStats> {
823 let algo = odb.hash_algo();
824 let mut out = HashingWriter::new(algo, w);
825 let reuse_stats = if let Some(reuse) = reuse {
826 let (_, body_bytes) = write_delta_plan_stream_body(&mut out, odb, plan, opts, Some(reuse))?;
827 out.finish().map_err(Error::Io)?;
828 let pack_bytes = 12u64 + body_bytes + algo.len() as u64;
829 let stats = reuse.take_stats();
830 (pack_bytes, stats)
831 } else {
832 let (_, body_bytes) = write_delta_plan_stream_body(&mut out, odb, plan, opts, None)?;
833 out.finish().map_err(Error::Io)?;
834 (
835 12u64 + body_bytes + algo.len() as u64,
836 crate::pack_reuse::ReuseWriteStats::default(),
837 )
838 };
839 let delta_count = plan.entries.iter().filter(|e| e.base.is_some()).count() as u32;
840 Ok(PackStats {
841 object_count: u32::try_from(plan.entries.len())
842 .map_err(|_| Error::CorruptObject("pack object count exceeds u32".to_owned()))?,
843 pack_bytes: reuse_stats.0,
844 delta_count,
845 verbatim_reuse_bytes: reuse_stats.1.verbatim_reuse_bytes,
846 verbatim_reuse_objects: reuse_stats.1.verbatim_reuse_objects,
847 pack_ofs_fixup_objects: reuse_stats.1.pack_ofs_fixup_objects,
848 pack_ref_from_ofs_objects: reuse_stats.1.pack_ref_from_ofs_objects,
849 })
850}
851
852fn write_delta_plan_stream_body(
853 w: &mut dyn Write,
854 odb: &Odb,
855 plan: &DeltaPlan,
856 opts: &PackBuildOptions,
857 reuse: Option<&mut crate::pack_reuse::StreamingReuse>,
858) -> Result<(PackIndexRows, u64)> {
859 let count = u32::try_from(plan.entries.len())
860 .map_err(|_| Error::CorruptObject("pack object count exceeds u32".to_owned()))?;
861 write_pack_header(w, count)?;
862 let mut index_rows = Vec::with_capacity(plan.entries.len());
863 let body_bytes = write_planned_entries(w, odb, plan, opts, 12, reuse, &mut index_rows)?;
864 Ok((index_rows, body_bytes))
865}
866
867pub(crate) const PACK_STREAM_COPY_CHUNK: usize = 64 * 1024;
868
869pub(crate) struct CrcWriter<'a, W: Write + ?Sized> {
870 inner: &'a mut W,
871 crc: crc32fast::Hasher,
872}
873
874impl<'a, W: Write + ?Sized> CrcWriter<'a, W> {
875 pub(crate) fn new(inner: &'a mut W, prefix: &[u8]) -> Self {
876 let mut crc = crc32fast::Hasher::new();
877 crc.update(prefix);
878 Self { inner, crc }
879 }
880
881 pub(crate) fn finalize(self) -> u32 {
882 self.crc.finalize()
883 }
884}
885
886impl<W: Write + ?Sized> Write for CrcWriter<'_, W> {
887 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
888 let n = self.inner.write(buf)?;
889 self.crc.update(&buf[..n]);
890 Ok(n)
891 }
892
893 fn flush(&mut self) -> std::io::Result<()> {
894 self.inner.flush()
895 }
896}
897
898struct ByteCounter<'a, W: Write + ?Sized> {
899 inner: &'a mut W,
900 n: u64,
901}
902
903impl<W: Write + ?Sized> Write for ByteCounter<'_, W> {
904 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
905 let n = self.inner.write(buf)?;
906 self.n += n as u64;
907 Ok(n)
908 }
909
910 fn flush(&mut self) -> std::io::Result<()> {
911 self.inner.flush()
912 }
913}
914
915pub(crate) fn write_all_chunked(w: &mut dyn Write, data: &[u8]) -> Result<()> {
916 for chunk in data.chunks(PACK_STREAM_COPY_CHUNK) {
917 w.write_all(chunk).map_err(Error::Io)?;
918 }
919 Ok(())
920}
921
922fn write_planned_entries(
923 w: &mut dyn Write,
924 odb: &Odb,
925 plan: &DeltaPlan,
926 opts: &PackBuildOptions,
927 pack_body_start: u64,
928 mut reuse: Option<&mut crate::pack_reuse::StreamingReuse>,
929 index_rows: &mut Vec<(ObjectId, u64, u32)>,
930) -> Result<u64> {
931 let algo = odb.hash_algo();
932 let compression = odb.pack_compression()?;
933 let mut oid_to_offset: OidHashMap<u64> = oid_hash_map();
934 let mut body_written = 0u64;
935
936 for entry in &plan.entries {
937 let start = pack_body_start + body_written;
938 let mut chunk = Vec::new();
939 if entry.base.is_none() {
940 let mut crc_w = CrcWriter {
941 inner: w,
942 crc: crc32fast::Hasher::new(),
943 };
944 let wrote = {
945 let mut counter = ByteCounter {
946 inner: &mut crc_w,
947 n: 0,
948 };
949 if let Some(reuse) = reuse.as_mut() {
950 if let Some(n) = reuse.try_write_whole(&mut counter, &entry.oid, opts)? {
951 n
952 } else {
953 write_whole_pack_object_to_odb(
954 &mut counter,
955 odb,
956 entry.oid,
957 entry.kind,
958 opts,
959 )?
960 }
961 } else {
962 write_whole_pack_object_to_odb(&mut counter, odb, entry.oid, entry.kind, opts)?
963 }
964 };
965 let crc = crc_w.finalize();
966 index_rows.push((entry.oid, start, crc));
967 body_written += wrote;
968 oid_to_offset.insert(entry.oid, start);
969 continue;
970 }
971 let Some(base_oid) = entry.base else {
972 continue;
973 };
974 let in_pack_offset = oid_to_offset.get(&base_oid).copied();
975 if let Some((delta_size, pack_bytes, zlib_start, zlib_end)) = &entry.reused_delta_zlib {
976 let delta_len = usize::try_from(*delta_size)
977 .map_err(|_| Error::CorruptObject("reused delta size exceeds usize".to_owned()))?;
978 if let Some(base_off) = in_pack_offset.filter(|_| opts.use_ofs_delta) {
979 let dist = start.checked_sub(base_off).ok_or_else(|| {
980 Error::CorruptObject("ofs-delta distance underflow".to_owned())
981 })?;
982 encode_pack_object_header(&mut chunk, 6, delta_len);
983 encode_ofs_delta_distance(&mut chunk, dist);
984 if let Some(reuse) = reuse.as_mut() {
985 reuse.record_ofs_fixup();
986 }
987 } else {
988 encode_pack_object_header(&mut chunk, 7, delta_len);
989 chunk.extend_from_slice(base_oid.as_bytes());
990 }
991 chunk.extend_from_slice(&pack_bytes[*zlib_start..*zlib_end]);
992 if let Some(reuse) = reuse.as_mut() {
993 reuse.record_verbatim_zlib((zlib_end - zlib_start) as u64);
994 }
995 } else {
996 let computed;
997 let (delta_len, zlib): (usize, &[u8]) = match &entry.searched_delta {
998 Some((len, zlib)) => (*len, zlib),
999 None => {
1000 let target_data = odb.read(&entry.oid)?.data;
1001 let base_data = odb.read(&base_oid)?.data;
1002 let delta = if target_data.starts_with(&base_data)
1003 && target_data.len() > base_data.len()
1004 {
1005 encode_prefix_extension_delta(&base_data, &target_data)?
1006 } else {
1007 DeltaIndex::new(&base_data)
1008 .encode(&target_data, 0)
1009 .ok_or_else(|| {
1010 Error::CorruptObject(
1011 "failed to encode pack delta for object".into(),
1012 )
1013 })?
1014 };
1015 let mut buf = Vec::new();
1016 write_zlib(&mut buf, &delta, compression)?;
1017 computed = buf;
1018 (delta.len(), &computed)
1019 }
1020 };
1021 if let Some(base_off) = in_pack_offset.filter(|_| opts.use_ofs_delta) {
1022 let dist = start.checked_sub(base_off).ok_or_else(|| {
1023 Error::CorruptObject("ofs-delta distance underflow".to_owned())
1024 })?;
1025 encode_pack_object_header(&mut chunk, 6, delta_len);
1026 encode_ofs_delta_distance(&mut chunk, dist);
1027 } else {
1028 encode_pack_object_header(&mut chunk, 7, delta_len);
1029 if base_oid.as_bytes().len() != algo.len() {
1030 return Err(Error::CorruptObject(
1031 "ref-delta base oid width mismatch".to_owned(),
1032 ));
1033 }
1034 chunk.extend_from_slice(base_oid.as_bytes());
1035 }
1036 chunk.extend_from_slice(zlib);
1037 }
1038 w.write_all(&chunk).map_err(Error::Io)?;
1039 index_rows.push((entry.oid, start, crc32fast::hash(&chunk)));
1040 body_written += chunk.len() as u64;
1041 oid_to_offset.insert(entry.oid, start);
1042 }
1043 Ok(body_written)
1044}
1045
1046pub fn build_pack_from_send_list(
1052 odb: &Odb,
1053 send: &[ObjectId],
1054 have_closure: &HashSet<ObjectId>,
1055 opts: &PackBuildOptions,
1056 blob_name_hashes: &HashMap<ObjectId, u32>,
1057) -> Result<Vec<u8>> {
1058 build_pack_from_send_list_with_parallel_threads(
1059 odb,
1060 send,
1061 have_closure,
1062 opts,
1063 blob_name_hashes,
1064 None,
1065 None,
1066 )
1067}
1068
1069pub fn build_pack_with_write_threads(
1073 odb: &Odb,
1074 wants: &[ObjectId],
1075 haves: &[ObjectId],
1076 opts: &PackBuildOptions,
1077 write_threads: Option<usize>,
1078) -> Result<Vec<u8>> {
1079 build_pack_with_write_threads_full(odb, wants, haves, opts, write_threads)
1080 .map(|pack| pack.bytes)
1081}
1082
1083pub(crate) fn build_pack_with_write_threads_full(
1085 odb: &Odb,
1086 wants: &[ObjectId],
1087 haves: &[ObjectId],
1088 opts: &PackBuildOptions,
1089 write_threads: Option<usize>,
1090) -> Result<SerializedDeltaPack> {
1091 let empty_shallow = empty_object_id_hash_set();
1092 let have_closure = reachable_closure(odb, haves, empty_shallow, true, empty_shallow)?;
1093 let selection = crate::pack_object_select::enumerate_pack_objects_naming(
1094 odb,
1095 wants,
1096 haves,
1097 &crate::pack_object_select::PackEnumerateOptions {
1098 filter: None,
1099 shallow_grafts: empty_shallow,
1100 have_shallow_grafts: Some(empty_shallow),
1101 use_bitmaps: opts.use_bitmaps,
1102 exclude_objects: Some(&have_closure),
1103 peer_odb: None,
1104 have_boundary: HaveBoundary::CommitClosure,
1105 },
1106 NameTrees::for_delta_search(opts),
1107 false,
1108 &CancelSignal::never(),
1109 )?;
1110 build_pack_from_send_list_full(
1111 odb,
1112 &selection.send,
1113 &selection.have_closure,
1114 opts,
1115 &selection.name_hashes,
1116 None,
1117 write_threads,
1118 )
1119}
1120
1121pub fn build_pack_with_parallel_threads(
1123 odb: &Odb,
1124 wants: &[ObjectId],
1125 haves: &[ObjectId],
1126 opts: &PackBuildOptions,
1127 search_threads: Option<usize>,
1128 write_threads: Option<usize>,
1129) -> Result<Vec<u8>> {
1130 let empty_shallow = empty_object_id_hash_set();
1131 let have_closure = reachable_closure(odb, haves, empty_shallow, true, empty_shallow)?;
1132 let selection = crate::pack_object_select::enumerate_pack_objects_naming(
1133 odb,
1134 wants,
1135 haves,
1136 &crate::pack_object_select::PackEnumerateOptions {
1137 filter: None,
1138 shallow_grafts: empty_shallow,
1139 have_shallow_grafts: Some(empty_shallow),
1140 use_bitmaps: opts.use_bitmaps,
1141 exclude_objects: Some(&have_closure),
1142 peer_odb: None,
1143 have_boundary: HaveBoundary::CommitClosure,
1144 },
1145 NameTrees::for_delta_search(opts),
1146 false,
1147 &CancelSignal::never(),
1148 )?;
1149 build_pack_from_send_list_with_parallel_threads(
1150 odb,
1151 &selection.send,
1152 &selection.have_closure,
1153 opts,
1154 &selection.name_hashes,
1155 search_threads,
1156 write_threads,
1157 )
1158}
1159
1160pub fn build_pack_from_send_list_with_write_threads(
1162 odb: &Odb,
1163 send: &[ObjectId],
1164 have_closure: &HashSet<ObjectId>,
1165 opts: &PackBuildOptions,
1166 blob_name_hashes: &HashMap<ObjectId, u32>,
1167 write_threads: Option<usize>,
1168) -> Result<Vec<u8>> {
1169 build_pack_from_send_list_with_parallel_threads(
1170 odb,
1171 send,
1172 have_closure,
1173 opts,
1174 blob_name_hashes,
1175 None,
1176 write_threads,
1177 )
1178}
1179
1180pub fn build_pack_from_send_list_with_parallel_threads(
1182 odb: &Odb,
1183 send: &[ObjectId],
1184 have_closure: &HashSet<ObjectId>,
1185 opts: &PackBuildOptions,
1186 blob_name_hashes: &HashMap<ObjectId, u32>,
1187 search_threads: Option<usize>,
1188 write_threads: Option<usize>,
1189) -> Result<Vec<u8>> {
1190 build_pack_from_send_list_full(
1191 odb,
1192 send,
1193 have_closure,
1194 opts,
1195 blob_name_hashes,
1196 search_threads,
1197 write_threads,
1198 )
1199 .map(|pack| pack.bytes)
1200}
1201
1202pub(crate) fn build_pack_from_send_list_full(
1203 odb: &Odb,
1204 send: &[ObjectId],
1205 have_closure: &HashSet<ObjectId>,
1206 opts: &PackBuildOptions,
1207 blob_name_hashes: &HashMap<ObjectId, u32>,
1208 search_threads: Option<usize>,
1209 write_threads: Option<usize>,
1210) -> Result<SerializedDeltaPack> {
1211 if !opts.delta {
1212 let bytes = serialize_pack(odb, send, opts)?;
1213 return Ok(SerializedDeltaPack {
1214 bytes,
1215 index_rows: Vec::new(),
1216 staging_path: None,
1217 });
1218 }
1219 let plan = plan_deltas(
1220 odb,
1221 send,
1222 have_closure,
1223 None,
1224 opts,
1225 blob_name_hashes,
1226 search_threads,
1227 &CancelSignal::never(),
1228 )?;
1229 crate::pack_delta::serialize_pack_with_deltas(odb, &plan.entries, opts, write_threads)
1230}
1231
1232pub(crate) fn write_pack_from_send_list(
1238 odb: &Odb,
1239 send: &[ObjectId],
1240 opts: &PackBuildOptions,
1241 name_hashes: &HashMap<ObjectId, u32>,
1242 stop: &CancelSignal<'_>,
1243 w: &mut dyn Write,
1244 mut on_objects: Option<&mut dyn FnMut(u64)>,
1245) -> Result<PackStats> {
1246 if !opts.delta {
1247 let bytes = serialize_pack(odb, send, opts)?;
1248 stop.poll()?;
1249 w.write_all(&bytes).map_err(Error::Io)?;
1250 if let Some(on) = on_objects.as_deref_mut() {
1251 on(send.len() as u64);
1252 }
1253 return Ok(PackStats {
1254 object_count: u32::try_from(send.len()).unwrap_or(u32::MAX),
1255 pack_bytes: bytes.len() as u64,
1256 ..PackStats::default()
1257 });
1258 }
1259 odb.with_pack_read_context(|| {
1260 read_object_infos_with_progress(odb, send, None, stop, &mut on_objects)?;
1261 stop.poll()?;
1262 let plan = plan_deltas(
1263 odb,
1264 send,
1265 &HashSet::new(),
1266 None,
1267 opts,
1268 name_hashes,
1269 None,
1270 stop,
1271 )?;
1272 stop.poll()?;
1273 crate::pack_delta::write_pack_with_deltas(
1274 odb,
1275 &plan.entries,
1276 opts,
1277 None,
1278 w,
1279 None,
1280 stop,
1281 on_objects,
1282 )
1283 })
1284}
1285
1286#[allow(clippy::too_many_arguments)]
1289pub(crate) fn write_pack_for_local_fetch(
1290 source_odb: &Odb,
1291 wants: &[ObjectId],
1292 have_odb: &Odb,
1293 haves: &[ObjectId],
1294 source_shallow: &HashSet<ObjectId>,
1295 have_shallow: &HashSet<ObjectId>,
1296 opts: &PackBuildOptions,
1297 stop: &CancelSignal<'_>,
1298 w: &mut dyn Write,
1299) -> Result<PackStats> {
1300 PackObjects::new(have_odb, PackObjectsOptions::from(*opts))
1301 .with_source_odb(source_odb)
1302 .wants(wants)
1303 .haves(haves)
1304 .have_shallow_grafts(have_shallow)
1305 .source_shallow_grafts(source_shallow)
1306 .write_to_with_cancel(w, stop)
1307}
1308
1309fn collect_reachable_excluding(
1310 odb: &Odb,
1311 roots: &[ObjectId],
1312 exclude: &OidHashSet,
1313 skip_missing: bool,
1314 shallow_grafts: &OidHashSet,
1315) -> Result<Vec<ObjectId>> {
1316 let mut visited: OidHashSet = oid_hash_set();
1317 let mut ordered: Vec<ObjectId> = Vec::new();
1318 let mut queue: VecDeque<ObjectId> = VecDeque::new();
1319
1320 let enqueue = |oid: ObjectId,
1321 queue: &mut VecDeque<ObjectId>,
1322 visited: &mut OidHashSet,
1323 ordered: &mut Vec<ObjectId>|
1324 -> bool {
1325 if !visited.insert(oid) {
1326 return false;
1327 }
1328 if !exclude.contains(&oid) {
1329 ordered.push(oid);
1330 }
1331 queue.push_back(oid);
1332 true
1333 };
1334
1335 for &root in roots {
1336 enqueue(root, &mut queue, &mut visited, &mut ordered);
1337 }
1338
1339 let oid_len = odb.hash_algo().len();
1340
1341 while let Some(oid) = queue.pop_front() {
1342 let obj = match odb.read(&oid) {
1343 Ok(o) => o,
1344 Err(_) if skip_missing => continue,
1348 Err(e) => return Err(e),
1349 };
1350 match obj.kind {
1351 ObjectKind::Commit => {
1352 let commit = parse_commit(&obj.data)?;
1353 if !shallow_grafts.contains(&oid) {
1354 for parent in commit.parents {
1355 enqueue(parent, &mut queue, &mut visited, &mut ordered);
1356 }
1357 }
1358 enqueue(commit.tree, &mut queue, &mut visited, &mut ordered);
1359 }
1360 ObjectKind::Tree => {
1361 for entry in TreeRefIter::with_oid_len(&obj.data, oid_len) {
1362 let entry = entry?;
1363 if entry.mode == 0o160000 {
1366 continue;
1367 }
1368 enqueue(entry.oid, &mut queue, &mut visited, &mut ordered);
1369 }
1370 }
1371 ObjectKind::Tag => {
1372 let tag = parse_tag(&obj.data)?;
1373 enqueue(tag.object, &mut queue, &mut visited, &mut ordered);
1374 }
1375 ObjectKind::Blob => {}
1376 }
1377 }
1378
1379 Ok(ordered)
1380}
1381
1382pub(crate) fn pack_type_code(kind: ObjectKind) -> u8 {
1384 match kind {
1385 ObjectKind::Commit => 1,
1386 ObjectKind::Tree => 2,
1387 ObjectKind::Blob => 3,
1388 ObjectKind::Tag => 4,
1389 }
1390}
1391
1392pub(crate) fn encode_pack_object_header(buf: &mut Vec<u8>, type_code: u8, payload_len: usize) {
1395 let mut size = payload_len;
1396 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
1397 size >>= 4;
1398 if size > 0 {
1399 buf.push(first | 0x80);
1400 while size > 0 {
1401 let b = (size & 0x7f) as u8;
1402 size >>= 7;
1403 buf.push(if size > 0 { b | 0x80 } else { b });
1404 }
1405 } else {
1406 buf.push(first);
1407 }
1408}
1409
1410fn serialize_pack(odb: &Odb, oids: &[ObjectId], opts: &PackBuildOptions) -> Result<Vec<u8>> {
1413 let mut buf = Vec::new();
1414 buf.extend_from_slice(b"PACK");
1415 buf.extend_from_slice(&2u32.to_be_bytes());
1416 let count = u32::try_from(oids.len())
1417 .map_err(|_| Error::CorruptObject("pack object count exceeds u32".to_owned()))?;
1418 buf.extend_from_slice(&count.to_be_bytes());
1419
1420 for oid in oids {
1421 let kind = odb.read_info(oid)?.kind;
1422 write_whole_pack_object_from_odb(&mut buf, odb, *oid, kind, opts)?;
1423 }
1424
1425 append_pack_trailer(&mut buf, odb.hash_algo());
1426 Ok(buf)
1427}
1428
1429fn write_whole_pack_object_from_odb(
1431 buf: &mut Vec<u8>,
1432 odb: &Odb,
1433 oid: ObjectId,
1434 kind: ObjectKind,
1435 opts: &PackBuildOptions,
1436) -> Result<()> {
1437 write_whole_pack_object_to_odb(buf, odb, oid, kind, opts).map(|_| ())
1438}
1439
1440pub(crate) fn write_whole_pack_object_to_odb(
1443 w: &mut dyn Write,
1444 odb: &Odb,
1445 oid: ObjectId,
1446 kind: ObjectKind,
1447 opts: &PackBuildOptions,
1448) -> Result<u64> {
1449 if opts.reuse_objects && odb.hash_algo() == HashAlgo::Sha1 {
1450 if let Some((bytes, start, end)) =
1451 crate::pack::packed_full_object_range(odb.objects_dir(), &oid)?
1452 {
1453 write_all_chunked(w, &bytes[start..end])?;
1454 return Ok((end - start) as u64);
1455 }
1456 }
1457 let size = odb.read_info(&oid)?.size;
1458 let stream_from = odb.big_file_threshold()?;
1459 if kind == ObjectKind::Blob && size >= stream_from {
1460 let mut counter = ByteCounter { inner: w, n: 0 };
1461 odb.with_stream(&oid, |mut stream| {
1462 let payload_len = usize::try_from(stream.size).map_err(|_| {
1463 Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
1464 })?;
1465 encode_pack_object_header_to(&mut counter, pack_type_code(kind), payload_len)?;
1466 crate::odb::deflate_stream_to_writer(
1467 &mut stream.reader,
1468 stream.size,
1469 &mut counter,
1470 odb.pack_compression()?,
1471 )?;
1472 Ok(())
1473 })?
1474 .ok_or_else(|| Error::ObjectNotFound(oid.to_hex()))?;
1475 return Ok(counter.n);
1476 }
1477 let obj = odb.read(&oid)?;
1478 write_whole_pack_object_to(w, odb, oid, kind, &obj.data, opts)
1479}
1480
1481fn encode_pack_object_header_to(w: &mut dyn Write, type_code: u8, size: usize) -> Result<()> {
1482 let mut header = Vec::new();
1483 encode_pack_object_header(&mut header, type_code, size);
1484 w.write_all(&header).map_err(Error::Io)?;
1485 Ok(())
1486}
1487
1488fn write_whole_pack_object(
1490 buf: &mut Vec<u8>,
1491 odb: &Odb,
1492 oid: ObjectId,
1493 kind: ObjectKind,
1494 data: &[u8],
1495 opts: &PackBuildOptions,
1496) -> Result<()> {
1497 write_whole_pack_object_to(buf, odb, oid, kind, data, opts).map(|_| ())
1498}
1499
1500fn write_whole_pack_object_to(
1501 w: &mut dyn Write,
1502 odb: &Odb,
1503 oid: ObjectId,
1504 kind: ObjectKind,
1505 data: &[u8],
1506 opts: &PackBuildOptions,
1507) -> Result<u64> {
1508 let mut counter = ByteCounter { inner: w, n: 0 };
1509 if opts.reuse_objects && odb.hash_algo() == HashAlgo::Sha1 {
1510 if let Some((bytes, start, end)) =
1511 crate::pack::packed_full_object_range(odb.objects_dir(), &oid)?
1512 {
1513 write_all_chunked(&mut counter, &bytes[start..end])?;
1514 return Ok(counter.n);
1515 }
1516 }
1517 encode_pack_object_header_to(&mut counter, pack_type_code(kind), data.len())?;
1518 write_zlib_to(&mut counter, data, odb.pack_compression()?)?;
1519 Ok(counter.n)
1520}
1521
1522pub(crate) fn append_pack_trailer(buf: &mut Vec<u8>, algo: HashAlgo) {
1525 buf.extend_from_slice(&algo.checksum_digest(&*buf));
1526}
1527
1528pub(crate) fn append_whole_object_from_odb(
1532 buf: &mut Vec<u8>,
1533 odb: &Odb,
1534 oid: ObjectId,
1535) -> Result<ObjectKind> {
1536 let obj = odb.read(&oid)?;
1537 write_whole_pack_object(
1538 buf,
1539 odb,
1540 oid,
1541 obj.kind,
1542 &obj.data,
1543 &PackBuildOptions::default(),
1544 )?;
1545 Ok(obj.kind)
1546}
1547
1548pub(crate) struct PlannedEntry {
1550 pub(crate) oid: ObjectId,
1551 pub(crate) kind: ObjectKind,
1552 pub(crate) base: Option<ObjectId>,
1555 pub(crate) searched_delta: Option<(usize, Vec<u8>)>,
1559 pub(crate) reused_delta_zlib:
1562 Option<(u64, std::sync::Arc<crate::pack::PackData>, usize, usize)>,
1563}
1564
1565pub(crate) struct DeltaPlan {
1568 pub(crate) entries: Vec<PlannedEntry>,
1569}
1570
1571pub(crate) struct SerializedDeltaPack {
1573 pub bytes: Vec<u8>,
1574 pub index_rows: Vec<(ObjectId, u64, u32)>,
1575 pub staging_path: Option<std::path::PathBuf>,
1577}
1578
1579fn read_object_infos(
1581 odb: &Odb,
1582 oids: &[ObjectId],
1583 search_threads: Option<usize>,
1584) -> Result<Vec<ObjectInfo>> {
1585 let mut none: Option<&mut dyn FnMut(u64)> = None;
1586 read_object_infos_with_progress(odb, oids, search_threads, &CancelSignal::never(), &mut none)
1587}
1588
1589fn read_object_infos_with_progress(
1590 odb: &Odb,
1591 oids: &[ObjectId],
1592 search_threads: Option<usize>,
1593 stop: &CancelSignal<'_>,
1594 on_objects: &mut Option<&mut dyn FnMut(u64)>,
1595) -> Result<Vec<ObjectInfo>> {
1596 let par = crate::pack_delta::pack_delta_search_parallel_options(odb, search_threads)?;
1597 let read = |oid: &ObjectId| odb.read_info(oid);
1598 let stride = crate::progress::OBJECT_PROGRESS_STRIDE as usize;
1599 if par.threads.get() <= 1 || oids.len() < 32 {
1600 let mut out = Vec::with_capacity(oids.len());
1601 for (i, oid) in oids.iter().enumerate() {
1602 stop.poll()?;
1603 out.push(read(oid)?);
1604 if (i + 1).is_multiple_of(stride) {
1605 if let Some(on) = on_objects.as_deref_mut() {
1606 on(stride as u64);
1607 }
1608 }
1609 }
1610 let rem = oids.len() % stride;
1611 if rem != 0 {
1612 if let Some(on) = on_objects.as_deref_mut() {
1613 on(rem as u64);
1614 }
1615 }
1616 return Ok(out);
1617 }
1618 try_par_map_force(oids, par, read).map_err(|e| match e {
1619 ParallelHashError::Task(err) => err,
1620 })
1621}
1622
1623fn order_bases_first(entries: &mut [PlannedEntry]) {
1629 let n = entries.len();
1630 let bases: Vec<Option<usize>> = {
1631 let position: OidHashMap<usize> = entries
1632 .iter()
1633 .enumerate()
1634 .map(|(i, e)| (e.oid, i))
1635 .collect();
1636 entries
1637 .iter()
1638 .map(|e| e.base.and_then(|b| position.get(&b).copied()))
1639 .collect()
1640 };
1641 let mut dest = vec![usize::MAX; n];
1643 let mut next = 0usize;
1644 let mut chain = Vec::new();
1645 for i in 0..n {
1646 let mut cur = Some(i);
1647 while let Some(j) = cur {
1648 if dest[j] != usize::MAX || chain.len() > n {
1649 break;
1650 }
1651 chain.push(j);
1652 cur = bases[j];
1653 }
1654 while let Some(j) = chain.pop() {
1655 if dest[j] == usize::MAX {
1656 dest[j] = next;
1657 next += 1;
1658 }
1659 }
1660 }
1661 for i in 0..n {
1662 while dest[i] != i {
1663 let d = dest[i];
1664 entries.swap(i, d);
1665 dest.swap(i, d);
1666 }
1667 }
1668}
1669
1670pub(crate) fn objects_dir_has_local_packs(objects_dir: &Path) -> bool {
1672 let pack_dir = objects_dir.join("pack");
1673 if !pack_dir.is_dir() {
1674 return false;
1675 }
1676 std::fs::read_dir(&pack_dir)
1677 .ok()
1678 .into_iter()
1679 .flatten()
1680 .filter_map(|e| e.ok())
1681 .any(|e| e.path().extension().is_some_and(|ext| ext == "pack"))
1682}
1683
1684#[allow(clippy::too_many_arguments)]
1716pub(crate) fn plan_deltas(
1717 odb: &Odb,
1718 send: &[ObjectId],
1719 peer_commits: &HashSet<ObjectId>,
1720 peer_held: Option<&ReachableSet>,
1721 opts: &PackBuildOptions,
1722 name_hashes: &HashMap<ObjectId, u32>,
1723 search_threads: Option<usize>,
1724 stop: &CancelSignal<'_>,
1725) -> Result<DeltaPlan> {
1726 let has_local_packs = objects_dir_has_local_packs(odb.objects_dir());
1727 let reuse_on_disk = opts.reuse_deltas && odb.hash_algo() == HashAlgo::Sha1 && has_local_packs;
1728 let search = opts.window > 0 && opts.max_depth > 0;
1729 stop.poll()?;
1730 let infos = read_object_infos(odb, send, search_threads)?;
1731 stop.poll()?;
1732 let in_pack: HashSet<ObjectId> = if reuse_on_disk || opts.thin || opts.respect_islands {
1733 send.iter().copied().collect()
1734 } else {
1735 HashSet::new()
1736 };
1737
1738 let islands = load_islands_for_pack(odb, &in_pack, opts);
1742
1743 let peer_has = |oid: &ObjectId| peer_held.is_some_and(|held| held.contains(oid));
1745 let thin_bases = if opts.thin && (reuse_on_disk || search) {
1746 crate::pack_thin_bases::find_thin_bases(
1747 odb,
1748 send,
1749 &infos,
1750 &|oid| in_pack.contains(oid),
1751 &|oid| peer_commits.contains(oid) || peer_has(oid),
1752 name_hashes,
1753 opts.window.max(1),
1754 )?
1755 } else {
1756 crate::pack_thin_bases::ThinBases::default()
1757 };
1758
1759 let mut delta_to_base: OidHashMap<ObjectId> = oid_hash_map();
1761 let mut reused_zlib: HashMap<ObjectId, crate::pack::OnDiskReuseZlibEntry> = HashMap::new();
1763
1764 if reuse_on_disk {
1766 let reusable_base = |oid: &ObjectId| {
1769 in_pack.contains(oid) || (opts.thin && (thin_bases.oids.contains(oid) || peer_has(oid)))
1770 };
1771 reused_zlib = crate::pack::collect_packed_delta_reuse_for_send(
1772 odb.objects_dir(),
1773 &in_pack,
1774 &reusable_base,
1775 )?;
1776 stop.poll()?;
1777 reused_zlib.retain(|t, (base, ..)| {
1778 let far_peer_base = !in_pack.contains(base) && !thin_bases.oids.contains(base);
1781 !(far_peer_base && search && thin_bases.targets.contains(t))
1782 && *base != *t
1783 && islands.in_same_island(t, base)
1784 });
1785 delta_to_base.extend(reused_zlib.iter().map(|(t, (base, ..))| (*t, *base)));
1786 for (oid, info) in send.iter().zip(&infos) {
1788 if info.kind == ObjectKind::Blob && info.size == 0 && reused_zlib.remove(oid).is_some()
1789 {
1790 delta_to_base.remove(oid);
1791 }
1792 }
1793 }
1794
1795 let reuse_dependents: OidHashMap<usize> = {
1800 let mut longest: OidHashMap<usize> = oid_hash_map();
1801 for target in reused_zlib.keys() {
1802 let mut cur = *target;
1803 let mut depth = 0usize;
1804 while let Some(base) = delta_to_base.get(&cur) {
1805 depth += 1;
1806 cur = *base;
1807 if depth > delta_to_base.len() {
1808 break;
1809 }
1810 }
1811 let entry = longest.entry(cur).or_insert(0);
1812 *entry = (*entry).max(depth);
1813 }
1814 longest
1815 };
1816
1817 let mut searched: OidHashMap<(usize, Vec<u8>)> = oid_hash_map();
1818 if search {
1819 let found = search_new_deltas(
1820 odb,
1821 send,
1822 &infos,
1823 &thin_bases,
1824 |oid| {
1825 delta_to_base.contains_key(oid)
1826 || (thin_bases.oids.is_empty() && reuse_dependents.contains_key(oid))
1827 },
1828 &reuse_dependents,
1829 name_hashes,
1830 &islands,
1831 opts,
1832 search_threads,
1833 has_local_packs,
1834 stop.flag(),
1835 )?;
1836 stop.poll()?;
1837 for (target, base, delta) in found {
1838 delta_to_base.insert(target, base);
1839 searched.insert(target, delta);
1840 }
1841 }
1842 drop((in_pack, thin_bases));
1843
1844 if opts.max_depth > 0 && !delta_to_base.is_empty() {
1845 let mut delta_std: HashMap<ObjectId, ObjectId> = delta_to_base.drain().collect();
1850 crate::pack::cap_delta_chain_depth(&mut delta_std, opts.max_depth);
1851 delta_to_base.extend(delta_std);
1852 }
1853
1854 let mut entries: Vec<PlannedEntry> = send
1858 .iter()
1859 .zip(&infos)
1860 .map(|(&oid, info)| {
1861 let base = delta_to_base.remove(&oid);
1862 let (searched_delta, reused_delta_zlib) = match base {
1863 Some(_) => (
1864 searched.remove(&oid),
1865 reused_zlib
1866 .remove(&oid)
1867 .map(|(_, size, bytes, start, end)| (size, bytes, start, end)),
1868 ),
1869 None => (None, None),
1870 };
1871 PlannedEntry {
1872 oid,
1873 kind: info.kind,
1874 base,
1875 searched_delta,
1876 reused_delta_zlib,
1877 }
1878 })
1879 .collect();
1880 drop((infos, delta_to_base, searched, reused_zlib));
1881 order_bases_first(&mut entries);
1882 Ok(DeltaPlan { entries })
1883}
1884
1885type SearchedDelta = (ObjectId, ObjectId, (usize, Vec<u8>));
1887
1888#[allow(clippy::too_many_arguments)]
1897fn search_new_deltas(
1898 odb: &Odb,
1899 send: &[ObjectId],
1900 infos: &[ObjectInfo],
1901 thin_bases: &crate::pack_thin_bases::ThinBases,
1902 skip: impl Fn(&ObjectId) -> bool,
1903 reuse_dependents: &OidHashMap<usize>,
1904 name_hashes: &HashMap<ObjectId, u32>,
1905 islands: &crate::delta_islands::DeltaIslands,
1906 opts: &PackBuildOptions,
1907 search_threads: Option<usize>,
1908 has_local_packs: bool,
1909 stop: &std::sync::atomic::AtomicBool,
1910) -> Result<Vec<SearchedDelta>> {
1911 u32::try_from(send.len() + thin_bases.bases.len())
1912 .map_err(|_| Error::CorruptObject("too many objects for one pack".to_owned()))?;
1913 let ids: Vec<u32> = (0..send.len())
1914 .filter(|&i| infos[i].size > 0 && !skip(&send[i]))
1915 .map(|i| i as u32)
1916 .collect();
1917 let packed = if opts.reuse_deltas && has_local_packs {
1919 let oids: OidHashSet = ids.iter().map(|&i| send[i as usize]).collect();
1920 crate::pack::locate_packed_objects(odb.objects_dir(), &oids).ok()
1921 } else {
1922 None
1923 };
1924 let base_oids: Vec<ObjectId> = thin_bases.bases.iter().map(|b| b.oid).collect();
1925 let base_infos = read_object_infos(odb, &base_oids, search_threads)?;
1926 let mut candidates: Vec<crate::delta_search::DeltaCandidate> = ids
1927 .into_iter()
1928 .map(|id| {
1929 let oid = send[id as usize];
1930 let info = infos[id as usize];
1931 let slot = packed.as_ref().and_then(|p| p.get(&oid));
1932 crate::delta_search::DeltaCandidate {
1933 id,
1934 oid,
1935 kind: info.kind,
1936 size: info.size,
1937 name_hash: name_hashes.get(&oid).copied().unwrap_or(0),
1938 whole_in_pack: slot.filter(|s| s.whole).map(|s| s.pack),
1939 in_pack: slot.map(|s| s.pack),
1940 dependent_depth: reuse_dependents.get(&oid).copied().unwrap_or(0),
1941 base_only: false,
1942 }
1943 })
1944 .collect();
1945 drop(packed);
1946 candidates.extend(
1947 (send.len()..)
1948 .zip(thin_bases.bases.iter().zip(&base_infos))
1949 .filter(|(_, (base, info))| info.kind == base.kind && info.size > 0)
1950 .map(|(id, (base, info))| crate::delta_search::DeltaCandidate {
1951 id: id as u32,
1952 oid: base.oid,
1953 kind: info.kind,
1954 size: info.size,
1955 name_hash: base.name_hash,
1956 whole_in_pack: None,
1957 in_pack: None,
1958 dependent_depth: 0,
1959 base_only: true,
1960 }),
1961 );
1962 let oid_of = |id: u32| {
1963 let id = id as usize;
1964 send.get(id)
1965 .copied()
1966 .unwrap_or_else(|| base_oids[id - send.len()])
1967 };
1968 let window_memory = match opts.window_memory {
1969 Some(bytes) => bytes,
1970 None => crate::pack_delta::pack_config(odb)?.pack_window_memory_bytes(),
1971 };
1972 let threads = crate::pack_delta::pack_delta_search_parallel_options(odb, search_threads)?;
1973 let allowed = |target: &ObjectId, base: &ObjectId| {
1974 islands.in_same_island(target, base)
1975 && (!reuse_dependents.contains_key(target) || thin_bases.oids.contains(base))
1976 };
1977 let found = crate::delta_search::search_deltas(
1978 odb,
1979 candidates,
1980 crate::delta_search::DeltaSearchParams {
1981 window: opts.window,
1982 max_depth: opts.max_depth,
1983 window_memory,
1984 hash_len: odb.hash_algo().len(),
1985 compression: odb.pack_compression()?,
1986 threads: threads.threads.get(),
1987 },
1988 &allowed,
1989 stop,
1990 )?;
1991 Ok(found
1992 .into_iter()
1993 .map(|d| (oid_of(d.target), oid_of(d.base), (d.delta_len, d.zlib)))
1994 .collect())
1995}
1996
1997fn load_islands_for_pack(
2003 odb: &Odb,
2004 in_pack: &HashSet<ObjectId>,
2005 opts: &PackBuildOptions,
2006) -> crate::delta_islands::DeltaIslands {
2007 if !opts.respect_islands {
2008 return crate::delta_islands::DeltaIslands::default();
2009 }
2010 let Some(git_dir) = odb.config_git_dir() else {
2011 return crate::delta_islands::DeltaIslands::default();
2012 };
2013 let Ok(repo) = crate::repo::Repository::open(git_dir, &RepositoryOptions::empty()) else {
2014 return crate::delta_islands::DeltaIslands::default();
2015 };
2016 let env = crate::environment::Environment::empty();
2017 let cfg = crate::config::ConfigSet::load(&env, Some(git_dir), true).unwrap_or_default();
2018 crate::delta_islands::load_delta_islands(&repo, &cfg, in_pack)
2019}
2020
2021fn write_zlib(buf: &mut Vec<u8>, data: &[u8], compression: Compression) -> Result<()> {
2023 write_zlib_to(buf, data, compression)
2024}
2025
2026fn write_zlib_to(w: &mut dyn Write, data: &[u8], compression: Compression) -> Result<()> {
2027 let mut enc = ZlibEncoder::new(w, compression);
2028 enc.write_all(data).map_err(Error::Io)?;
2029 enc.finish().map_err(Error::Io)?;
2030 Ok(())
2031}
2032
2033pub(crate) fn encode_ofs_delta_distance(buf: &mut Vec<u8>, mut ofs: u64) {
2036 let mut dheader = [0u8; 32];
2037 let mut pos = dheader.len() - 1;
2038 dheader[pos] = (ofs & 0x7f) as u8;
2039 while {
2040 ofs >>= 7;
2041 ofs != 0
2042 } {
2043 pos -= 1;
2044 ofs -= 1;
2045 dheader[pos] = 0x80 | ((ofs & 0x7f) as u8);
2046 }
2047 buf.extend_from_slice(&dheader[pos..]);
2048}
2049
2050#[cfg(test)]
2051#[allow(clippy::unwrap_used, clippy::expect_used)]
2052pub(crate) mod tests {
2053 use super::*;
2054 use crate::pack_object_select::{enumerate_pack_objects, PackEnumerateOptions};
2055
2056 fn planned(n: u8, base: Option<u8>) -> PlannedEntry {
2057 let oid = |n: u8| ObjectId::from_bytes(&[n; 20]).unwrap();
2058 PlannedEntry {
2059 oid: oid(n),
2060 kind: ObjectKind::Blob,
2061 base: base.map(oid),
2062 searched_delta: None,
2063 reused_delta_zlib: None,
2064 }
2065 }
2066
2067 fn order_of(mut entries: Vec<PlannedEntry>) -> Vec<u8> {
2068 order_bases_first(&mut entries);
2069 entries.iter().map(|e| e.oid.as_bytes()[0]).collect()
2070 }
2071
2072 #[test]
2073 fn bases_are_ordered_before_their_targets() {
2074 let entries = vec![
2076 planned(1, Some(3)),
2077 planned(2, None),
2078 planned(3, Some(5)),
2079 planned(4, Some(9)),
2080 planned(5, None),
2081 planned(6, Some(2)),
2082 ];
2083 assert_eq!(order_of(entries), [5, 3, 1, 2, 4, 6]);
2084 }
2085
2086 #[test]
2087 fn base_cycles_do_not_hang_or_drop_entries() {
2088 let entries = vec![planned(1, Some(2)), planned(2, Some(1)), planned(3, None)];
2089 let mut order = order_of(entries);
2090 order.sort_unstable();
2091 assert_eq!(order, [1, 2, 3]);
2092 }
2093
2094 pub(crate) fn packed_selection(
2096 commits: usize,
2097 ) -> (
2098 tempfile::TempDir,
2099 Repository,
2100 crate::pack_object_select::PackObjectSelection,
2101 ) {
2102 let tmp = tempfile::tempdir().unwrap();
2103 let path = grit_test_support::git_packed_history(tmp.path(), commits);
2104 let tip = grit_test_support::git(&path, &["rev-parse", "main"]);
2105 let tip = ObjectId::from_hex(tip.trim()).unwrap();
2106 let repo = Repository::open(&path, &RepositoryOptions::empty()).unwrap();
2107 let empty = HashSet::new();
2108 let selection = enumerate_pack_objects(
2109 repo.odb(),
2110 &[tip],
2111 &[],
2112 &PackEnumerateOptions {
2113 filter: None,
2114 shallow_grafts: &empty,
2115 have_shallow_grafts: Some(&empty),
2116 use_bitmaps: false,
2117 exclude_objects: None,
2118 peer_odb: None,
2119 have_boundary: HaveBoundary::CommitClosure,
2120 },
2121 )
2122 .unwrap();
2123 (tmp, repo, selection)
2124 }
2125
2126 #[cfg_attr(
2127 not(feature = "full-tests"),
2128 ignore = "starts system git or is heavy; run with --features full-tests"
2129 )]
2130 #[test]
2131 fn serve_plan_reuses_on_disk_deltas() {
2132 let (_tmp, repo, selection) = packed_selection(80);
2133 let opts = PackBuildOptions::for_serve(None, false, true);
2134 let plan = plan_deltas(
2135 repo.odb(),
2136 &selection.send,
2137 &selection.have_closure,
2138 selection.peer_held.as_deref(),
2139 &opts,
2140 &selection.name_hashes,
2141 None,
2142 &CancelSignal::never(),
2143 )
2144 .unwrap();
2145
2146 let reused = plan
2147 .entries
2148 .iter()
2149 .filter(|e| e.reused_delta_zlib.is_some())
2150 .count();
2151 let trees_and_blobs = plan
2152 .entries
2153 .iter()
2154 .filter(|e| matches!(e.kind, ObjectKind::Tree | ObjectKind::Blob))
2155 .count();
2156 assert!(
2157 reused * 2 > trees_and_blobs,
2158 "most trees and blobs keep their on-disk delta ({reused} of {trees_and_blobs})"
2159 );
2160 }
2161
2162 fn loose_history(commits: usize) -> (tempfile::TempDir, Repository, ObjectId) {
2165 let tmp = tempfile::tempdir().unwrap();
2166 let dir = tmp.path();
2167 grit_test_support::git(dir, &["init", "-q", "-b", "main"]);
2168 std::fs::create_dir_all(dir.join("src")).unwrap();
2169 for i in 0..20 {
2170 std::fs::write(
2171 dir.join(format!("src/stable{i}.txt")),
2172 format!("stable {i}\n"),
2173 )
2174 .unwrap();
2175 }
2176 let mut lines: Vec<String> = (0..200).map(|i| format!("line {i} of the file")).collect();
2177 for c in 0..commits {
2178 lines[(c * 13) % 200] = format!("changed in commit {c}");
2179 std::fs::write(dir.join("src/file.txt"), lines.join("\n")).unwrap();
2180 std::fs::write(dir.join("src/other.txt"), format!("other {c}\n")).unwrap();
2181 grit_test_support::git(dir, &["add", "."]);
2182 grit_test_support::git(
2183 dir,
2184 &[
2185 "-c",
2186 "user.name=T",
2187 "-c",
2188 "user.email=t@e",
2189 "commit",
2190 "-qm",
2191 "c",
2192 ],
2193 );
2194 }
2195 let tip = grit_test_support::git(dir, &["rev-parse", "main"]);
2196 let tip = ObjectId::from_hex(tip.trim()).unwrap();
2197 let repo = Repository::open(&dir.join(".git"), &RepositoryOptions::empty()).unwrap();
2198 (tmp, repo, tip)
2199 }
2200
2201 #[cfg_attr(
2202 not(feature = "full-tests"),
2203 ignore = "starts system git or is heavy; run with --features full-tests"
2204 )]
2205 #[test]
2206 fn loose_plan_deltifies_trees_and_blobs_with_compressed_search_output() {
2207 let (tmp, repo, tip) = loose_history(12);
2208 assert!(!objects_dir_has_local_packs(repo.odb().objects_dir()));
2209 let empty = HashSet::new();
2210 let opts = PackBuildOptions::for_local_copy(None);
2211 let selection = crate::pack_object_select::enumerate_pack_objects_naming(
2212 repo.odb(),
2213 &[tip],
2214 &[],
2215 &PackEnumerateOptions {
2216 filter: None,
2217 shallow_grafts: &empty,
2218 have_shallow_grafts: Some(&empty),
2219 use_bitmaps: false,
2220 exclude_objects: None,
2221 peer_odb: None,
2222 have_boundary: HaveBoundary::CommitClosure,
2223 },
2224 NameTrees::for_delta_search(&opts),
2225 false,
2226 &CancelSignal::never(),
2227 )
2228 .unwrap();
2229 let plan = plan_deltas(
2230 repo.odb(),
2231 &selection.send,
2232 &selection.have_closure,
2233 selection.peer_held.as_deref(),
2234 &opts,
2235 &selection.name_hashes,
2236 Some(1),
2237 &CancelSignal::never(),
2238 )
2239 .unwrap();
2240 let deltas_of = |kind| {
2241 plan.entries
2242 .iter()
2243 .filter(|e| e.kind == kind && e.base.is_some())
2244 .count()
2245 };
2246 assert!(deltas_of(ObjectKind::Blob) >= 10, "blob deltas");
2247 assert!(deltas_of(ObjectKind::Tree) >= 10, "subtree deltas");
2248 assert!(plan
2249 .entries
2250 .iter()
2251 .filter(|e| e.base.is_some())
2252 .all(|e| e.searched_delta.is_some()));
2253
2254 let pack = crate::pack_delta::serialize_pack_with_deltas(
2255 repo.odb(),
2256 &plan.entries,
2257 &opts,
2258 Some(2),
2259 )
2260 .unwrap();
2261 std::fs::write(tmp.path().join("plan.pack"), &pack.bytes).unwrap();
2262 grit_test_support::git(tmp.path(), &["index-pack", "--strict", "plan.pack"]);
2263 }
2264
2265 #[test]
2266 fn serve_options_follow_pack_config() {
2267 use crate::config::{ConfigFile, ConfigScope, ConfigSet};
2268 let file = ConfigFile::parse(
2269 Path::new(".git/config"),
2270 "[pack]\n\twindow = 4\n\tdepth = 0\n",
2271 ConfigScope::Local,
2272 )
2273 .unwrap();
2274 let mut cfg = ConfigSet::new();
2275 cfg.merge(&file);
2276 let opts = PackBuildOptions::for_serve(Some(&cfg), true, false);
2277 assert_eq!((opts.window, opts.max_depth), (4, 0));
2278 assert!(opts.thin && !opts.use_ofs_delta && opts.delta);
2279 assert!(
2280 opts.reuse_deltas,
2281 "like git upload-pack, depth 0 stops new deltas but still sends on-disk deltas"
2282 );
2283
2284 let defaults = PackBuildOptions::for_serve(None, false, true);
2285 assert_eq!(
2286 (defaults.window, defaults.max_depth),
2287 (10, crate::pack::DEFAULT_PACK_DEPTH)
2288 );
2289 assert!(defaults.reuse_deltas && defaults.reuse_objects && !defaults.thin);
2290 }
2291
2292 struct PeerHoldsSentBlob {
2296 dir: tempfile::TempDir,
2297 repo: Repository,
2298 old: ObjectId,
2299 new: ObjectId,
2300 peer_objects: Vec<ObjectId>,
2302 peer_commits: HashSet<ObjectId>,
2303 send: Vec<ObjectId>,
2304 name_hashes: HashMap<ObjectId, u32>,
2305 }
2306
2307 impl PeerHoldsSentBlob {
2308 fn new() -> Self {
2309 use crate::objects::{serialize_tree, TreeEntry};
2310 use crate::pack_name_hash::pack_name_hash;
2311 let dir = tempfile::TempDir::new().unwrap();
2312 let repo =
2313 Repository::init(dir.path(), &crate::repo::InitOptions::default()).expect("init");
2314 let odb = repo.odb();
2315 let tree = |entries: &[(&str, ObjectId)]| {
2316 let entries: Vec<TreeEntry> = entries
2317 .iter()
2318 .map(|&(name, oid)| TreeEntry {
2319 mode: 0o100644,
2320 name: name.as_bytes().to_vec(),
2321 oid,
2322 })
2323 .collect();
2324 odb.write(ObjectKind::Tree, &serialize_tree(&entries))
2325 .unwrap()
2326 };
2327 let commit = |tree: ObjectId, parents: &[ObjectId]| {
2328 let parents: String = parents
2329 .iter()
2330 .map(|p| format!("parent {}\n", p.to_hex()))
2331 .collect();
2332 let body = format!(
2333 "tree {}\n{parents}author A <a@example.com> 0 +0000\ncommitter A <a@example.com> 0 +0000\n\nc\n",
2334 tree.to_hex()
2335 );
2336 odb.write(ObjectKind::Commit, body.as_bytes()).unwrap()
2337 };
2338 let lines: Vec<String> = (0..200).map(|i| format!("line {i} of f.txt")).collect();
2339 let old = odb
2340 .write(ObjectKind::Blob, lines.join("\n").as_bytes())
2341 .unwrap();
2342 let new = odb
2343 .write(
2344 ObjectKind::Blob,
2345 format!("{}\nmore\n", lines.join("\n")).as_bytes(),
2346 )
2347 .unwrap();
2348 let old_tree = tree(&[("f.txt", old)]);
2349 let new_tree = tree(&[("f.txt", new)]);
2350 let first = commit(old_tree, &[]);
2351 let second = commit(new_tree, &[]);
2352 let extra = odb.write(ObjectKind::Blob, b"extra\n").unwrap();
2353 let merge_tree = tree(&[("extra.txt", extra), ("f.txt", new)]);
2354 let merge = commit(merge_tree, &[first, second]);
2355 Self {
2356 dir,
2357 old,
2358 new,
2359 peer_objects: vec![old, new, old_tree, new_tree, first, second],
2360 peer_commits: HashSet::from([first, second]),
2361 send: vec![merge, merge_tree, new, extra],
2362 name_hashes: HashMap::from([
2363 (new, pack_name_hash("f.txt")),
2364 (extra, pack_name_hash("extra.txt")),
2365 ]),
2366 repo,
2367 }
2368 }
2369
2370 fn thin_plan(&self) -> DeltaPlan {
2371 plan_deltas(
2372 self.repo.odb(),
2373 &self.send,
2374 &self.peer_commits,
2375 None,
2376 &PackBuildOptions::for_serve(None, true, true),
2377 &self.name_hashes,
2378 Some(1),
2379 &CancelSignal::never(),
2380 )
2381 .unwrap()
2382 }
2383 }
2384
2385 #[test]
2386 fn thin_pack_never_deltas_a_sent_blob_against_itself() {
2387 let fx = PeerHoldsSentBlob::new();
2388 let plan = fx.thin_plan();
2389 assert_eq!(plan.entries.len(), fx.send.len());
2390 for entry in &plan.entries {
2391 assert_ne!(
2392 entry.base,
2393 Some(entry.oid),
2394 "a delta against itself never resolves"
2395 );
2396 }
2397 let new = plan.entries.iter().find(|e| e.oid == fx.new).unwrap();
2398 assert_eq!(new.base, Some(fx.old), "the other held version is the base");
2399 }
2400
2401 #[cfg_attr(
2402 not(feature = "full-tests"),
2403 ignore = "starts system git or is heavy; run with --features full-tests"
2404 )]
2405 #[test]
2406 fn thin_pack_with_held_sent_blob_completes_in_git() {
2407 let fx = PeerHoldsSentBlob::new();
2408 let plan = fx.thin_plan();
2409 let opts = PackBuildOptions::for_serve(None, true, true);
2410 let pack = crate::pack_delta::serialize_pack_with_deltas(
2411 fx.repo.odb(),
2412 &plan.entries,
2413 &opts,
2414 Some(1),
2415 )
2416 .unwrap();
2417
2418 let root = fx.dir.path();
2419 grit_test_support::git(root, &["init", "-q", "--bare", "peer"]);
2420 let peer = root.join("peer");
2421 for oid in &fx.peer_objects {
2422 let obj = fx.repo.odb().read(oid).unwrap();
2423 let written = grit_test_support::git_cmd(&[
2424 "hash-object",
2425 "-w",
2426 "--stdin",
2427 "-t",
2428 obj.kind.as_str(),
2429 ])
2430 .in_dir(&peer)
2431 .stdin(obj.data)
2432 .suc();
2433 assert_eq!(written.stdout.trim(), oid.to_hex());
2434 }
2435 grit_test_support::git_cmd(&["index-pack", "--strict", "--fix-thin", "--stdin"])
2436 .in_dir(&peer)
2437 .stdin(pack.bytes)
2438 .suc();
2439 let merge = fx.send[0].to_hex();
2440 grit_test_support::git(&peer, &["update-ref", "refs/heads/main", &merge]);
2441 grit_test_support::git(&peer, &["fsck", "--strict", "--no-dangling"]);
2442 }
2443}