1use flate2::Compression;
2use flate2::write::ZlibEncoder;
3use flate2::{Decompress, FlushDecompress};
4use sley_core::{
5 CancelFlag, GitError, MissingObjectContext, ObjectFormat, ObjectId, Result, StreamingDigest,
6 primitives::u32_be,
7};
8use sley_object::{Commit, EncodedObject, ObjectType, Tag, TreeEntries, tree_entry_object_type};
9use sley_pack::{
10 MultiPackIndex, PackBitmapIndex, PackBitmapWriter, PackFile, PackIndex, PackIndexEntry,
11 PackIndexViewData, PackInput, PackPlanningHints, PackWrite, PackWriteLimits, PackWriteOptions,
12 PackWriteSummary,
13};
14use std::collections::{HashMap, HashSet};
15use std::io::{self, Write};
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18use std::{env, fs};
19
20use crate::{
21 ObjectReader, ObjectWriter, ReusablePackCandidate, grafted_parents, unique_temp_path,
22 with_missing_object_context,
23};
24
25use crate::install::{
26 PackInstallResult, REACHABLE_PACK_STREAMING_MIN_OBJECTS, RawPackInstallOptions,
27 RawPackInstallResult, RawPackInstaller, ReachablePackFile, ReachablePackWriteSummary,
28};
29use crate::loose::{LooseObjectStore, collect_loose_object_ids};
30use crate::pack::{FileObjectDatabase, PackData, load_pack_data};
31use crate::registry::{read_incremental_midx_chain, repository_objects_dir};
32
33pub fn collect_reachable_object_ids<R, I>(
34 reader: &R,
35 format: ObjectFormat,
36 starts: I,
37) -> Result<HashSet<ObjectId>>
38where
39 R: ObjectReader,
40 I: IntoIterator<Item = ObjectId>,
41{
42 walk_reachable_objects(reader, format, starts, &HashSet::new(), |_, _| {})
43}
44
45pub fn collect_reachable_object_ids_tolerating_promised_missing<R, I>(
46 reader: &R,
47 format: ObjectFormat,
48 starts: I,
49) -> Result<HashSet<ObjectId>>
50where
51 R: ObjectReader,
52 I: IntoIterator<Item = ObjectId>,
53{
54 collect_reachable_object_ids_excluding_promised_missing(reader, format, starts, &HashSet::new())
55}
56
57pub fn collect_reachable_object_ids_tolerating_missing<R, I>(
58 reader: &R,
59 format: ObjectFormat,
60 starts: I,
61) -> Result<HashSet<ObjectId>>
62where
63 R: ObjectReader,
64 I: IntoIterator<Item = ObjectId>,
65{
66 walk_reachable_objects_tolerating_missing(reader, format, starts)
67}
68
69pub fn collect_reachable_object_ids_with_cut<R, I>(
74 reader: &R,
75 format: ObjectFormat,
76 starts: I,
77 cut: &HashSet<ObjectId>,
78) -> Result<HashSet<ObjectId>>
79where
80 R: ObjectReader,
81 I: IntoIterator<Item = ObjectId>,
82{
83 walk_reachable_objects_with_cut(reader, format, starts, &HashSet::new(), cut, |_, _| {})
84}
85
86pub fn collect_reachable_object_ids_excluding<R, I>(
90 reader: &R,
91 format: ObjectFormat,
92 starts: I,
93 excluded: &HashSet<ObjectId>,
94) -> Result<HashSet<ObjectId>>
95where
96 R: ObjectReader,
97 I: IntoIterator<Item = ObjectId>,
98{
99 walk_reachable_objects(reader, format, starts, excluded, |_, _| {})
100}
101
102pub(crate) fn collect_reachable_object_ids_excluding_promised_missing<R, I>(
103 reader: &R,
104 format: ObjectFormat,
105 starts: I,
106 excluded: &HashSet<ObjectId>,
107) -> Result<HashSet<ObjectId>>
108where
109 R: ObjectReader,
110 I: IntoIterator<Item = ObjectId>,
111{
112 walk_reachable_objects_excluding_promised_missing(reader, format, starts, excluded, |_, _| {})
113}
114
115fn walk_reachable_objects_excluding_promised_missing<R, I, F>(
116 reader: &R,
117 format: ObjectFormat,
118 starts: I,
119 excluded: &HashSet<ObjectId>,
120 mut visit: F,
121) -> Result<HashSet<ObjectId>>
122where
123 R: ObjectReader,
124 I: IntoIterator<Item = ObjectId>,
125 F: FnMut(&ObjectId, &Arc<EncodedObject>),
126{
127 let mut seen = HashSet::new();
128 let mut pending: Vec<ObjectId> = starts.into_iter().collect();
129 while let Some(oid) = pending.pop() {
130 if excluded.contains(&oid) || !seen.insert(oid) {
131 continue;
132 }
133 let object = match reader
134 .read_object(&oid)
135 .map_err(|err| with_missing_object_context(err, oid, MissingObjectContext::Traversal))
136 {
137 Ok(object) => object,
138 Err(GitError::NotFound(_)) if reader.is_promised_object(&oid) => continue,
139 Err(err) => return Err(err),
140 };
141 match object.object_type {
142 ObjectType::Commit => {
143 let commit = Commit::parse_ref(format, &object.body)?;
144 visit(&oid, &object);
145 pending.extend(grafted_parents(reader, &oid, commit.parents));
146 pending.push(commit.tree);
147 }
148 ObjectType::Tree => {
149 visit(&oid, &object);
150 for entry in TreeEntries::new(format, &object.body) {
151 let entry = entry?;
152 if !entry.is_gitlink() {
153 pending.push(entry.oid);
154 }
155 }
156 }
157 ObjectType::Tag => {
158 let tag = Tag::parse_ref(format, &object.body)?;
159 visit(&oid, &object);
160 pending.push(tag.object);
161 }
162 ObjectType::Blob => visit(&oid, &object),
163 }
164 }
165 Ok(seen)
166}
167
168pub fn collect_reachable_objects<R, I>(
169 reader: &R,
170 format: ObjectFormat,
171 starts: I,
172 excluded: &HashSet<ObjectId>,
173) -> Result<Vec<Arc<EncodedObject>>>
174where
175 R: ObjectReader,
176 I: IntoIterator<Item = ObjectId>,
177{
178 let mut objects = Vec::new();
179 walk_reachable_objects(reader, format, starts, excluded, |_, object| {
180 objects.push(Arc::clone(object));
181 })?;
182 Ok(objects)
183}
184
185#[derive(Debug, Clone)]
186pub(crate) struct ReachablePackObject {
187 pub(crate) oid: ObjectId,
188 pub(crate) object: Arc<EncodedObject>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub(crate) struct ReachablePackObjectMeta {
193 pub(crate) oid: ObjectId,
194 pub(crate) object_type: ObjectType,
195 pub(crate) size: u64,
196 pub(crate) name_hash: u32,
197}
198
199pub(crate) enum ReachablePackObjectsForWrite {
200 Buffered {
201 objects: Vec<ReachablePackObject>,
202 name_hashes: HashMap<ObjectId, u32>,
203 },
204 Streaming(Vec<ReachablePackObjectMeta>),
205}
206
207#[derive(Clone, Copy)]
208enum ReachablePackTraversal {
209 Natural,
210 RepackLegacy,
211}
212
213fn collect_reachable_pack_objects<R, I>(
214 reader: &R,
215 format: ObjectFormat,
216 starts: I,
217 excluded: &HashSet<ObjectId>,
218) -> Result<Vec<ReachablePackObject>>
219where
220 R: ObjectReader,
221 I: IntoIterator<Item = ObjectId>,
222{
223 let mut objects = Vec::new();
224 walk_reachable_objects(reader, format, starts, excluded, |oid, object| {
225 objects.push(ReachablePackObject {
226 oid: *oid,
227 object: Arc::clone(object),
228 });
229 })?;
230 Ok(objects)
231}
232
233fn collect_reachable_pack_objects_tolerating_promised_missing<R, I>(
234 reader: &R,
235 format: ObjectFormat,
236 starts: I,
237 excluded: &HashSet<ObjectId>,
238) -> Result<Vec<ReachablePackObject>>
239where
240 R: ObjectReader,
241 I: IntoIterator<Item = ObjectId>,
242{
243 let mut objects = Vec::new();
244 walk_reachable_objects_excluding_promised_missing(
245 reader,
246 format,
247 starts,
248 excluded,
249 |oid, object| {
250 objects.push(ReachablePackObject {
251 oid: *oid,
252 object: Arc::clone(object),
253 });
254 },
255 )?;
256 Ok(objects)
257}
258
259pub(crate) fn collect_reachable_pack_objects_for_write<R, I>(
260 reader: &R,
261 format: ObjectFormat,
262 starts: I,
263 excluded: &HashSet<ObjectId>,
264) -> Result<ReachablePackObjectsForWrite>
265where
266 R: ObjectReader,
267 I: IntoIterator<Item = ObjectId>,
268{
269 collect_reachable_pack_objects_for_write_with_order(
270 reader,
271 format,
272 starts,
273 excluded,
274 true,
275 ReachablePackTraversal::Natural,
276 )
277}
278
279fn collect_reachable_pack_objects_for_write_with_order<R, I>(
280 reader: &R,
281 format: ObjectFormat,
282 starts: I,
283 excluded: &HashSet<ObjectId>,
284 reorder: bool,
285 traversal: ReachablePackTraversal,
286) -> Result<ReachablePackObjectsForWrite>
287where
288 R: ObjectReader,
289 I: IntoIterator<Item = ObjectId>,
290{
291 let mut buffered = Some(Vec::new());
292 let mut metadata: Vec<ReachablePackObjectMeta> = Vec::new();
293 let mut metadata_positions: HashMap<ObjectId, usize> = HashMap::new();
294 let mut seen = HashSet::new();
295 let starts = starts.into_iter().collect::<Vec<_>>();
296 let mut pending = Vec::with_capacity(starts.len());
297 match traversal {
298 ReachablePackTraversal::Natural => {
299 pending.extend(starts.into_iter().rev().map(|oid| (oid, None::<Vec<u8>>)))
300 }
301 ReachablePackTraversal::RepackLegacy => {
302 pending.extend(starts.into_iter().map(|oid| (oid, None::<Vec<u8>>)));
303 }
304 }
305 while let Some((oid, path)) = pending.pop() {
306 if excluded.contains(&oid) {
307 continue;
308 }
309 let hash = path
310 .as_deref()
311 .filter(|path| !path.is_empty())
312 .map(pack_name_hash)
313 .unwrap_or(0);
314 if !seen.insert(oid) {
315 if hash != 0
316 && let Some(position) = metadata_positions.get(&oid).copied()
317 && metadata[position].name_hash == 0
318 {
319 metadata[position].name_hash = hash;
320 }
321 continue;
322 }
323 let object = reader.read_object(&oid).map_err(|err| {
324 with_missing_object_context(err, oid, MissingObjectContext::Traversal)
325 })?;
326 match object.object_type {
327 ObjectType::Commit => {
328 let commit = Commit::parse_ref(format, &object.body)?;
329 let parents = grafted_parents(reader, &oid, commit.parents);
330 match traversal {
331 ReachablePackTraversal::Natural => {
332 pending.extend(parents.into_iter().rev().map(|parent| (parent, None)))
333 }
334 ReachablePackTraversal::RepackLegacy => {
335 pending.extend(parents.into_iter().map(|parent| (parent, None)));
336 }
337 }
338 pending.push((commit.tree, Some(Vec::new())));
339 }
340 ObjectType::Tree => {
341 let prefix = path.as_deref().unwrap_or_default();
342 let mut children = Vec::new();
343 for entry in TreeEntries::new(format, &object.body) {
344 let entry = entry?;
345 if entry.is_gitlink() {
346 continue;
347 }
348 let mut child_path = Vec::with_capacity(
349 prefix.len() + usize::from(!prefix.is_empty()) + entry.name.len(),
350 );
351 child_path.extend_from_slice(prefix);
352 if !prefix.is_empty() {
353 child_path.push(b'/');
354 }
355 child_path.extend_from_slice(entry.name);
356 children.push((entry.oid, Some(child_path)));
357 }
358 pending.extend(children.into_iter().rev());
359 }
360 ObjectType::Tag => {
361 let tag = Tag::parse_ref(format, &object.body)?;
362 pending.push((tag.object, None));
363 }
364 ObjectType::Blob => {}
365 }
366 metadata_positions.insert(oid, metadata.len());
367 metadata.push(ReachablePackObjectMeta {
368 oid,
369 object_type: object.object_type,
370 size: object.body.len() as u64,
371 name_hash: hash,
372 });
373 let should_stream = buffered
374 .as_ref()
375 .is_some_and(|objects| objects.len() + 1 >= REACHABLE_PACK_STREAMING_MIN_OBJECTS);
376 if should_stream {
377 buffered = None;
378 }
379 if let Some(objects) = buffered.as_mut() {
380 objects.push(ReachablePackObject {
381 oid,
382 object: Arc::clone(&object),
383 });
384 }
385 }
386
387 match buffered {
388 Some(objects) => Ok(ReachablePackObjectsForWrite::Buffered {
389 objects,
390 name_hashes: metadata
391 .into_iter()
392 .filter(|meta| meta.name_hash != 0)
393 .map(|meta| (meta.oid, meta.name_hash))
394 .collect(),
395 }),
396 None => {
397 if reorder {
398 sort_reachable_pack_metadata(&mut metadata);
399 }
400 Ok(ReachablePackObjectsForWrite::Streaming(metadata))
401 }
402 }
403}
404
405pub(crate) fn sort_reachable_pack_metadata(metadata: &mut [ReachablePackObjectMeta]) {
406 sley_pack::sort_by_pack_planning_order(metadata, |entry| {
407 (entry.oid, entry.object_type, entry.size, entry.name_hash)
408 });
409}
410
411pub(crate) fn pack_inputs(objects: &[ReachablePackObject]) -> Vec<PackInput<'_>> {
412 objects
413 .iter()
414 .map(|entry| PackInput {
415 oid: &entry.oid,
416 object: &entry.object,
417 })
418 .collect()
419}
420
421pub fn install_reachable_pack<I>(
422 source: &impl ObjectReader,
423 destination: &impl RawPackInstaller,
424 format: ObjectFormat,
425 starts: I,
426) -> Result<Option<RawPackInstallResult>>
427where
428 I: IntoIterator<Item = ObjectId>,
429{
430 install_reachable_pack_excluding(source, destination, format, starts, &HashSet::new())
431}
432
433pub fn install_reachable_pack_excluding<I>(
434 source: &impl ObjectReader,
435 destination: &impl RawPackInstaller,
436 format: ObjectFormat,
437 starts: I,
438 excluded: &HashSet<ObjectId>,
439) -> Result<Option<RawPackInstallResult>>
440where
441 I: IntoIterator<Item = ObjectId>,
442{
443 let pack = match build_reachable_pack(source, format, starts, excluded)? {
444 Some(pack) => pack,
445 None => return Ok(None),
446 };
447 let mut reader = pack.pack.as_slice();
448 destination
449 .install_raw_pack_from_reader(&mut reader)
450 .map(Some)
451}
452
453pub fn build_reachable_pack<R, I>(
454 reader: &R,
455 format: ObjectFormat,
456 starts: I,
457 excluded: &HashSet<ObjectId>,
458) -> Result<Option<PackWrite>>
459where
460 R: ObjectReader,
461 I: IntoIterator<Item = ObjectId>,
462{
463 build_reachable_pack_with_reuse_stats(reader, format, starts, excluded)
464 .map(|build| build.map(|build| build.pack))
465}
466
467#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
474pub struct ReachablePackReuseStats {
475 pub verbatim_entries: u32,
476 pub redeltified_entries: u32,
477 pub whole_pack: bool,
478}
479
480#[derive(Debug, Clone, PartialEq, Eq)]
482pub struct ReachablePackBuild {
483 pub pack: PackWrite,
484 pub reuse: ReachablePackReuseStats,
485}
486
487#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct ReachablePackReuseWrite {
491 pub summary: ReachablePackWriteSummary,
492 pub reuse: ReachablePackReuseStats,
493}
494
495pub fn build_reachable_pack_with_reuse_stats<R, I>(
497 reader: &R,
498 format: ObjectFormat,
499 starts: I,
500 excluded: &HashSet<ObjectId>,
501) -> Result<Option<ReachablePackBuild>>
502where
503 R: ObjectReader,
504 I: IntoIterator<Item = ObjectId>,
505{
506 let objects = collect_reachable_pack_objects_for_write(reader, format, starts, excluded)?;
507 match &objects {
508 ReachablePackObjectsForWrite::Buffered { objects, .. } if objects.is_empty() => {
509 return Ok(None);
510 }
511 ReachablePackObjectsForWrite::Streaming(objects) if objects.is_empty() => return Ok(None),
512 ReachablePackObjectsForWrite::Buffered { .. }
513 | ReachablePackObjectsForWrite::Streaming(_) => {}
514 }
515 build_reachable_pack_objects_with_reuse(reader, format, objects).map(Some)
516}
517
518pub fn build_reachable_pack_file<R, I>(
519 reader: &R,
520 format: ObjectFormat,
521 starts: I,
522 excluded: &HashSet<ObjectId>,
523 pack_path: impl AsRef<Path>,
524) -> Result<Option<ReachablePackFile>>
525where
526 R: ObjectReader,
527 I: IntoIterator<Item = ObjectId>,
528{
529 let pack_path = pack_path.as_ref();
530 let parent = pack_path
531 .parent()
532 .ok_or_else(|| GitError::InvalidPath("prepared pack path has no parent".into()))?;
533 fs::create_dir_all(parent)?;
534 let temp_path = unique_temp_path(parent).with_extension("pack");
535 let result = (|| -> Result<Option<ReachablePackFile>> {
536 let mut file = fs::OpenOptions::new()
537 .write(true)
538 .create_new(true)
539 .open(&temp_path)?;
540 let summary = write_reachable_pack_to_writer(reader, format, starts, excluded, &mut file)?;
541 let Some(summary) = summary else {
542 return Ok(None);
543 };
544 file.sync_all()?;
545 drop(file);
546 fs::rename(&temp_path, pack_path)?;
547 sync_directory(parent)?;
548 Ok(Some(ReachablePackFile {
549 pack_path: pack_path.to_path_buf(),
550 pack_size: summary.pack_size,
551 checksum: summary.checksum,
552 object_count: summary.object_count,
553 delta_count: summary.delta_count,
554 }))
555 })();
556 if result.is_err() || matches!(result.as_ref(), Ok(None)) {
557 let _ = fs::remove_file(&temp_path);
558 }
559 result
560}
561
562fn sync_directory(path: &Path) -> Result<()> {
563 #[cfg(unix)]
564 fs::File::open(path)?.sync_all()?;
565 #[cfg(not(unix))]
566 let _ = path;
567 Ok(())
568}
569
570pub fn write_reachable_pack_to_writer<R, I, W>(
571 reader: &R,
572 format: ObjectFormat,
573 starts: I,
574 excluded: &HashSet<ObjectId>,
575 writer: &mut W,
576) -> Result<Option<ReachablePackWriteSummary>>
577where
578 R: ObjectReader,
579 I: IntoIterator<Item = ObjectId>,
580 W: Write,
581{
582 write_reachable_pack_to_writer_with_options_and_cancel(
583 reader,
584 format,
585 starts,
586 excluded,
587 &PackWriteOptions::new(),
588 writer,
589 CancelFlag::never(),
590 )
591}
592
593pub fn write_reachable_pack_to_writer_with_cancel<R, I, W>(
596 reader: &R,
597 format: ObjectFormat,
598 starts: I,
599 excluded: &HashSet<ObjectId>,
600 writer: &mut W,
601 cancel: CancelFlag<'_>,
602) -> Result<Option<ReachablePackWriteSummary>>
603where
604 R: ObjectReader,
605 I: IntoIterator<Item = ObjectId>,
606 W: Write,
607{
608 write_reachable_pack_to_writer_with_options_and_cancel(
609 reader,
610 format,
611 starts,
612 excluded,
613 &PackWriteOptions::new(),
614 writer,
615 cancel,
616 )
617}
618
619#[allow(clippy::too_many_arguments)]
621pub(crate) fn write_reachable_pack_to_writer_with_options_and_cancel<R, I, W>(
622 reader: &R,
623 format: ObjectFormat,
624 starts: I,
625 excluded: &HashSet<ObjectId>,
626 options: &PackWriteOptions,
627 writer: &mut W,
628 cancel: CancelFlag<'_>,
629) -> Result<Option<ReachablePackWriteSummary>>
630where
631 R: ObjectReader,
632 I: IntoIterator<Item = ObjectId>,
633 W: Write,
634{
635 write_reachable_pack_to_writer_with_traversal(
636 reader,
637 format,
638 starts,
639 excluded,
640 options,
641 writer,
642 cancel,
643 ReachablePackTraversal::Natural,
644 )
645}
646
647pub(crate) fn write_repack_reachable_pack_to_writer_with_options<R, I, W>(
651 reader: &R,
652 format: ObjectFormat,
653 starts: I,
654 excluded: &HashSet<ObjectId>,
655 options: &PackWriteOptions,
656 writer: &mut W,
657) -> Result<Option<ReachablePackWriteSummary>>
658where
659 R: ObjectReader,
660 I: IntoIterator<Item = ObjectId>,
661 W: Write,
662{
663 write_reachable_pack_to_writer_with_traversal(
664 reader,
665 format,
666 starts,
667 excluded,
668 options,
669 writer,
670 CancelFlag::never(),
671 ReachablePackTraversal::RepackLegacy,
672 )
673}
674
675#[allow(clippy::too_many_arguments)]
676fn write_reachable_pack_to_writer_with_traversal<R, I, W>(
677 reader: &R,
678 format: ObjectFormat,
679 starts: I,
680 excluded: &HashSet<ObjectId>,
681 options: &PackWriteOptions,
682 writer: &mut W,
683 cancel: CancelFlag<'_>,
684 traversal: ReachablePackTraversal,
685) -> Result<Option<ReachablePackWriteSummary>>
686where
687 R: ObjectReader,
688 I: IntoIterator<Item = ObjectId>,
689 W: Write,
690{
691 match collect_reachable_pack_objects_for_write_with_order(
692 reader,
693 format,
694 starts,
695 excluded,
696 options.reorder,
697 traversal,
698 )? {
699 ReachablePackObjectsForWrite::Buffered {
700 objects,
701 name_hashes,
702 } => {
703 if objects.is_empty() {
704 return Ok(None);
705 }
706 cancel.check()?;
707 let inputs = pack_inputs(&objects);
708 let summary = PackFile::write_packed_with_known_ids_and_options_and_hints_to_writer(
709 &inputs,
710 format,
711 options,
712 PackPlanningHints::new().with_name_hashes(&name_hashes),
713 writer,
714 )?;
715 Ok(Some(reachable_pack_write_summary(summary)))
716 }
717 ReachablePackObjectsForWrite::Streaming(metadata) => {
718 if metadata.is_empty() {
719 return Ok(None);
720 }
721 let object_count = u32::try_from(metadata.len())
722 .map_err(|_| GitError::InvalidFormat("too many pack objects".into()))?;
723 let name_hashes = metadata
724 .iter()
725 .filter(|meta| meta.name_hash != 0)
726 .map(|meta| (meta.oid, meta.name_hash))
727 .collect::<HashMap<_, _>>();
728 let streaming_options = options.clone().with_reorder(false);
729 let summary = PackFile::write_packed_from_source_to_writer_with_hints_and_cancel(
730 metadata.iter().map(|meta| meta.oid),
731 object_count,
732 format,
733 &streaming_options,
734 PackPlanningHints::new().with_name_hashes(&name_hashes),
735 PackWriteLimits::default(),
736 |oid| reader.read_object(oid),
737 writer,
738 cancel,
739 )?;
740 Ok(Some(reachable_pack_write_summary(summary)))
741 }
742 }
743}
744
745pub fn write_object_id_pack_to_writer<R, I, W>(
746 reader: &R,
747 format: ObjectFormat,
748 selected_objects: I,
749 object_count: u32,
750 writer: &mut W,
751) -> Result<ReachablePackWriteSummary>
752where
753 R: ObjectReader,
754 I: IntoIterator<Item = ObjectId>,
755 W: Write,
756{
757 write_object_id_pack_to_writer_with_cancel(
758 reader,
759 format,
760 selected_objects,
761 object_count,
762 writer,
763 CancelFlag::never(),
764 )
765}
766
767pub fn write_object_id_pack_to_writer_with_cancel<R, I, W>(
770 reader: &R,
771 format: ObjectFormat,
772 selected_objects: I,
773 object_count: u32,
774 writer: &mut W,
775 cancel: CancelFlag<'_>,
776) -> Result<ReachablePackWriteSummary>
777where
778 R: ObjectReader,
779 I: IntoIterator<Item = ObjectId>,
780 W: Write,
781{
782 let summary = PackFile::write_packed_from_source_to_writer_with_cancel(
783 selected_objects,
784 object_count,
785 format,
786 &PackWriteOptions::new(),
787 PackWriteLimits::default(),
788 |oid| reader.read_object(oid),
789 writer,
790 cancel,
791 )?;
792 Ok(reachable_pack_write_summary(summary))
793}
794
795fn reachable_pack_write_summary(summary: PackWriteSummary) -> ReachablePackWriteSummary {
796 ReachablePackWriteSummary {
797 index: summary.index,
798 checksum: summary.checksum,
799 object_count: summary.entries.len(),
800 delta_count: summary.delta_count,
801 pack_size: summary.pack_size,
802 }
803}
804
805pub fn build_and_install_reachable_pack<R, I>(
806 source: &R,
807 destination: &FileObjectDatabase,
808 format: ObjectFormat,
809 starts: I,
810 excluded: &HashSet<ObjectId>,
811 options: RawPackInstallOptions,
812) -> Result<Option<PackInstallResult>>
813where
814 R: ObjectReader,
815 I: IntoIterator<Item = ObjectId>,
816{
817 build_and_install_reachable_pack_filtered(
818 source,
819 destination,
820 format,
821 starts,
822 excluded,
823 options,
824 None,
825 None,
826 )
827}
828
829#[derive(Debug, Clone, PartialEq, Eq)]
836pub enum PackObjectFilter {
837 BlobNone,
839 BlobLimit(u64),
841 TreeDepth(u32),
843 SparsePathSet(Vec<String>),
845}
846
847#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
849pub enum ReachablePackMissingPolicy {
850 #[default]
852 RequireComplete,
853 OmitPromised,
855}
856
857#[derive(Debug, Clone, Copy, Default)]
866pub struct ReachablePackThinBaseCandidates<'a> {
867 pub object_ids: Option<&'a HashSet<ObjectId>>,
868}
869
870impl<'a> ReachablePackThinBaseCandidates<'a> {
871 pub fn from_object_ids(object_ids: &'a HashSet<ObjectId>) -> Self {
872 Self {
873 object_ids: Some(object_ids),
874 }
875 }
876}
877
878#[allow(clippy::too_many_arguments)]
882pub(crate) fn retain_filtered_pack_objects<R>(
887 objects: &mut Vec<ReachablePackObject>,
888 filter: Option<&PackObjectFilter>,
889 wanted: &HashSet<ObjectId>,
890 source: &R,
891 format: ObjectFormat,
892) -> Result<()>
893where
894 R: ObjectReader,
895{
896 match filter {
897 Some(PackObjectFilter::BlobNone) => {
898 objects.retain(|entry| {
899 entry.object.object_type != ObjectType::Blob || wanted.contains(&entry.oid)
900 });
901 }
902 Some(PackObjectFilter::BlobLimit(limit)) => {
903 let limit = *limit;
904 objects.retain(|entry| {
905 entry.object.object_type != ObjectType::Blob
906 || wanted.contains(&entry.oid)
907 || (entry.object.body.len() as u64) < limit
908 });
909 }
910 Some(PackObjectFilter::TreeDepth(depth)) => {
911 let depth = *depth;
912 let tree_depths = collect_tree_filter_depths(source, format, objects)?;
913 objects.retain(|entry| {
914 if wanted.contains(&entry.oid) {
915 return true;
916 }
917 match entry.object.object_type {
918 ObjectType::Blob => false,
919 ObjectType::Tree => tree_depths
920 .get(&entry.oid)
921 .is_some_and(|tree_depth| *tree_depth < depth),
922 _ => true,
923 }
924 });
925 }
926 Some(PackObjectFilter::SparsePathSet(paths)) => {
927 let allowed_blobs = collect_sparse_filter_blobs(source, format, objects, paths)?;
928 objects.retain(|entry| {
929 entry.object.object_type != ObjectType::Blob
930 || wanted.contains(&entry.oid)
931 || allowed_blobs.contains(&entry.oid)
932 });
933 }
934 None => {}
935 }
936 Ok(())
937}
938
939pub fn build_reachable_pack_filtered<R, I>(
945 reader: &R,
946 format: ObjectFormat,
947 starts: I,
948 excluded: &HashSet<ObjectId>,
949 filter: Option<PackObjectFilter>,
950) -> Result<Option<PackWrite>>
951where
952 R: ObjectReader,
953 I: IntoIterator<Item = ObjectId>,
954{
955 build_reachable_pack_filtered_with_reuse_stats(reader, format, starts, excluded, filter)
956 .map(|build| build.map(|build| build.pack))
957}
958
959pub fn build_reachable_pack_filtered_with_reuse_stats<R, I>(
961 reader: &R,
962 format: ObjectFormat,
963 starts: I,
964 excluded: &HashSet<ObjectId>,
965 filter: Option<PackObjectFilter>,
966) -> Result<Option<ReachablePackBuild>>
967where
968 R: ObjectReader,
969 I: IntoIterator<Item = ObjectId>,
970{
971 let starts: Vec<ObjectId> = starts.into_iter().collect();
972 let wanted: HashSet<ObjectId> = starts.iter().copied().collect();
973 let mut objects = collect_reachable_pack_objects(reader, format, starts, excluded)?;
974 retain_filtered_pack_objects(&mut objects, filter.as_ref(), &wanted, reader, format)?;
975 if objects.is_empty() {
976 return Ok(None);
977 }
978 build_reachable_pack_objects_with_reuse(
979 reader,
980 format,
981 ReachablePackObjectsForWrite::Buffered {
982 objects,
983 name_hashes: HashMap::new(),
984 },
985 )
986 .map(Some)
987}
988
989#[derive(Debug)]
990struct ValidatedReusablePack {
991 pack: Arc<ReusablePackData>,
992 entries: Vec<ValidatedReusableEntry>,
993 oids: HashSet<ObjectId>,
994 checksum: ObjectId,
995 self_contained: bool,
996}
997
998#[derive(Debug, Clone)]
999struct ValidatedReusableEntry {
1000 oid: ObjectId,
1001 start: usize,
1002 end: usize,
1003 payload_start: usize,
1004 kind: u8,
1005 base_oid: Option<ObjectId>,
1006 crc32: u32,
1007}
1008
1009#[derive(Debug)]
1010struct SelectedReusableEntry {
1011 pack: Arc<ReusablePackData>,
1012 entry: ValidatedReusableEntry,
1013}
1014
1015#[derive(Debug)]
1016enum ReusablePackData {
1017 Bytes(Arc<[u8]>),
1018 File(Arc<PackData>),
1019}
1020
1021impl std::ops::Deref for ReusablePackData {
1022 type Target = [u8];
1023
1024 fn deref(&self) -> &[u8] {
1025 match self {
1026 Self::Bytes(bytes) => bytes,
1027 Self::File(data) => data,
1028 }
1029 }
1030}
1031
1032fn build_reachable_pack_objects_with_reuse<R: ObjectReader>(
1033 reader: &R,
1034 format: ObjectFormat,
1035 objects: ReachablePackObjectsForWrite,
1036) -> Result<ReachablePackBuild> {
1037 let selected: HashSet<ObjectId> = match &objects {
1038 ReachablePackObjectsForWrite::Buffered { objects, .. } => {
1039 objects.iter().map(|entry| entry.oid).collect()
1040 }
1041 ReachablePackObjectsForWrite::Streaming(objects) => {
1042 objects.iter().map(|entry| entry.oid).collect()
1043 }
1044 };
1045 let candidates = reader.reusable_pack_candidates(&selected)?;
1046 let mut validated = Vec::with_capacity(candidates.len());
1047 for candidate in candidates {
1048 if let Some(candidate) = validate_reusable_pack_candidate(format, candidate)? {
1049 validated.push(candidate);
1050 }
1051 }
1052
1053 for candidate in &validated {
1057 if candidate.self_contained && candidate.oids == selected {
1058 let entries = candidate
1059 .entries
1060 .iter()
1061 .map(|entry| PackIndexEntry {
1062 oid: entry.oid,
1063 crc32: entry.crc32,
1064 offset: entry.start as u64,
1065 })
1066 .collect::<Vec<_>>();
1067 let index = PackIndex::write_v2(format, &entries, &candidate.checksum)?;
1068 let delta_count = candidate
1069 .entries
1070 .iter()
1071 .filter(|entry| matches!(entry.kind, 6 | 7))
1072 .count() as u32;
1073 let reuse = ReachablePackReuseStats {
1074 verbatim_entries: entries.len() as u32,
1075 redeltified_entries: 0,
1076 whole_pack: true,
1077 };
1078 trace_reachable_pack_reuse(reuse);
1079 return Ok(ReachablePackBuild {
1080 pack: PackWrite {
1081 pack: candidate.pack.to_vec(),
1082 index,
1083 checksum: candidate.checksum,
1084 entries,
1085 delta_count,
1086 },
1087 reuse,
1088 });
1089 }
1090 }
1091
1092 let mut reused_oids = HashSet::new();
1097 let mut reused_entries = Vec::new();
1098 for candidate in validated {
1099 for entry in candidate.entries {
1100 if !selected.contains(&entry.oid)
1101 || entry
1102 .base_oid
1103 .is_some_and(|base_oid| !selected.contains(&base_oid))
1104 || !reused_oids.insert(entry.oid)
1105 {
1106 continue;
1107 }
1108 reused_entries.push(SelectedReusableEntry {
1109 pack: Arc::clone(&candidate.pack),
1110 entry,
1111 });
1112 }
1113 }
1114
1115 let fresh_oids = match &objects {
1116 ReachablePackObjectsForWrite::Buffered { objects, .. } => objects
1117 .iter()
1118 .filter_map(|entry| (!reused_oids.contains(&entry.oid)).then_some(entry.oid))
1119 .collect::<Vec<_>>(),
1120 ReachablePackObjectsForWrite::Streaming(objects) => objects
1121 .iter()
1122 .filter_map(|entry| (!reused_oids.contains(&entry.oid)).then_some(entry.oid))
1123 .collect::<Vec<_>>(),
1124 };
1125 let fresh_pack = if fresh_oids.is_empty() {
1126 None
1127 } else {
1128 match &objects {
1129 ReachablePackObjectsForWrite::Buffered {
1130 objects,
1131 name_hashes,
1132 } => {
1133 let inputs = objects
1134 .iter()
1135 .filter(|entry| !reused_oids.contains(&entry.oid))
1136 .map(|entry| PackInput {
1137 oid: &entry.oid,
1138 object: &entry.object,
1139 })
1140 .collect::<Vec<_>>();
1141 Some(PackFile::write_packed_with_known_ids_and_options_and_hints(
1142 &inputs,
1143 format,
1144 &PackWriteOptions::new(),
1145 PackPlanningHints::new().with_name_hashes(name_hashes),
1146 )?)
1147 }
1148 ReachablePackObjectsForWrite::Streaming(_) => {
1149 let mut pack = Vec::new();
1150 let object_count = u32::try_from(fresh_oids.len())
1151 .map_err(|_| GitError::InvalidFormat("too many pack objects".into()))?;
1152 let summary = PackFile::write_packed_from_source_to_writer(
1153 fresh_oids.iter().copied(),
1154 object_count,
1155 format,
1156 &PackWriteOptions::new(),
1157 PackWriteLimits::default(),
1158 |oid| reader.read_object(oid),
1159 &mut pack,
1160 )?;
1161 Some(PackWrite {
1162 pack,
1163 index: summary.index,
1164 checksum: summary.checksum,
1165 entries: summary.entries,
1166 delta_count: summary.delta_count,
1167 })
1168 }
1169 }
1170 };
1171 let pack = assemble_generated_and_reused_entries(format, fresh_pack, &reused_entries)?;
1172 let reuse = ReachablePackReuseStats {
1173 verbatim_entries: u32::try_from(reused_entries.len())
1174 .map_err(|_| GitError::InvalidFormat("too many reusable pack entries".into()))?,
1175 redeltified_entries: u32::try_from(fresh_oids.len())
1176 .map_err(|_| GitError::InvalidFormat("too many generated pack entries".into()))?,
1177 whole_pack: false,
1178 };
1179 trace_reachable_pack_reuse(reuse);
1180 Ok(ReachablePackBuild { pack, reuse })
1181}
1182
1183pub fn write_reachable_pack_with_reuse_stats_to_writer<R, I, W>(
1188 reader: &R,
1189 format: ObjectFormat,
1190 starts: I,
1191 excluded: &HashSet<ObjectId>,
1192 writer: &mut W,
1193) -> Result<Option<ReachablePackReuseWrite>>
1194where
1195 R: ObjectReader,
1196 I: IntoIterator<Item = ObjectId>,
1197 W: Write,
1198{
1199 let objects = collect_reachable_pack_objects_for_write(reader, format, starts, excluded)?;
1200 let selected: HashSet<ObjectId> = match &objects {
1201 ReachablePackObjectsForWrite::Buffered { objects, .. } => {
1202 objects.iter().map(|entry| entry.oid).collect()
1203 }
1204 ReachablePackObjectsForWrite::Streaming(objects) => {
1205 objects.iter().map(|entry| entry.oid).collect()
1206 }
1207 };
1208 if selected.is_empty() {
1209 return Ok(None);
1210 }
1211
1212 let candidates = reader.reusable_pack_candidates(&selected)?;
1213 let mut validated = Vec::with_capacity(candidates.len());
1214 for candidate in candidates {
1215 if let Some(candidate) = validate_reusable_pack_candidate(format, candidate)? {
1216 validated.push(candidate);
1217 }
1218 }
1219
1220 for candidate in &validated {
1221 if candidate.self_contained && candidate.oids == selected {
1222 writer.write_all(&candidate.pack)?;
1223 let entries = candidate
1224 .entries
1225 .iter()
1226 .map(|entry| PackIndexEntry {
1227 oid: entry.oid,
1228 crc32: entry.crc32,
1229 offset: entry.start as u64,
1230 })
1231 .collect::<Vec<_>>();
1232 let index = PackIndex::write_v2(format, &entries, &candidate.checksum)?;
1233 let delta_count = candidate
1234 .entries
1235 .iter()
1236 .filter(|entry| matches!(entry.kind, 6 | 7))
1237 .count() as u32;
1238 let reuse = ReachablePackReuseStats {
1239 verbatim_entries: entries.len() as u32,
1240 redeltified_entries: 0,
1241 whole_pack: true,
1242 };
1243 trace_reachable_pack_reuse(reuse);
1244 return Ok(Some(ReachablePackReuseWrite {
1245 summary: ReachablePackWriteSummary {
1246 index,
1247 checksum: candidate.checksum,
1248 object_count: entries.len(),
1249 delta_count,
1250 pack_size: candidate.pack.len() as u64,
1251 },
1252 reuse,
1253 }));
1254 }
1255 }
1256
1257 let mut reused_oids = HashSet::new();
1258 let mut reused_entries = Vec::new();
1259 for candidate in validated {
1260 for entry in candidate.entries {
1261 if !selected.contains(&entry.oid)
1262 || entry
1263 .base_oid
1264 .is_some_and(|base_oid| !selected.contains(&base_oid))
1265 || !reused_oids.insert(entry.oid)
1266 {
1267 continue;
1268 }
1269 reused_entries.push(SelectedReusableEntry {
1270 pack: Arc::clone(&candidate.pack),
1271 entry,
1272 });
1273 }
1274 }
1275
1276 let fresh_oids = match &objects {
1277 ReachablePackObjectsForWrite::Buffered { objects, .. } => objects
1278 .iter()
1279 .filter_map(|entry| (!reused_oids.contains(&entry.oid)).then_some(entry.oid))
1280 .collect::<Vec<_>>(),
1281 ReachablePackObjectsForWrite::Streaming(objects) => objects
1282 .iter()
1283 .filter_map(|entry| (!reused_oids.contains(&entry.oid)).then_some(entry.oid))
1284 .collect::<Vec<_>>(),
1285 };
1286 let object_count = fresh_oids
1287 .len()
1288 .checked_add(reused_entries.len())
1289 .ok_or_else(|| GitError::InvalidFormat("too many pack objects".into()))?;
1290 let declared_count = u32::try_from(object_count)
1291 .map_err(|_| GitError::InvalidFormat("too many pack objects".into()))?;
1292
1293 let mut output = ReachablePackDigestWriter::new(writer, format);
1294 output.write_pack_bytes(b"PACK")?;
1295 output.write_pack_bytes(&2u32.to_be_bytes())?;
1296 output.write_pack_bytes(&declared_count.to_be_bytes())?;
1297
1298 let mut entries = Vec::with_capacity(object_count);
1299 let mut delta_count = 0u32;
1300 if !fresh_oids.is_empty() {
1301 let mut fresh_body = GeneratedPackBodyWriter::new(&mut output, format.raw_len());
1302 let fresh_count = u32::try_from(fresh_oids.len())
1303 .map_err(|_| GitError::InvalidFormat("too many pack objects".into()))?;
1304 let fresh = PackFile::write_packed_from_source_to_writer(
1305 fresh_oids.iter().copied(),
1306 fresh_count,
1307 format,
1308 &PackWriteOptions::new(),
1309 PackWriteLimits::default(),
1310 |oid| reader.read_object(oid),
1311 &mut fresh_body,
1312 )?;
1313 fresh_body.finish(&fresh.checksum, fresh_oids.len())?;
1314 delta_count = fresh.delta_count;
1315 entries.extend(fresh.entries);
1316 }
1317
1318 for reused in &reused_entries {
1319 let offset = output.position();
1320 let encoded = encode_selected_reusable_entry(reused)?;
1321 let crc32 = crc32fast::hash(&encoded);
1322 output.write_pack_bytes(&encoded)?;
1323 entries.push(PackIndexEntry {
1324 oid: reused.entry.oid,
1325 crc32,
1326 offset,
1327 });
1328 if matches!(reused.entry.kind, 6 | 7) {
1329 delta_count = delta_count
1330 .checked_add(1)
1331 .ok_or_else(|| GitError::InvalidFormat("too many pack deltas".into()))?;
1332 }
1333 }
1334
1335 let (checksum, pack_size) = output.finish()?;
1336 let index = PackIndex::write_v2(format, &entries, &checksum)?;
1337 let reuse = ReachablePackReuseStats {
1338 verbatim_entries: u32::try_from(reused_entries.len())
1339 .map_err(|_| GitError::InvalidFormat("too many reusable pack entries".into()))?,
1340 redeltified_entries: u32::try_from(fresh_oids.len())
1341 .map_err(|_| GitError::InvalidFormat("too many generated pack entries".into()))?,
1342 whole_pack: false,
1343 };
1344 trace_reachable_pack_reuse(reuse);
1345 Ok(Some(ReachablePackReuseWrite {
1346 summary: ReachablePackWriteSummary {
1347 index,
1348 checksum,
1349 object_count,
1350 delta_count,
1351 pack_size,
1352 },
1353 reuse,
1354 }))
1355}
1356
1357fn validate_reusable_pack_candidate(
1358 format: ObjectFormat,
1359 candidate: ReusablePackCandidate,
1360) -> Result<Option<ValidatedReusablePack>> {
1361 let pack = if let Some(path) = candidate.pack_path {
1362 let Ok(data) = load_pack_data(&path) else {
1363 return Ok(None);
1364 };
1365 Arc::new(ReusablePackData::File(Arc::new(data)))
1366 } else {
1367 Arc::new(ReusablePackData::Bytes(candidate.pack))
1368 };
1369 let hash_len = format.raw_len();
1370 if pack.len() < 12 + hash_len
1371 || &pack[..4] != b"PACK"
1372 || candidate.pack_checksum.format() != format
1373 {
1374 return Ok(None);
1375 }
1376 let version = u32::from_be_bytes([pack[4], pack[5], pack[6], pack[7]]);
1377 if !matches!(version, 2 | 3) {
1378 return Ok(None);
1379 }
1380 let declared_count = u32::from_be_bytes([pack[8], pack[9], pack[10], pack[11]]) as usize;
1381 if declared_count != candidate.entries.len() {
1382 return Ok(None);
1383 }
1384 let trailer_offset = pack.len() - hash_len;
1385 let trailer = ObjectId::from_raw(format, &pack[trailer_offset..])?;
1386 let actual = sley_core::digest_bytes(format, &pack[..trailer_offset])?;
1387 if actual != trailer || trailer != candidate.pack_checksum {
1388 return Ok(None);
1389 }
1390
1391 let mut by_offset = candidate.entries;
1392 by_offset.sort_by_key(|entry| entry.offset);
1393 let mut oids = HashSet::with_capacity(by_offset.len());
1394 let mut offset_oids = HashMap::with_capacity(by_offset.len());
1395 for entry in &by_offset {
1396 if entry.oid.format() != format
1397 || !oids.insert(entry.oid)
1398 || offset_oids.insert(entry.offset, entry.oid).is_some()
1399 {
1400 return Ok(None);
1401 }
1402 }
1403
1404 let mut entries = Vec::with_capacity(by_offset.len());
1405 for (idx, index_entry) in by_offset.iter().enumerate() {
1406 let Ok(start) = usize::try_from(index_entry.offset) else {
1407 return Ok(None);
1408 };
1409 let end = match by_offset.get(idx + 1) {
1410 Some(next) => match usize::try_from(next.offset) {
1411 Ok(offset) => offset,
1412 Err(_) => return Ok(None),
1413 },
1414 None => trailer_offset,
1415 };
1416 if start < 12 || start >= end || end > trailer_offset {
1417 return Ok(None);
1418 }
1419 let raw = &pack[start..end];
1420 let Some((kind, payload_start, base_oid)) =
1421 reusable_entry_header(format, raw, index_entry.offset, &offset_oids)?
1422 else {
1423 return Ok(None);
1424 };
1425 entries.push(ValidatedReusableEntry {
1426 oid: index_entry.oid,
1427 start,
1428 end,
1429 payload_start,
1430 kind,
1431 base_oid,
1432 crc32: crc32fast::hash(raw),
1433 });
1434 }
1435 let self_contained = entries.iter().all(|entry| {
1436 entry
1437 .base_oid
1438 .is_none_or(|base_oid| oids.contains(&base_oid))
1439 });
1440 Ok(Some(ValidatedReusablePack {
1441 pack,
1442 entries,
1443 oids,
1444 checksum: trailer,
1445 self_contained,
1446 }))
1447}
1448
1449fn reusable_entry_header(
1450 format: ObjectFormat,
1451 raw: &[u8],
1452 entry_offset: u64,
1453 offset_oids: &HashMap<u64, ObjectId>,
1454) -> Result<Option<(u8, usize, Option<ObjectId>)>> {
1455 let Some(first) = raw.first().copied() else {
1456 return Ok(None);
1457 };
1458 let kind = (first >> 4) & 0x07;
1459 if !matches!(kind, 1 | 2 | 3 | 4 | 6 | 7) {
1460 return Ok(None);
1461 }
1462 let mut cursor = 1usize;
1463 let mut byte = first;
1464 while byte & 0x80 != 0 {
1465 let Some(next) = raw.get(cursor).copied() else {
1466 return Ok(None);
1467 };
1468 cursor += 1;
1469 byte = next;
1470 }
1471 let base_oid = match kind {
1472 6 => {
1473 let Some(first_offset) = raw.get(cursor).copied() else {
1474 return Ok(None);
1475 };
1476 cursor += 1;
1477 let mut byte = first_offset;
1478 let mut relative = u64::from(byte & 0x7f);
1479 while byte & 0x80 != 0 {
1480 let Some(next) = raw.get(cursor).copied() else {
1481 return Ok(None);
1482 };
1483 cursor += 1;
1484 byte = next;
1485 let Some(next_relative) = relative
1486 .checked_add(1)
1487 .and_then(|value| value.checked_shl(7))
1488 .and_then(|value| value.checked_add(u64::from(byte & 0x7f)))
1489 else {
1490 return Ok(None);
1491 };
1492 relative = next_relative;
1493 }
1494 let Some(base_offset) = entry_offset.checked_sub(relative) else {
1495 return Ok(None);
1496 };
1497 let Some(base_oid) = offset_oids.get(&base_offset).copied() else {
1498 return Ok(None);
1499 };
1500 Some(base_oid)
1501 }
1502 7 => {
1503 let end = match cursor.checked_add(format.raw_len()) {
1504 Some(end) if end <= raw.len() => end,
1505 _ => return Ok(None),
1506 };
1507 let base_oid = ObjectId::from_raw(format, &raw[cursor..end])?;
1508 cursor = end;
1509 Some(base_oid)
1510 }
1511 _ => None,
1512 };
1513 if cursor >= raw.len() {
1514 return Ok(None);
1515 }
1516 Ok(Some((kind, cursor, base_oid)))
1517}
1518
1519fn assemble_generated_and_reused_entries(
1520 format: ObjectFormat,
1521 fresh: Option<PackWrite>,
1522 reused: &[SelectedReusableEntry],
1523) -> Result<PackWrite> {
1524 let fresh_count = fresh.as_ref().map_or(0usize, |pack| pack.entries.len());
1525 let total = fresh_count
1526 .checked_add(reused.len())
1527 .and_then(|count| u32::try_from(count).ok())
1528 .ok_or_else(|| GitError::InvalidFormat("too many pack objects".into()))?;
1529 let mut out = Vec::new();
1530 out.extend_from_slice(b"PACK");
1531 out.extend_from_slice(&2u32.to_be_bytes());
1532 out.extend_from_slice(&total.to_be_bytes());
1533
1534 let mut entries = Vec::with_capacity(total as usize);
1535 let mut delta_count = 0u32;
1536 if let Some(fresh) = fresh {
1537 let hash_len = format.raw_len();
1538 if fresh.pack.len() < 12 + hash_len || &fresh.pack[..4] != b"PACK" {
1539 return Err(GitError::InvalidFormat(
1540 "generated pack is not reusable".into(),
1541 ));
1542 }
1543 out.extend_from_slice(&fresh.pack[12..fresh.pack.len() - hash_len]);
1544 delta_count = fresh.delta_count;
1545 entries.extend(fresh.entries);
1546 }
1547
1548 for reused in reused {
1549 let offset = out.len() as u64;
1550 let encoded = encode_selected_reusable_entry(reused)?;
1551 let crc32 = crc32fast::hash(&encoded);
1552 out.extend_from_slice(&encoded);
1553 entries.push(PackIndexEntry {
1554 oid: reused.entry.oid,
1555 crc32,
1556 offset,
1557 });
1558 if matches!(reused.entry.kind, 6 | 7) {
1559 delta_count = delta_count
1560 .checked_add(1)
1561 .ok_or_else(|| GitError::InvalidFormat("too many pack deltas".into()))?;
1562 }
1563 }
1564
1565 let checksum = sley_core::digest_bytes(format, &out)?;
1566 out.extend_from_slice(checksum.as_bytes());
1567 let index = PackIndex::write_v2(format, &entries, &checksum)?;
1568 Ok(PackWrite {
1569 pack: out,
1570 index,
1571 checksum,
1572 entries,
1573 delta_count,
1574 })
1575}
1576
1577fn encode_selected_reusable_entry(reused: &SelectedReusableEntry) -> Result<Vec<u8>> {
1578 let raw = &reused.pack[reused.entry.start..reused.entry.end];
1579 if reused.entry.kind != 6 {
1580 return Ok(raw.to_vec());
1581 }
1582 let base_oid = reused
1583 .entry
1584 .base_oid
1585 .ok_or_else(|| GitError::InvalidFormat("reused ofs-delta has no base oid".into()))?;
1586 let mut entry = raw[..reused.entry.payload_start].to_vec();
1587 let mut header_end = 1usize;
1588 while entry[header_end - 1] & 0x80 != 0 {
1589 header_end += 1;
1590 }
1591 entry.truncate(header_end);
1592 entry[0] = (entry[0] & 0x8f) | (7 << 4);
1593 entry.extend_from_slice(base_oid.as_bytes());
1594 entry.extend_from_slice(&raw[reused.entry.payload_start..]);
1595 Ok(entry)
1596}
1597
1598struct ReachablePackDigestWriter<'a, W> {
1599 writer: &'a mut W,
1600 digest: StreamingDigest,
1601 position: u64,
1602}
1603
1604impl<'a, W: Write> ReachablePackDigestWriter<'a, W> {
1605 fn new(writer: &'a mut W, format: ObjectFormat) -> Self {
1606 Self {
1607 writer,
1608 digest: StreamingDigest::new(format),
1609 position: 0,
1610 }
1611 }
1612
1613 fn position(&self) -> u64 {
1614 self.position
1615 }
1616
1617 fn write_pack_bytes(&mut self, bytes: &[u8]) -> Result<()> {
1618 self.writer.write_all(bytes)?;
1619 self.digest.update(bytes);
1620 self.position = self
1621 .position
1622 .checked_add(bytes.len() as u64)
1623 .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
1624 Ok(())
1625 }
1626
1627 fn finish(mut self) -> Result<(ObjectId, u64)> {
1628 let checksum = self.digest.finalize()?;
1629 self.writer.write_all(checksum.as_bytes())?;
1630 self.position = self
1631 .position
1632 .checked_add(checksum.as_bytes().len() as u64)
1633 .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
1634 Ok((checksum, self.position))
1635 }
1636}
1637
1638struct GeneratedPackBodyWriter<'a, 'b, W> {
1642 output: &'a mut ReachablePackDigestWriter<'b, W>,
1643 prefix: Vec<u8>,
1644 pending_trailer: Vec<u8>,
1645 hash_len: usize,
1646}
1647
1648impl<'a, 'b, W: Write> GeneratedPackBodyWriter<'a, 'b, W> {
1649 fn new(output: &'a mut ReachablePackDigestWriter<'b, W>, hash_len: usize) -> Self {
1650 Self {
1651 output,
1652 prefix: Vec::with_capacity(12),
1653 pending_trailer: Vec::with_capacity(hash_len),
1654 hash_len,
1655 }
1656 }
1657
1658 fn finish(self, checksum: &ObjectId, object_count: usize) -> Result<()> {
1659 if self.prefix.len() != 12 || &self.prefix[..4] != b"PACK" {
1660 return Err(GitError::InvalidFormat(
1661 "generated pack is missing its header".into(),
1662 ));
1663 }
1664 let declared_count = u32::from_be_bytes([
1665 self.prefix[8],
1666 self.prefix[9],
1667 self.prefix[10],
1668 self.prefix[11],
1669 ]);
1670 if declared_count as usize != object_count {
1671 return Err(GitError::InvalidFormat(format!(
1672 "generated pack declared {declared_count} objects, expected {object_count}"
1673 )));
1674 }
1675 if self.pending_trailer.as_slice() != checksum.as_bytes() {
1676 return Err(GitError::InvalidFormat(
1677 "generated pack checksum trailer mismatch".into(),
1678 ));
1679 }
1680 Ok(())
1681 }
1682
1683 fn forward_body(&mut self, mut bytes: &[u8]) -> Result<()> {
1684 if self.pending_trailer.len() < self.hash_len {
1685 let fill = (self.hash_len - self.pending_trailer.len()).min(bytes.len());
1686 self.pending_trailer.extend_from_slice(&bytes[..fill]);
1687 bytes = &bytes[fill..];
1688 }
1689 if bytes.is_empty() {
1690 return Ok(());
1691 }
1692
1693 if bytes.len() >= self.hash_len {
1694 self.output.write_pack_bytes(&self.pending_trailer)?;
1695 let body_len = bytes.len() - self.hash_len;
1696 self.output.write_pack_bytes(&bytes[..body_len])?;
1697 self.pending_trailer.clear();
1698 self.pending_trailer.extend_from_slice(&bytes[body_len..]);
1699 return Ok(());
1700 }
1701
1702 self.output
1703 .write_pack_bytes(&self.pending_trailer[..bytes.len()])?;
1704 self.pending_trailer.copy_within(bytes.len().., 0);
1705 let retained = self.hash_len - bytes.len();
1706 self.pending_trailer.truncate(retained);
1707 self.pending_trailer.extend_from_slice(bytes);
1708 Ok(())
1709 }
1710}
1711
1712impl<W: Write> Write for GeneratedPackBodyWriter<'_, '_, W> {
1713 fn write(&mut self, mut bytes: &[u8]) -> io::Result<usize> {
1714 let input_len = bytes.len();
1715 if self.prefix.len() < 12 {
1716 let fill = (12 - self.prefix.len()).min(bytes.len());
1717 self.prefix.extend_from_slice(&bytes[..fill]);
1718 bytes = &bytes[fill..];
1719 }
1720 self.forward_body(bytes)
1721 .map_err(|err| io::Error::other(err.to_string()))?;
1722 Ok(input_len)
1723 }
1724
1725 fn flush(&mut self) -> io::Result<()> {
1726 self.output
1727 .writer
1728 .flush()
1729 .map_err(|err| io::Error::other(err.to_string()))
1730 }
1731}
1732
1733fn trace_reachable_pack_reuse(stats: ReachablePackReuseStats) {
1734 sley_core::trace2::data(
1735 "pack-objects",
1736 "verbatim-entries",
1737 u64::from(stats.verbatim_entries),
1738 );
1739 sley_core::trace2::data(
1740 "pack-objects",
1741 "redeltified-entries",
1742 u64::from(stats.redeltified_entries),
1743 );
1744 sley_core::trace2::data(
1745 "pack-objects",
1746 "whole-pack-reused",
1747 u64::from(stats.whole_pack),
1748 );
1749}
1750
1751#[allow(clippy::too_many_arguments)]
1754pub fn build_and_install_reachable_pack_filtered<R, I>(
1755 source: &R,
1756 destination: &FileObjectDatabase,
1757 format: ObjectFormat,
1758 starts: I,
1759 excluded: &HashSet<ObjectId>,
1760 options: RawPackInstallOptions,
1761 filter: Option<PackObjectFilter>,
1762 unpack_limit: Option<usize>,
1763) -> Result<Option<PackInstallResult>>
1764where
1765 R: ObjectReader,
1766 I: IntoIterator<Item = ObjectId>,
1767{
1768 build_and_install_reachable_pack_filtered_with_missing_policy(
1769 source,
1770 destination,
1771 format,
1772 starts,
1773 excluded,
1774 options,
1775 filter,
1776 unpack_limit,
1777 ReachablePackMissingPolicy::RequireComplete,
1778 )
1779}
1780
1781#[allow(clippy::too_many_arguments)]
1785pub fn build_and_install_reachable_pack_filtered_with_missing_policy<R, I>(
1786 source: &R,
1787 destination: &FileObjectDatabase,
1788 format: ObjectFormat,
1789 starts: I,
1790 excluded: &HashSet<ObjectId>,
1791 options: RawPackInstallOptions,
1792 filter: Option<PackObjectFilter>,
1793 unpack_limit: Option<usize>,
1794 missing_policy: ReachablePackMissingPolicy,
1795) -> Result<Option<PackInstallResult>>
1796where
1797 R: ObjectReader,
1798 I: IntoIterator<Item = ObjectId>,
1799{
1800 build_and_install_reachable_pack_filtered_with_thin_bases(
1801 source,
1802 destination,
1803 format,
1804 starts,
1805 excluded,
1806 options,
1807 filter,
1808 unpack_limit,
1809 missing_policy,
1810 ReachablePackThinBaseCandidates::default(),
1811 )
1812 .map(|outcome| outcome.install)
1813}
1814
1815#[derive(Debug, Clone, PartialEq, Eq)]
1819pub struct ReachablePackInstallOutcome {
1820 pub install: Option<PackInstallResult>,
1823 pub object_count: usize,
1825 pub compression_count: usize,
1829 pub delta_count: u32,
1833}
1834
1835#[allow(clippy::too_many_arguments)]
1840pub fn build_and_install_reachable_pack_filtered_with_thin_bases<R, I>(
1841 source: &R,
1842 destination: &FileObjectDatabase,
1843 format: ObjectFormat,
1844 starts: I,
1845 excluded: &HashSet<ObjectId>,
1846 options: RawPackInstallOptions,
1847 filter: Option<PackObjectFilter>,
1848 unpack_limit: Option<usize>,
1849 missing_policy: ReachablePackMissingPolicy,
1850 thin_base_candidates: ReachablePackThinBaseCandidates<'_>,
1851) -> Result<ReachablePackInstallOutcome>
1852where
1853 R: ObjectReader,
1854 I: IntoIterator<Item = ObjectId>,
1855{
1856 let starts: Vec<ObjectId> = starts.into_iter().collect();
1857 let wanted: HashSet<ObjectId> = starts.iter().copied().collect();
1858 let mut objects = match missing_policy {
1859 ReachablePackMissingPolicy::RequireComplete => {
1860 collect_reachable_pack_objects(source, format, starts, excluded)?
1861 }
1862 ReachablePackMissingPolicy::OmitPromised => {
1863 collect_reachable_pack_objects_tolerating_promised_missing(
1864 source, format, starts, excluded,
1865 )?
1866 }
1867 };
1868 retain_filtered_pack_objects(&mut objects, filter.as_ref(), &wanted, source, format)?;
1869 let object_count = objects.len();
1870 let compression_count = objects
1871 .iter()
1872 .filter(|entry| entry.object.body.len() >= 50)
1873 .count();
1874 if objects.is_empty() {
1875 return Ok(ReachablePackInstallOutcome {
1876 install: None,
1877 object_count,
1878 compression_count,
1879 delta_count: 0,
1880 });
1881 }
1882 let unpack_loose = unpack_limit.is_some_and(|limit| objects.len() < limit);
1887 let inputs = pack_inputs(&objects);
1888 let (thin_bases, preferred_thin_bases) =
1889 select_reachable_pack_thin_bases(source, &objects, thin_base_candidates.object_ids)?;
1890 let pack_dir = destination.objects_dir.join("pack");
1891 fs::create_dir_all(&pack_dir)?;
1892 let temp_pack_path = unique_temp_path(&pack_dir);
1893 let result = (|| -> Result<(Option<PackInstallResult>, u32)> {
1894 let mut file = fs::OpenOptions::new()
1895 .write(true)
1896 .create_new(true)
1897 .open(&temp_pack_path)?;
1898 let pack_options = PackWriteOptions::new()
1899 .with_thin_bases(thin_bases)
1900 .with_preferred_thin_bases(preferred_thin_bases);
1901 let summary = PackFile::write_packed_with_known_ids_to_writer(
1902 &inputs,
1903 format,
1904 &pack_options,
1905 &mut file,
1906 )?;
1907 file.flush()?;
1908 file.sync_all()?;
1909 drop(file);
1910 trace_packfile_path(&temp_pack_path)?;
1911 let delta_count = summary.delta_count;
1912 if unpack_loose {
1913 for entry in &objects {
1914 destination.loose().write_object((*entry.object).clone())?;
1915 }
1916 fs::remove_file(&temp_pack_path)?;
1917 return Ok((None, delta_count));
1918 }
1919 destination
1920 .install_pack_file_from_temp(
1921 &temp_pack_path,
1922 summary.checksum,
1923 &summary.index,
1924 summary.entries.iter().map(|entry| entry.oid).collect(),
1925 options,
1926 )
1927 .map(|install| (Some(install), delta_count))
1928 })();
1929 if result.is_err() {
1930 let _ = fs::remove_file(&temp_pack_path);
1931 }
1932 result.map(|(install, delta_count)| ReachablePackInstallOutcome {
1933 install,
1934 object_count,
1935 compression_count,
1936 delta_count,
1937 })
1938}
1939
1940fn select_reachable_pack_thin_bases<R: ObjectReader>(
1944 source: &R,
1945 objects: &[ReachablePackObject],
1946 candidate_ids: Option<&HashSet<ObjectId>>,
1947) -> Result<(
1948 HashMap<ObjectId, EncodedObject>,
1949 HashMap<ObjectId, ObjectId>,
1950)> {
1951 let Some(candidate_ids) = candidate_ids else {
1952 return Ok((HashMap::new(), HashMap::new()));
1953 };
1954 if candidate_ids.is_empty() || objects.is_empty() {
1955 return Ok((HashMap::new(), HashMap::new()));
1956 }
1957
1958 let mut bases = HashMap::new();
1959 let mut preferred = HashMap::new();
1960 for entry in objects {
1961 let Some(base_oid) = source.reusable_delta_base(&entry.oid)? else {
1962 continue;
1963 };
1964 if !candidate_ids.contains(&base_oid) {
1965 continue;
1966 }
1967 if let std::collections::hash_map::Entry::Vacant(slot) = bases.entry(base_oid) {
1968 slot.insert((*source.read_object(&base_oid)?).clone());
1969 }
1970 preferred.insert(entry.oid, base_oid);
1971 }
1972 Ok((bases, preferred))
1973}
1974
1975#[cfg(test)]
1976mod thin_base_tests {
1977 use super::*;
1978
1979 #[test]
1980 fn reachable_transfer_retains_count_when_unpacking_loose_objects() {
1981 let format = ObjectFormat::Sha1;
1982 let root = unique_temp_path(&env::temp_dir());
1983 let source = FileObjectDatabase::new(root.join("source/objects"), format);
1984 let destination = FileObjectDatabase::new(root.join("destination/objects"), format);
1985 let blob = EncodedObject::new(ObjectType::Blob, b"small transfer\n".to_vec());
1986 let oid = source.write_object(blob).expect("write source blob");
1987
1988 let outcome = build_and_install_reachable_pack_filtered_with_thin_bases(
1989 &source,
1990 &destination,
1991 format,
1992 [oid],
1993 &HashSet::new(),
1994 RawPackInstallOptions::default(),
1995 None,
1996 Some(100),
1997 ReachablePackMissingPolicy::RequireComplete,
1998 ReachablePackThinBaseCandidates::default(),
1999 )
2000 .expect("install loose transfer");
2001
2002 assert_eq!(outcome.object_count, 1);
2003 assert_eq!(outcome.compression_count, 0);
2004 assert_eq!(outcome.delta_count, 0);
2005 assert!(outcome.install.is_none());
2006 assert!(destination.contains(&oid).expect("read destination blob"));
2007 fs::remove_dir_all(root).expect("remove test repositories");
2008 }
2009
2010 #[test]
2011 fn reachable_transfer_reuses_stored_client_owned_blob_base() {
2012 for format in [ObjectFormat::Sha1, ObjectFormat::Sha256] {
2013 let root = unique_temp_path(&env::temp_dir());
2014 let source = FileObjectDatabase::new(root.join("source/objects"), format);
2015 let destination = FileObjectDatabase::new(root.join("destination/objects"), format);
2016
2017 let base = EncodedObject::new(ObjectType::Blob, vec![b'a'; 16_384]);
2018 let mut target_body = base.body.clone();
2019 target_body[8_192..8_256].fill(b'b');
2020 let target = EncodedObject::new(ObjectType::Blob, target_body);
2021 let base_oid = base.object_id(format).expect("base oid");
2022 let target_oid = target.object_id(format).expect("target oid");
2023 let source_pack = PackFile::write_packed_with_options(
2024 &[base.clone(), target.clone()],
2025 format,
2026 &PackWriteOptions::new().with_reorder(false),
2027 )
2028 .expect("write source pack");
2029 source
2030 .install_pack(&source_pack)
2031 .expect("install source pack");
2032 assert_eq!(
2033 source
2034 .reusable_delta_base(&target_oid)
2035 .expect("read source delta metadata"),
2036 Some(base_oid)
2037 );
2038 destination
2039 .write_object(base)
2040 .expect("client already owns base");
2041
2042 let candidates = HashSet::from([base_oid]);
2043
2044 let result = build_and_install_reachable_pack_filtered_with_thin_bases(
2045 &source,
2046 &destination,
2047 format,
2048 [target_oid],
2049 &HashSet::new(),
2050 RawPackInstallOptions::default(),
2051 None,
2052 Some(1),
2053 ReachablePackMissingPolicy::RequireComplete,
2054 ReachablePackThinBaseCandidates::from_object_ids(&candidates),
2055 )
2056 .expect("build thin transfer")
2057 .install
2058 .expect("installed pack");
2059
2060 let index = PackIndex::parse(
2061 &fs::read(&result.index_path).expect("read transfer index"),
2062 format,
2063 )
2064 .expect("parse transfer index");
2065 let target_offset = index
2066 .entries
2067 .iter()
2068 .find(|entry| entry.oid == target_oid)
2069 .expect("target index entry")
2070 .offset as usize;
2071 let pack = fs::read(&result.pack_path).expect("read transfer pack");
2072 let first = pack[target_offset];
2073 assert_eq!((first >> 4) & 0x07, 7, "target must be a ref-delta");
2074 let mut base_offset = target_offset + 1;
2075 let mut header_byte = first;
2076 while header_byte & 0x80 != 0 {
2077 header_byte = pack[base_offset];
2078 base_offset += 1;
2079 }
2080 assert_eq!(
2081 &pack[base_offset..base_offset + format.raw_len()],
2082 base_oid.as_bytes(),
2083 "thin delta should use the buried client-owned base"
2084 );
2085
2086 destination.refresh_read_cache();
2087 assert_eq!(
2088 destination
2089 .read_object(&target_oid)
2090 .expect("read target through external pack base")
2091 .as_ref(),
2092 &target
2093 );
2094 fs::remove_dir_all(root).expect("remove test repository");
2095 }
2096 }
2097}
2098
2099#[cfg(test)]
2100mod pack_reuse_tests {
2101 use super::*;
2102
2103 #[derive(Clone)]
2104 struct CandidateReader {
2105 objects: HashMap<ObjectId, Arc<EncodedObject>>,
2106 candidates: Vec<ReusablePackCandidate>,
2107 }
2108
2109 impl ObjectReader for CandidateReader {
2110 fn read_object(&self, oid: &ObjectId) -> Result<Arc<EncodedObject>> {
2111 self.objects
2112 .get(oid)
2113 .cloned()
2114 .ok_or_else(|| GitError::object_not_found_in(*oid, MissingObjectContext::Read))
2115 }
2116
2117 fn reusable_pack_candidates(
2118 &self,
2119 object_ids: &HashSet<ObjectId>,
2120 ) -> Result<Vec<ReusablePackCandidate>> {
2121 Ok(self
2122 .candidates
2123 .iter()
2124 .filter(|candidate| {
2125 candidate
2126 .entries
2127 .iter()
2128 .any(|entry| object_ids.contains(&entry.oid))
2129 })
2130 .cloned()
2131 .collect())
2132 }
2133 }
2134
2135 fn reusable_candidate(pack: &PackWrite) -> ReusablePackCandidate {
2136 ReusablePackCandidate {
2137 pack: Arc::from(pack.pack.clone()),
2138 pack_path: None,
2139 entries: pack.entries.clone(),
2140 pack_checksum: pack.checksum,
2141 }
2142 }
2143
2144 fn similar_blobs(format: ObjectFormat) -> (EncodedObject, ObjectId, EncodedObject, ObjectId) {
2145 let base = EncodedObject::new(ObjectType::Blob, vec![b'a'; 16_384]);
2146 let mut target_body = base.body.clone();
2147 target_body[8_192..8_256].fill(b'b');
2148 let target = EncodedObject::new(ObjectType::Blob, target_body);
2149 let base_oid = base.object_id(format).expect("base oid");
2150 let target_oid = target.object_id(format).expect("target oid");
2151 (base, base_oid, target, target_oid)
2152 }
2153
2154 #[test]
2155 fn exact_self_contained_pack_is_returned_verbatim() {
2156 for format in [ObjectFormat::Sha1, ObjectFormat::Sha256] {
2157 let root = unique_temp_path(&env::temp_dir());
2158 let source = FileObjectDatabase::new(root.join("objects"), format);
2159 let (base, base_oid, target, target_oid) = similar_blobs(format);
2160 let stored = PackFile::write_packed_with_options(
2161 &[base, target],
2162 format,
2163 &PackWriteOptions::new().with_reorder(false),
2164 )
2165 .expect("write source pack");
2166 assert_eq!(stored.delta_count, 1);
2167 source.install_pack(&stored).expect("install source pack");
2168
2169 let build = build_reachable_pack_with_reuse_stats(
2170 &source,
2171 format,
2172 [base_oid, target_oid],
2173 &HashSet::new(),
2174 )
2175 .expect("build reachable pack")
2176 .expect("non-empty pack");
2177
2178 assert_eq!(build.pack.pack, stored.pack);
2179 assert_eq!(
2180 build.reuse,
2181 ReachablePackReuseStats {
2182 verbatim_entries: 2,
2183 redeltified_entries: 0,
2184 whole_pack: true,
2185 }
2186 );
2187 assert_eq!(build.pack.delta_count, 1);
2188 PackFile::parse(&build.pack.pack, format).expect("verbatim pack is self-contained");
2189
2190 let mut streamed = Vec::new();
2191 let streamed_build = write_reachable_pack_with_reuse_stats_to_writer(
2192 &source,
2193 format,
2194 [base_oid, target_oid],
2195 &HashSet::new(),
2196 &mut streamed,
2197 )
2198 .expect("stream reachable pack")
2199 .expect("non-empty streamed pack");
2200 assert_eq!(streamed, stored.pack);
2201 assert_eq!(streamed_build.reuse, build.reuse);
2202 assert_eq!(streamed_build.summary.checksum, stored.checksum);
2203 assert_eq!(streamed_build.summary.object_count, 2);
2204 fs::remove_dir_all(root).expect("remove test repository");
2205 }
2206 }
2207
2208 #[test]
2209 fn partial_pack_reuses_existing_delta_chain_without_redeltifying() {
2210 let format = ObjectFormat::Sha1;
2211 let root = unique_temp_path(&env::temp_dir());
2212 let source = FileObjectDatabase::new(root.join("objects"), format);
2213 let (base, base_oid, target, target_oid) = similar_blobs(format);
2214 let unrelated = EncodedObject::new(ObjectType::Blob, b"not selected\n".to_vec());
2215 let stored = PackFile::write_packed_with_options(
2216 &[base.clone(), target.clone(), unrelated],
2217 format,
2218 &PackWriteOptions::new().with_reorder(false),
2219 )
2220 .expect("write source pack");
2221 assert_eq!(stored.delta_count, 1);
2222 source.install_pack(&stored).expect("install source pack");
2223
2224 let build = build_reachable_pack_with_reuse_stats(
2225 &source,
2226 format,
2227 [base_oid, target_oid],
2228 &HashSet::new(),
2229 )
2230 .expect("build partial reuse pack")
2231 .expect("non-empty pack");
2232
2233 assert_ne!(build.pack.pack, stored.pack);
2234 assert_eq!(
2235 build.reuse,
2236 ReachablePackReuseStats {
2237 verbatim_entries: 2,
2238 redeltified_entries: 0,
2239 whole_pack: false,
2240 }
2241 );
2242 assert_eq!(build.pack.delta_count, 1);
2243 let parsed =
2244 PackFile::parse(&build.pack.pack, format).expect("partial reuse is self-contained");
2245 let parsed = parsed
2246 .entries
2247 .into_iter()
2248 .map(|entry| (entry.entry.oid, entry.object))
2249 .collect::<HashMap<_, _>>();
2250 assert_eq!(parsed.get(&base_oid), Some(&base));
2251 assert_eq!(parsed.get(&target_oid), Some(&target));
2252 fs::remove_dir_all(root).expect("remove test repository");
2253 }
2254
2255 #[test]
2256 fn thin_whole_pack_candidate_is_refused_and_regenerated() {
2257 let format = ObjectFormat::Sha1;
2258 let (base, base_oid, target, target_oid) = similar_blobs(format);
2259 let thin = PackFile::write_packed_with_options(
2260 std::slice::from_ref(&target),
2261 format,
2262 &PackWriteOptions::new().with_thin_bases(HashMap::from([(base_oid, base)])),
2263 )
2264 .expect("write thin source pack");
2265 assert_eq!(thin.delta_count, 1, "test candidate must really be thin");
2266 let reader = CandidateReader {
2267 objects: HashMap::from([(target_oid, Arc::new(target.clone()))]),
2268 candidates: vec![reusable_candidate(&thin)],
2269 };
2270
2271 let build =
2272 build_reachable_pack_with_reuse_stats(&reader, format, [target_oid], &HashSet::new())
2273 .expect("refuse thin candidate and regenerate")
2274 .expect("non-empty pack");
2275
2276 assert_ne!(build.pack.pack, thin.pack);
2277 assert_eq!(
2278 build.reuse,
2279 ReachablePackReuseStats {
2280 verbatim_entries: 0,
2281 redeltified_entries: 1,
2282 whole_pack: false,
2283 }
2284 );
2285 let parsed =
2286 PackFile::parse(&build.pack.pack, format).expect("fallback pack is self-contained");
2287 assert_eq!(parsed.entries.len(), 1);
2288 assert_eq!(parsed.entries[0].object, target);
2289
2290 let mut streamed = Vec::new();
2291 let streamed_build = write_reachable_pack_with_reuse_stats_to_writer(
2292 &reader,
2293 format,
2294 [target_oid],
2295 &HashSet::new(),
2296 &mut streamed,
2297 )
2298 .expect("stream regenerated pack")
2299 .expect("non-empty streamed pack");
2300 assert_eq!(streamed, build.pack.pack);
2301 assert_eq!(streamed_build.reuse, build.reuse);
2302 assert_eq!(streamed_build.summary.object_count, 1);
2303 assert_eq!(streamed_build.summary.checksum, build.pack.checksum);
2304 }
2305
2306 #[test]
2307 fn selected_cross_pack_ref_delta_is_reused_safely() {
2308 let format = ObjectFormat::Sha1;
2309 let (base, base_oid, target, target_oid) = similar_blobs(format);
2310 let base_pack =
2311 PackFile::write_packed(std::slice::from_ref(&base), format).expect("write base pack");
2312 let target_pack = PackFile::write_packed_with_options(
2313 std::slice::from_ref(&target),
2314 format,
2315 &PackWriteOptions::new().with_thin_bases(HashMap::from([(base_oid, base.clone())])),
2316 )
2317 .expect("write cross-pack ref-delta");
2318 assert_eq!(target_pack.delta_count, 1);
2319 let reader = CandidateReader {
2320 objects: HashMap::from([
2321 (base_oid, Arc::new(base.clone())),
2322 (target_oid, Arc::new(target.clone())),
2323 ]),
2324 candidates: vec![
2325 reusable_candidate(&target_pack),
2326 reusable_candidate(&base_pack),
2327 ],
2328 };
2329
2330 let build = build_reachable_pack_with_reuse_stats(
2331 &reader,
2332 format,
2333 [base_oid, target_oid],
2334 &HashSet::new(),
2335 )
2336 .expect("reuse cross-pack delta")
2337 .expect("non-empty pack");
2338
2339 assert_eq!(
2340 build.reuse,
2341 ReachablePackReuseStats {
2342 verbatim_entries: 2,
2343 redeltified_entries: 0,
2344 whole_pack: false,
2345 }
2346 );
2347 assert_eq!(build.pack.delta_count, 1);
2348 let parsed =
2349 PackFile::parse(&build.pack.pack, format).expect("cross-pack reuse is self-contained");
2350 let parsed = parsed
2351 .entries
2352 .into_iter()
2353 .map(|entry| (entry.entry.oid, entry.object))
2354 .collect::<HashMap<_, _>>();
2355 assert_eq!(parsed.get(&base_oid), Some(&base));
2356 assert_eq!(parsed.get(&target_oid), Some(&target));
2357 }
2358
2359 #[test]
2360 fn closure_spanning_multiple_packs_never_serves_one_partial_pack() {
2361 let format = ObjectFormat::Sha1;
2362 let root = unique_temp_path(&env::temp_dir());
2363 let source = FileObjectDatabase::new(root.join("objects"), format);
2364
2365 let blob = EncodedObject::new(ObjectType::Blob, b"across packs\n".to_vec());
2366 let blob_oid = blob.object_id(format).expect("blob oid");
2367 let mut tree_body = b"100644 file\0".to_vec();
2368 tree_body.extend_from_slice(blob_oid.as_bytes());
2369 let tree = EncodedObject::new(ObjectType::Tree, tree_body);
2370 let tree_oid = tree.object_id(format).expect("tree oid");
2371 let commit = EncodedObject::new(
2372 ObjectType::Commit,
2373 format!(
2374 "tree {tree_oid}\nauthor A <a@example.com> 0 +0000\ncommitter A <a@example.com> 0 +0000\n\nsplit closure\n"
2375 )
2376 .into_bytes(),
2377 );
2378 let commit_oid = commit.object_id(format).expect("commit oid");
2379
2380 let data_pack = PackFile::write_packed(&[blob, tree], format).expect("write data pack");
2381 let commit_pack = PackFile::write_packed(std::slice::from_ref(&commit), format)
2382 .expect("write commit pack");
2383 source.install_pack(&data_pack).expect("install data pack");
2384 source
2385 .install_pack(&commit_pack)
2386 .expect("install commit pack");
2387
2388 let build =
2389 build_reachable_pack_with_reuse_stats(&source, format, [commit_oid], &HashSet::new())
2390 .expect("build multi-pack closure")
2391 .expect("non-empty pack");
2392
2393 assert!(!build.reuse.whole_pack);
2394 assert_eq!(build.reuse.verbatim_entries, 3);
2395 assert_eq!(build.reuse.redeltified_entries, 0);
2396 assert_ne!(build.pack.pack, data_pack.pack);
2397 assert_ne!(build.pack.pack, commit_pack.pack);
2398 let parsed =
2399 PackFile::parse(&build.pack.pack, format).expect("combined closure is self-contained");
2400 assert_eq!(parsed.entries.len(), 3);
2401 assert!(
2402 parsed
2403 .entries
2404 .iter()
2405 .any(|entry| entry.entry.oid == commit_oid && entry.object == commit)
2406 );
2407
2408 let mut streamed = Vec::new();
2409 let streamed_build = write_reachable_pack_with_reuse_stats_to_writer(
2410 &source,
2411 format,
2412 [commit_oid],
2413 &HashSet::new(),
2414 &mut streamed,
2415 )
2416 .expect("stream multi-pack closure")
2417 .expect("non-empty streamed pack");
2418 assert_eq!(streamed, build.pack.pack);
2419 assert_eq!(streamed_build.reuse, build.reuse);
2420 assert_eq!(streamed_build.summary.object_count, 3);
2421 assert_eq!(streamed_build.summary.checksum, build.pack.checksum);
2422 fs::remove_dir_all(root).expect("remove test repository");
2423 }
2424}
2425
2426fn trace_packfile_path(pack_path: &Path) -> Result<()> {
2427 let Some(path) = env::var_os("GIT_TRACE_PACKFILE").filter(|value| !value.is_empty()) else {
2428 return Ok(());
2429 };
2430 fs::copy(pack_path, path)?;
2431 Ok(())
2432}
2433
2434fn collect_tree_filter_depths<R>(
2435 reader: &R,
2436 format: ObjectFormat,
2437 objects: &[ReachablePackObject],
2438) -> Result<HashMap<ObjectId, u32>>
2439where
2440 R: ObjectReader,
2441{
2442 let available: HashSet<ObjectId> = objects.iter().map(|entry| entry.oid).collect();
2443 let mut depths = HashMap::new();
2444 let mut stack = Vec::new();
2445 for entry in objects {
2446 if entry.object.object_type != ObjectType::Commit {
2447 continue;
2448 }
2449 let commit = Commit::parse(format, &entry.object.body)?;
2450 if available.contains(&commit.tree) {
2451 stack.push((commit.tree, 0u32));
2452 }
2453 }
2454 while let Some((tree_oid, depth)) = stack.pop() {
2455 if depths
2456 .get(&tree_oid)
2457 .is_some_and(|old_depth| *old_depth <= depth)
2458 {
2459 continue;
2460 }
2461 depths.insert(tree_oid, depth);
2462 let tree = reader.read_object(&tree_oid)?;
2463 if tree.object_type != ObjectType::Tree {
2464 continue;
2465 }
2466 let child_depth = depth.saturating_add(1);
2467 for entry in TreeEntries::new(format, &tree.body) {
2468 let entry = entry?;
2469 if tree_entry_object_type(entry.mode) == ObjectType::Tree
2470 && available.contains(&entry.oid)
2471 {
2472 stack.push((entry.oid, child_depth));
2473 }
2474 }
2475 }
2476 Ok(depths)
2477}
2478
2479fn collect_sparse_filter_blobs<R>(
2480 reader: &R,
2481 format: ObjectFormat,
2482 objects: &[ReachablePackObject],
2483 paths: &[String],
2484) -> Result<HashSet<ObjectId>>
2485where
2486 R: ObjectReader,
2487{
2488 let wanted_paths: HashSet<&str> = paths.iter().map(String::as_str).collect();
2489 let mut allowed = HashSet::new();
2490 let mut seen_trees = HashSet::new();
2491 for entry in objects {
2492 if entry.object.object_type != ObjectType::Commit {
2493 continue;
2494 }
2495 let commit = Commit::parse(format, &entry.object.body)?;
2496 collect_sparse_tree_blobs(
2497 reader,
2498 format,
2499 &commit.tree,
2500 "",
2501 &wanted_paths,
2502 &mut seen_trees,
2503 &mut allowed,
2504 )?;
2505 }
2506 Ok(allowed)
2507}
2508
2509fn collect_sparse_tree_blobs<R>(
2510 reader: &R,
2511 format: ObjectFormat,
2512 tree_oid: &ObjectId,
2513 prefix: &str,
2514 wanted_paths: &HashSet<&str>,
2515 seen_trees: &mut HashSet<ObjectId>,
2516 allowed: &mut HashSet<ObjectId>,
2517) -> Result<()>
2518where
2519 R: ObjectReader,
2520{
2521 if !seen_trees.insert(*tree_oid) {
2522 return Ok(());
2523 }
2524 let tree = reader.read_object(tree_oid)?;
2525 if tree.object_type != ObjectType::Tree {
2526 return Ok(());
2527 }
2528 for entry in TreeEntries::new(format, &tree.body) {
2529 let entry = entry?;
2530 let name = String::from_utf8_lossy(entry.name);
2531 let path = if prefix.is_empty() {
2532 name.into_owned()
2533 } else {
2534 format!("{prefix}/{name}")
2535 };
2536 if tree_entry_object_type(entry.mode) == ObjectType::Tree {
2537 collect_sparse_tree_blobs(
2538 reader,
2539 format,
2540 &entry.oid,
2541 &path,
2542 wanted_paths,
2543 seen_trees,
2544 allowed,
2545 )?;
2546 } else if wanted_paths.contains(path.as_str()) {
2547 allowed.insert(entry.oid);
2548 }
2549 }
2550 Ok(())
2551}
2552
2553pub fn assemble_pack_with_verbatim_reuse(
2563 format: ObjectFormat,
2564 reused_pack_bytes: &[u8],
2565 appended: &[PackInput<'_>],
2566) -> Result<(Vec<u8>, u32)> {
2567 assemble_pack_with_verbatim_reuses(format, &[reused_pack_bytes], appended)
2568}
2569
2570pub fn assemble_pack_with_verbatim_reuses(
2573 format: ObjectFormat,
2574 reused_packs: &[&[u8]],
2575 appended: &[PackInput<'_>],
2576) -> Result<(Vec<u8>, u32)> {
2577 let hash_len = format.raw_len();
2578 let mut reused_count = 0u32;
2579 let mut capacity = 12 + hash_len + 64 * appended.len();
2580 for reused_pack_bytes in reused_packs {
2581 if reused_pack_bytes.len() < 12 + hash_len {
2582 return Err(GitError::InvalidFormat("reused pack too short".into()));
2583 }
2584 if &reused_pack_bytes[..4] != b"PACK" {
2585 return Err(GitError::InvalidFormat(
2586 "reused pack has no signature".into(),
2587 ));
2588 }
2589 let version = u32::from_be_bytes([
2590 reused_pack_bytes[4],
2591 reused_pack_bytes[5],
2592 reused_pack_bytes[6],
2593 reused_pack_bytes[7],
2594 ]);
2595 if version != 2 {
2596 return Err(GitError::Unsupported(format!(
2597 "reused pack version {version}"
2598 )));
2599 }
2600 let count = u32::from_be_bytes([
2601 reused_pack_bytes[8],
2602 reused_pack_bytes[9],
2603 reused_pack_bytes[10],
2604 reused_pack_bytes[11],
2605 ]);
2606 reused_count = reused_count
2607 .checked_add(count)
2608 .ok_or_else(|| GitError::InvalidFormat("too many pack objects".into()))?;
2609 capacity = capacity.saturating_add(reused_pack_bytes.len().saturating_sub(12 + hash_len));
2610 }
2611 let total = reused_count
2612 .checked_add(appended.len() as u32)
2613 .ok_or_else(|| GitError::InvalidFormat("too many pack objects".into()))?;
2614
2615 let mut out = Vec::with_capacity(capacity);
2616 out.extend_from_slice(b"PACK");
2617 out.extend_from_slice(&2u32.to_be_bytes());
2618 out.extend_from_slice(&total.to_be_bytes());
2619 for reused_pack_bytes in reused_packs {
2620 out.extend_from_slice(&reused_pack_bytes[12..reused_pack_bytes.len() - hash_len]);
2621 }
2622 for input in appended {
2623 write_undeltified_pack_entry(&mut out, input.object)?;
2624 }
2625 let checksum = sley_core::digest_bytes(format, &out)?;
2626 out.extend_from_slice(checksum.as_bytes());
2627 Ok((out, reused_count))
2628}
2629
2630pub fn assemble_pack_with_verbatim_entries(
2633 format: ObjectFormat,
2634 reused_entries: &[&[u8]],
2635 appended: &[PackInput<'_>],
2636) -> Result<(Vec<u8>, u32)> {
2637 let reused_count = u32::try_from(reused_entries.len())
2638 .map_err(|_| GitError::InvalidFormat("too many pack objects".into()))?;
2639 let total = reused_count
2640 .checked_add(appended.len() as u32)
2641 .ok_or_else(|| GitError::InvalidFormat("too many pack objects".into()))?;
2642
2643 let mut capacity = 12 + format.raw_len() + 64 * appended.len();
2644 for entry in reused_entries {
2645 capacity = capacity.saturating_add(entry.len());
2646 }
2647 let mut out = Vec::with_capacity(capacity);
2648 out.extend_from_slice(b"PACK");
2649 out.extend_from_slice(&2u32.to_be_bytes());
2650 out.extend_from_slice(&total.to_be_bytes());
2651 for entry in reused_entries {
2652 out.extend_from_slice(entry);
2653 }
2654 for input in appended {
2655 write_undeltified_pack_entry(&mut out, input.object)?;
2656 }
2657 let checksum = sley_core::digest_bytes(format, &out)?;
2658 out.extend_from_slice(checksum.as_bytes());
2659 Ok((out, reused_count))
2660}
2661
2662fn write_undeltified_pack_entry(out: &mut Vec<u8>, object: &EncodedObject) -> Result<()> {
2664 let type_bits: u8 = match object.object_type {
2665 ObjectType::Commit => 1,
2666 ObjectType::Tree => 2,
2667 ObjectType::Blob => 3,
2668 ObjectType::Tag => 4,
2669 };
2670 let mut size = object.body.len() as u64;
2671 let mut byte = (type_bits << 4) | (size & 0x0f) as u8;
2672 size >>= 4;
2673 while size > 0 {
2674 out.push(byte | 0x80);
2675 byte = (size & 0x7f) as u8;
2676 size >>= 7;
2677 }
2678 out.push(byte);
2679 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
2680 encoder.write_all(&object.body)?;
2681 out.extend_from_slice(&encoder.finish()?);
2682 Ok(())
2683}
2684pub fn prune_unreachable_loose<I>(
2693 git_dir: &Path,
2694 format: ObjectFormat,
2695 roots: I,
2696 delete: bool,
2697) -> Result<Vec<ObjectId>>
2698where
2699 I: IntoIterator<Item = ObjectId>,
2700{
2701 prune_unreachable_loose_with_reachability(git_dir, format, roots, delete, false)
2702}
2703
2704pub fn prune_unreachable_loose_tolerating_missing<I>(
2709 git_dir: &Path,
2710 format: ObjectFormat,
2711 roots: I,
2712 delete: bool,
2713) -> Result<Vec<ObjectId>>
2714where
2715 I: IntoIterator<Item = ObjectId>,
2716{
2717 prune_unreachable_loose_with_reachability(git_dir, format, roots, delete, true)
2718}
2719
2720fn prune_unreachable_loose_with_reachability<I>(
2721 git_dir: &Path,
2722 format: ObjectFormat,
2723 roots: I,
2724 delete: bool,
2725 tolerate_missing: bool,
2726) -> Result<Vec<ObjectId>>
2727where
2728 I: IntoIterator<Item = ObjectId>,
2729{
2730 let objects_dir = repository_objects_dir(git_dir);
2731 let database = FileObjectDatabase::new(objects_dir.clone(), format);
2732 let reachable = if tolerate_missing {
2733 collect_reachable_object_ids_tolerating_missing(&database, format, roots)?
2734 } else {
2735 collect_reachable_object_ids(&database, format, roots)?
2736 };
2737
2738 let store = LooseObjectStore::new(objects_dir.clone(), format);
2739 let mut pruned: Vec<ObjectId> = loose_object_ids(&objects_dir, format)?
2740 .into_iter()
2741 .filter(|oid| !reachable.contains(oid))
2742 .collect();
2743 pruned.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
2744
2745 if delete {
2746 for oid in &pruned {
2747 let path = store.object_path(oid)?;
2748 match fs::remove_file(&path) {
2749 Ok(()) => {}
2750 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
2751 Err(err) => return Err(GitError::from(err)),
2752 }
2753 }
2754 }
2755 Ok(pruned)
2756}
2757
2758pub(crate) fn loose_object_ids(objects_dir: &Path, format: ObjectFormat) -> Result<Vec<ObjectId>> {
2761 let oids = loose_object_id_set(objects_dir, format)?;
2762 let mut oids = oids.into_iter().collect::<Vec<_>>();
2763 oids.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
2764 Ok(oids)
2765}
2766
2767pub(crate) fn loose_object_id_set(
2768 objects_dir: &Path,
2769 format: ObjectFormat,
2770) -> Result<HashSet<ObjectId>> {
2771 let mut oids = HashSet::new();
2772 collect_loose_object_ids(objects_dir, format, &mut oids)?;
2773 Ok(oids)
2774}
2775
2776pub(crate) fn existing_pack_files(pack_dir: &Path) -> Result<Vec<PathBuf>> {
2779 if !pack_dir.exists() {
2780 return Ok(Vec::new());
2781 }
2782 let mut packs = Vec::new();
2783 for entry in fs::read_dir(pack_dir)? {
2784 let path = entry?.path();
2785 if path.extension().and_then(|ext| ext.to_str()) == Some("pack") && path.is_file() {
2786 packs.push(path);
2787 }
2788 }
2789 packs.sort();
2790 Ok(packs)
2791}
2792
2793pub(crate) fn prune_obsolete_pack_paths(
2797 objects_dir: &Path,
2798 format: ObjectFormat,
2799 packs: &[PathBuf],
2800 keep: &Path,
2801 retained_pack_stems: &[String],
2802 prune_promisor: bool,
2803) -> Result<()> {
2804 prune_pack_paths_matching(
2805 objects_dir,
2806 format,
2807 packs.iter(),
2808 keep,
2809 retained_pack_stems,
2810 prune_promisor,
2811 |_| Ok(true),
2812 )
2813}
2814
2815fn prune_pack_paths_matching<'a>(
2816 objects_dir: &Path,
2817 format: ObjectFormat,
2818 packs: impl IntoIterator<Item = &'a PathBuf>,
2819 keep: &Path,
2820 retained_pack_stems: &[String],
2821 prune_promisor: bool,
2822 mut should_prune: impl FnMut(&Path) -> Result<bool>,
2823) -> Result<()> {
2824 let pack_dir = objects_dir.join("pack");
2825 let keep_stem = keep.file_stem().map(|stem| stem.to_owned());
2826 let retained_pack_stems: HashSet<&str> =
2827 retained_pack_stems.iter().map(String::as_str).collect();
2828 let mut removed_stems: HashSet<String> = HashSet::new();
2829
2830 for pack_path in packs {
2831 if pack_path == keep {
2832 continue;
2833 }
2834 let Some(stem) = pack_path.file_stem() else {
2835 continue;
2836 };
2837 if Some(stem) == keep_stem.as_deref() {
2838 continue;
2839 }
2840 if let Some(stem) = stem.to_str()
2841 && retained_pack_stems.contains(stem)
2842 {
2843 continue;
2844 }
2845 if pack_path.with_extension("keep").exists() {
2846 continue;
2847 }
2848 if pack_path.with_extension("promisor").exists() && !prune_promisor {
2849 continue;
2850 }
2851 if !should_prune(pack_path)? {
2852 continue;
2853 }
2854 remove_file_if_exists(pack_path)?;
2855 remove_file_if_exists(&pack_path.with_extension("idx"))?;
2856 for ext in ["rev", "mtimes", "bitmap", "promisor"] {
2857 remove_file_if_exists(&pack_path.with_extension(ext))?;
2858 }
2859 removed_stems.insert(stem.to_string_lossy().into_owned());
2860 }
2861
2862 prune_stale_multi_pack_index(&pack_dir, format, &removed_stems)?;
2863 Ok(())
2864}
2865
2866pub(crate) fn prune_stale_multi_pack_index(
2873 pack_dir: &Path,
2874 format: ObjectFormat,
2875 removed_stems: &HashSet<String>,
2876) -> Result<()> {
2877 if removed_stems.is_empty() {
2878 return Ok(());
2879 }
2880 let midx_path = pack_dir.join("multi-pack-index");
2881 if !midx_path.exists() {
2882 return Ok(());
2883 }
2884 let midx = MultiPackIndex::parse(&fs::read(&midx_path)?, format)?;
2885 let references_removed_pack = midx.pack_names.iter().any(|name| {
2886 let stem = name.strip_suffix(".idx").unwrap_or(name);
2887 removed_stems.contains(stem)
2888 });
2889 if references_removed_pack {
2890 remove_file_if_exists(&midx_path)?;
2893 if let Ok(entries) = fs::read_dir(pack_dir) {
2894 for entry in entries.flatten() {
2895 let path = entry.path();
2896 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
2897 continue;
2898 };
2899 if name.starts_with("multi-pack-index") {
2900 let _ = fs::remove_file(&path);
2901 }
2902 }
2903 }
2904 }
2905 Ok(())
2906}
2907
2908pub(crate) fn prune_loose_objects<'a, I>(
2911 objects_dir: &Path,
2912 format: ObjectFormat,
2913 candidates: I,
2914 present: &HashSet<ObjectId>,
2915) -> Result<()>
2916where
2917 I: IntoIterator<Item = &'a ObjectId>,
2918{
2919 let store = LooseObjectStore::new(objects_dir.to_path_buf(), format);
2920 for oid in candidates {
2921 if !present.contains(oid) {
2922 continue;
2923 }
2924 remove_file_if_exists(&store.object_path(oid)?)?;
2925 }
2926 Ok(())
2927}
2928
2929pub(crate) enum PackDeltaBase {
2930 Offset(u64),
2931 Ref(ObjectId),
2932}
2933
2934pub(crate) struct PackIndexOffsetInfo {
2935 pub(crate) end_offset: u64,
2936 pub(crate) delta_base_oid: Option<ObjectId>,
2937}
2938
2939pub(crate) fn scan_pack_index_offsets(
2940 index: &PackIndexViewData,
2941 target_offset: u64,
2942 trailer_offset: Option<u64>,
2943 delta_base_offset: Option<u64>,
2944) -> Result<PackIndexOffsetInfo> {
2945 let mut target_count = 0usize;
2946 let mut next_offset = None;
2947 let mut delta_base_oid = None;
2948
2949 for idx in 0..index.count {
2950 let Some(lookup) = index.lookup_at(idx) else {
2951 continue;
2952 };
2953 if lookup.offset == target_offset {
2954 target_count += 1;
2955 } else if lookup.offset > target_offset {
2956 match next_offset {
2957 Some(current) if current <= lookup.offset => {}
2958 _ => next_offset = Some(lookup.offset),
2959 }
2960 }
2961 if Some(lookup.offset) == delta_base_offset {
2962 delta_base_oid = Some(index.oid_at(idx)?);
2963 }
2964 }
2965
2966 if target_count == 0 {
2967 return Err(GitError::InvalidFormat(format!(
2968 "pack index offset {target_offset} not found"
2969 )));
2970 }
2971 if let Some(offset) = delta_base_offset
2972 && delta_base_oid.is_none()
2973 {
2974 return Err(GitError::InvalidFormat(format!(
2975 "ofs-delta base offset {offset} not found"
2976 )));
2977 }
2978
2979 Ok(PackIndexOffsetInfo {
2980 end_offset: if target_count > 1 {
2983 target_offset
2984 } else if let Some(offset) = next_offset {
2985 offset
2986 } else {
2987 trailer_offset.ok_or_else(|| {
2988 GitError::InvalidFormat("pack size unavailable for final indexed object".into())
2989 })?
2990 },
2991 delta_base_oid,
2992 })
2993}
2994
2995pub(crate) fn scan_pack_offsets_without_index(
2996 format: ObjectFormat,
2997 pack: &[u8],
2998 target_offset: u64,
2999) -> Result<Option<u64>> {
3000 let trailer_len = format.raw_len();
3001 if pack.len() < 12 + trailer_len {
3002 return Err(GitError::InvalidFormat("pack file too short".into()));
3003 }
3004 let trailer_offset = pack.len() - trailer_len;
3005 let checksum = sley_core::digest_bytes(format, &pack[..trailer_offset])?;
3006 let expected = ObjectId::from_raw(format, &pack[trailer_offset..])?;
3007 if checksum != expected {
3008 return Err(GitError::InvalidFormat(format!(
3009 "pack checksum mismatch: expected {expected}, got {checksum}"
3010 )));
3011 }
3012 if &pack[..4] != b"PACK" {
3013 return Err(GitError::InvalidFormat("missing PACK signature".into()));
3014 }
3015 let version = u32_be(&pack[4..8]);
3016 if version != 2 && version != 3 {
3017 return Err(GitError::Unsupported(format!("pack version {version}")));
3018 }
3019
3020 let count = u32_be(&pack[8..12]);
3021 let mut cursor = 12usize;
3022 for _ in 0..count {
3023 let entry_offset = cursor as u64;
3024 let first = pack_next_byte(pack, &mut cursor)?;
3025 let kind = (first >> 4) & 0x07;
3026 let mut byte = first;
3027 while byte & 0x80 != 0 {
3028 byte = pack_next_byte(pack, &mut cursor)?;
3029 }
3030 match kind {
3031 1..=4 => {}
3032 6 => {
3033 parse_ofs_delta_base_offset(pack, &mut cursor, entry_offset)?;
3034 }
3035 7 => {
3036 parse_ref_delta_base_oid(format, pack, &mut cursor)?;
3037 }
3038 _ => {
3039 return Err(GitError::InvalidFormat(format!(
3040 "invalid pack object kind {kind}"
3041 )));
3042 }
3043 }
3044 if cursor > trailer_offset {
3045 return Err(GitError::InvalidFormat(
3046 "pack entry extends past checksum".into(),
3047 ));
3048 }
3049 let consumed = inflate_pack_member_len(&pack[cursor..trailer_offset])?;
3050 if consumed == 0 {
3051 return Err(GitError::InvalidFormat(
3052 "empty compressed pack entry".into(),
3053 ));
3054 }
3055 cursor = cursor
3056 .checked_add(consumed)
3057 .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
3058 if cursor > trailer_offset {
3059 return Err(GitError::InvalidFormat(
3060 "pack entry extends past checksum".into(),
3061 ));
3062 }
3063 if entry_offset == target_offset {
3064 return Ok(Some(cursor as u64));
3065 }
3066 }
3067 if cursor != trailer_offset {
3068 return Err(GitError::InvalidFormat(format!(
3069 "pack has {} trailing bytes before checksum",
3070 trailer_offset - cursor
3071 )));
3072 }
3073 Ok(None)
3074}
3075
3076pub(crate) fn inflate_pack_member_len(compressed: &[u8]) -> Result<usize> {
3077 let mut decompress = Decompress::new(true);
3078 let mut input = compressed;
3079 let mut consumed_total = 0usize;
3080 let mut out = [0u8; 8192];
3081 loop {
3082 let before_in = decompress.total_in();
3083 let before_out = decompress.total_out();
3084 let status = decompress
3085 .decompress(input, &mut out, FlushDecompress::None)
3086 .map_err(|err| GitError::InvalidObject(format!("zlib inflate failed: {err}")))?;
3087 let consumed = (decompress.total_in() - before_in) as usize;
3088 let produced = decompress.total_out() - before_out;
3089 input = &input[consumed..];
3090 consumed_total += consumed;
3091 match status {
3092 flate2::Status::StreamEnd => return Ok(consumed_total),
3093 _ if consumed == 0 && produced == 0 => {
3094 return Err(GitError::InvalidObject("truncated zlib stream".into()));
3095 }
3096 _ => {}
3097 }
3098 }
3099}
3100
3101pub(crate) fn pack_entry_delta_base(
3102 format: ObjectFormat,
3103 pack: &[u8],
3104 entry_offset: u64,
3105) -> Result<Option<PackDeltaBase>> {
3106 let mut cursor = usize::try_from(entry_offset)
3107 .map_err(|_| GitError::InvalidFormat("pack entry offset overflows usize".into()))?;
3108 let first = pack_next_byte(pack, &mut cursor)?;
3109 let kind = (first >> 4) & 0x07;
3110 let mut byte = first;
3111 while byte & 0x80 != 0 {
3112 byte = pack_next_byte(pack, &mut cursor)?;
3113 }
3114 match kind {
3115 6 => Ok(Some(PackDeltaBase::Offset(parse_ofs_delta_base_offset(
3116 pack,
3117 &mut cursor,
3118 entry_offset,
3119 )?))),
3120 7 => Ok(Some(PackDeltaBase::Ref(parse_ref_delta_base_oid(
3121 format,
3122 pack,
3123 &mut cursor,
3124 )?))),
3125 _ => Ok(None),
3126 }
3127}
3128
3129fn parse_ref_delta_base_oid(
3130 format: ObjectFormat,
3131 pack: &[u8],
3132 cursor: &mut usize,
3133) -> Result<ObjectId> {
3134 let raw_len = format.raw_len();
3135 if *cursor + raw_len > pack.len() {
3136 return Err(GitError::InvalidFormat(
3137 "truncated ref-delta base object id".into(),
3138 ));
3139 }
3140 let oid = ObjectId::from_raw(format, &pack[*cursor..*cursor + raw_len])?;
3141 *cursor += raw_len;
3142 Ok(oid)
3143}
3144
3145fn parse_ofs_delta_base_offset(pack: &[u8], cursor: &mut usize, entry_offset: u64) -> Result<u64> {
3146 let relative = sley_core::primitives::read_biased_varint(pack, cursor).map_err(|err| {
3147 GitError::InvalidFormat(
3148 match err {
3149 sley_core::primitives::BiasedVarintError::Truncated => "truncated pack entry",
3150 sley_core::primitives::BiasedVarintError::Overflow => "ofs-delta offset overflow",
3151 }
3152 .into(),
3153 )
3154 })?;
3155 entry_offset
3156 .checked_sub(relative)
3157 .ok_or_else(|| GitError::InvalidFormat("ofs-delta points before pack start".into()))
3158}
3159
3160fn pack_next_byte(pack: &[u8], cursor: &mut usize) -> Result<u8> {
3161 let Some(byte) = pack.get(*cursor).copied() else {
3162 return Err(GitError::InvalidFormat("truncated pack entry".into()));
3163 };
3164 *cursor += 1;
3165 Ok(byte)
3166}
3167
3168pub(crate) fn remove_file_if_exists(path: &Path) -> Result<()> {
3170 match fs::remove_file(path) {
3171 Ok(()) => Ok(()),
3172 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
3173 Err(err) => Err(GitError::from(err)),
3174 }
3175}
3176
3177fn walk_reachable_objects<R, I, F>(
3178 reader: &R,
3179 format: ObjectFormat,
3180 starts: I,
3181 excluded: &HashSet<ObjectId>,
3182 visit: F,
3183) -> Result<HashSet<ObjectId>>
3184where
3185 R: ObjectReader,
3186 I: IntoIterator<Item = ObjectId>,
3187 F: FnMut(&ObjectId, &Arc<EncodedObject>),
3188{
3189 walk_reachable_objects_with_cut(reader, format, starts, excluded, &HashSet::new(), visit)
3190}
3191
3192fn walk_reachable_objects_with_cut<R, I, F>(
3196 reader: &R,
3197 format: ObjectFormat,
3198 starts: I,
3199 excluded: &HashSet<ObjectId>,
3200 cut: &HashSet<ObjectId>,
3201 mut visit: F,
3202) -> Result<HashSet<ObjectId>>
3203where
3204 R: ObjectReader,
3205 I: IntoIterator<Item = ObjectId>,
3206 F: FnMut(&ObjectId, &Arc<EncodedObject>),
3207{
3208 let mut seen = HashSet::new();
3209 let mut pending = Vec::new();
3210 for start in starts {
3211 pending.push(start);
3212 while let Some(oid) = pending.pop() {
3213 if excluded.contains(&oid) {
3214 continue;
3215 }
3216 if !seen.insert(oid) {
3217 continue;
3218 }
3219 let object = reader.read_object(&oid).map_err(|err| {
3220 with_missing_object_context(err, oid, MissingObjectContext::Traversal)
3221 })?;
3222 match object.object_type {
3223 ObjectType::Commit => {
3224 let (tree, parents) = {
3225 let commit = Commit::parse_ref(format, &object.body)?;
3226 (commit.tree, commit.parents)
3227 };
3228 visit(&oid, &object);
3229 if !cut.contains(&oid) {
3230 for parent in grafted_parents(reader, &oid, parents).into_iter().rev() {
3231 pending.push(parent);
3232 }
3233 }
3234 pending.push(tree);
3235 }
3236 ObjectType::Tree => {
3237 let mut child_oids = Vec::new();
3238 for entry in TreeEntries::new(format, &object.body) {
3239 let entry = entry?;
3240 if entry.is_gitlink() {
3241 continue;
3242 }
3243 child_oids.push(entry.oid);
3244 }
3245 visit(&oid, &object);
3246 pending.extend(child_oids.into_iter().rev());
3247 }
3248 ObjectType::Tag => {
3249 let target = {
3250 let tag = Tag::parse_ref(format, &object.body)?;
3251 tag.object
3252 };
3253 visit(&oid, &object);
3254 pending.push(target);
3255 }
3256 ObjectType::Blob => visit(&oid, &object),
3257 }
3258 }
3259 }
3260 Ok(seen)
3261}
3262
3263fn walk_reachable_objects_tolerating_missing<R, I>(
3264 reader: &R,
3265 format: ObjectFormat,
3266 starts: I,
3267) -> Result<HashSet<ObjectId>>
3268where
3269 R: ObjectReader,
3270 I: IntoIterator<Item = ObjectId>,
3271{
3272 let mut seen = HashSet::new();
3273 let mut pending: Vec<ObjectId> = starts.into_iter().collect();
3274 while let Some(oid) = pending.pop() {
3275 if !seen.insert(oid) {
3276 continue;
3277 }
3278 let object = match reader
3279 .read_object(&oid)
3280 .map_err(|err| with_missing_object_context(err, oid, MissingObjectContext::Traversal))
3281 {
3282 Ok(object) => object,
3283 Err(GitError::NotFound(_)) => continue,
3284 Err(err) => return Err(err),
3285 };
3286 match object.object_type {
3287 ObjectType::Commit => {
3288 let commit = Commit::parse_ref(format, &object.body)?;
3289 pending.extend(grafted_parents(reader, &oid, commit.parents));
3290 pending.push(commit.tree);
3291 }
3292 ObjectType::Tree => {
3293 for entry in TreeEntries::new(format, &object.body) {
3294 let entry = entry?;
3295 if !entry.is_gitlink() {
3296 pending.push(entry.oid);
3297 }
3298 }
3299 }
3300 ObjectType::Tag => {
3301 let tag = Tag::parse_ref(format, &object.body)?;
3302 pending.push(tag.object);
3303 }
3304 ObjectType::Blob => {}
3305 }
3306 }
3307 Ok(seen)
3308}
3309
3310#[derive(Debug, Clone)]
3313pub struct BitmapPseudoMergeGroup {
3314 pub commits: Vec<ObjectId>,
3315 pub exclude_selected: bool,
3316 pub partition: Option<BitmapPseudoMergePartition>,
3317}
3318
3319#[derive(Debug, Clone)]
3320pub struct BitmapPseudoMergePartition {
3321 pub max_merges: usize,
3322 pub decay: f64,
3323 pub sample_rate: f64,
3324}
3325
3326#[derive(Debug, Clone, Default)]
3330pub struct ReachabilityBitmapOptions {
3331 pub write_lookup_table: bool,
3332 pub name_hash_cache: Option<Vec<u32>>,
3333 pub restrict_to_tips: bool,
3337}
3338
3339fn bitset_get(words: &[u64], position: u32) -> bool {
3342 let word = (position / 64) as usize;
3343 word < words.len() && words[word] & (1u64 << (position % 64)) != 0
3344}
3345
3346fn bitset_set(words: &mut [u64], position: u32) {
3347 let word = (position / 64) as usize;
3348 if word < words.len() {
3349 words[word] |= 1u64 << (position % 64);
3350 }
3351}
3352
3353fn bitset_or(acc: &mut [u64], other: &[u64]) {
3354 for (dst, src) in acc.iter_mut().zip(other) {
3355 *dst |= *src;
3356 }
3357}
3358
3359fn bitset_is_subset(needles: &[u64], haystack: &[u64]) -> bool {
3360 needles
3361 .iter()
3362 .zip(haystack)
3363 .all(|(needle, hay)| needle & !hay == 0)
3364}
3365
3366fn bitset_positions(words: &[u64]) -> Vec<u32> {
3368 let mut positions = Vec::new();
3369 for (word_index, word) in words.iter().enumerate() {
3370 let mut remaining = *word;
3371 while remaining != 0 {
3372 let bit = remaining.trailing_zeros();
3373 positions.push(word_index as u32 * 64 + bit);
3374 remaining &= remaining - 1;
3375 }
3376 }
3377 positions
3378}
3379
3380fn commit_identity_timestamp(identity: &[u8]) -> i64 {
3384 let mut fields = identity.rsplitn(3, |byte| *byte == b' ');
3385 let _tz = fields.next();
3386 fields
3387 .next()
3388 .and_then(|raw| std::str::from_utf8(raw).ok())
3389 .and_then(|raw| raw.parse::<i64>().ok())
3390 .unwrap_or(0)
3391}
3392
3393fn bitmap_next_commit_index(idx: u32) -> u32 {
3396 const MIN_COMMITS: u32 = 100;
3397 const MAX_COMMITS: u32 = 5000;
3398 const MUST_REGION: u32 = 100;
3399 const MIN_REGION: u32 = 20000;
3400
3401 if idx <= MUST_REGION {
3402 return 0;
3403 }
3404 if idx <= MIN_REGION {
3405 let offset = idx - MUST_REGION;
3406 return offset.min(MIN_COMMITS);
3407 }
3408 let offset = idx - MIN_REGION;
3409 offset.clamp(MIN_COMMITS, MAX_COMMITS)
3410}
3411
3412pub fn build_pack_bitmap(
3426 db: &FileObjectDatabase,
3427 format: ObjectFormat,
3428 index_entries: &[PackIndexEntry],
3429 pack_checksum: &ObjectId,
3430 preferred_tips: &HashSet<ObjectId>,
3431 pseudo_merge_groups: &[BitmapPseudoMergeGroup],
3432) -> Result<Option<Vec<u8>>> {
3433 let mut by_offset: Vec<usize> = (0..index_entries.len()).collect();
3436 by_offset.sort_by_key(|&slot| index_entries[slot].offset);
3437 let bit_order: Vec<ObjectId> = by_offset
3438 .into_iter()
3439 .map(|slot| index_entries[slot].oid)
3440 .collect();
3441 build_reachability_bitmap(
3442 db,
3443 BitmapObjectTypes::Database(db),
3444 format,
3445 pack_checksum,
3446 &bit_order,
3447 preferred_tips,
3448 pseudo_merge_groups,
3449 &ReachabilityBitmapOptions::default(),
3450 )
3451}
3452
3453pub(crate) fn build_pack_name_hash_cache<R: ObjectReader>(
3458 db: &R,
3459 format: ObjectFormat,
3460 index_entries: &[PackIndexEntry],
3461 known_object_types: &HashMap<ObjectId, ObjectType>,
3462) -> Result<Vec<u32>> {
3463 let packed_objects = index_entries
3464 .iter()
3465 .map(|entry| {
3466 known_object_types
3467 .get(&entry.oid)
3468 .copied()
3469 .map(|object_type| (entry.oid, object_type))
3470 .ok_or_else(|| {
3471 GitError::InvalidFormat(format!(
3472 "repack bitmap type cache is missing packed object {}",
3473 entry.oid
3474 ))
3475 })
3476 })
3477 .collect::<Result<Vec<_>>>()?;
3478 let by_oid = build_pack_name_hashes(db, format, &packed_objects)?;
3479 let mut sorted: Vec<&PackIndexEntry> = index_entries.iter().collect();
3480 sorted.sort_by(|left, right| left.oid.as_bytes().cmp(right.oid.as_bytes()));
3481 Ok(sorted
3482 .into_iter()
3483 .map(|entry| by_oid.get(&entry.oid).copied().unwrap_or(0))
3484 .collect())
3485}
3486
3487pub(crate) fn build_pack_name_hashes<R: ObjectReader>(
3490 db: &R,
3491 format: ObjectFormat,
3492 packed_objects: &[(ObjectId, ObjectType)],
3493) -> Result<HashMap<ObjectId, u32>> {
3494 let packed: HashSet<ObjectId> = packed_objects.iter().map(|(oid, _)| *oid).collect();
3495 let mut by_oid = HashMap::new();
3496 let mut seen_trees = HashSet::new();
3497 for (oid, object_type) in packed_objects {
3498 if *object_type != ObjectType::Commit {
3499 continue;
3500 }
3501 let object = db.read_object(oid)?;
3502 let commit = Commit::parse_ref(format, &object.body)?;
3503 collect_tree_name_hashes(
3504 db,
3505 format,
3506 &commit.tree,
3507 &[],
3508 &packed,
3509 &mut seen_trees,
3510 &mut by_oid,
3511 )?;
3512 }
3513 Ok(by_oid)
3514}
3515
3516fn collect_tree_name_hashes<R: ObjectReader>(
3517 db: &R,
3518 format: ObjectFormat,
3519 tree: &ObjectId,
3520 prefix: &[u8],
3521 packed: &HashSet<ObjectId>,
3522 seen_trees: &mut HashSet<ObjectId>,
3523 by_oid: &mut HashMap<ObjectId, u32>,
3524) -> Result<()> {
3525 if !seen_trees.insert(*tree) {
3526 return Ok(());
3527 }
3528 let object = db.read_object(tree)?;
3529 for entry in TreeEntries::new(format, &object.body) {
3530 let entry = entry?;
3531 if entry.is_gitlink() {
3532 continue;
3533 }
3534 let mut path =
3535 Vec::with_capacity(prefix.len() + usize::from(!prefix.is_empty()) + entry.name.len());
3536 path.extend_from_slice(prefix);
3537 if !prefix.is_empty() {
3538 path.push(b'/');
3539 }
3540 path.extend_from_slice(entry.name);
3541 if packed.contains(&entry.oid) {
3542 by_oid
3543 .entry(entry.oid)
3544 .or_insert_with(|| pack_name_hash(&path));
3545 }
3546 if entry.is_tree() {
3547 collect_tree_name_hashes(db, format, &entry.oid, &path, packed, seen_trees, by_oid)?;
3548 }
3549 }
3550 Ok(())
3551}
3552
3553pub(crate) fn pack_name_hash(name: &[u8]) -> u32 {
3554 let mut hash = 0u32;
3555 for &byte in name {
3556 if byte.is_ascii_whitespace() {
3557 continue;
3558 }
3559 hash = (hash >> 2).wrapping_add(u32::from(byte) << 24);
3560 }
3561 hash
3562}
3563
3564pub fn build_midx_bitmap(
3570 db: &FileObjectDatabase,
3571 format: ObjectFormat,
3572 midx_entries: &[sley_pack::MultiPackIndexEntry],
3573 midx_checksum: &ObjectId,
3574 preferred_pack: u32,
3575 preferred_tips: &HashSet<ObjectId>,
3576 pseudo_merge_groups: &[BitmapPseudoMergeGroup],
3577) -> Result<Option<Vec<u8>>> {
3578 build_midx_bitmap_with_options(
3579 db,
3580 format,
3581 midx_entries,
3582 midx_checksum,
3583 preferred_pack,
3584 preferred_tips,
3585 pseudo_merge_groups,
3586 &ReachabilityBitmapOptions::default(),
3587 )
3588}
3589
3590#[allow(clippy::too_many_arguments)]
3591pub fn build_midx_bitmap_with_options(
3592 db: &FileObjectDatabase,
3593 format: ObjectFormat,
3594 midx_entries: &[sley_pack::MultiPackIndexEntry],
3595 midx_checksum: &ObjectId,
3596 preferred_pack: u32,
3597 preferred_tips: &HashSet<ObjectId>,
3598 pseudo_merge_groups: &[BitmapPseudoMergeGroup],
3599 options: &ReachabilityBitmapOptions,
3600) -> Result<Option<Vec<u8>>> {
3601 let mut pseudo: Vec<usize> = (0..midx_entries.len()).collect();
3602 pseudo.sort_by_key(|&slot| {
3603 let entry = &midx_entries[slot];
3604 (
3605 entry.pack_int_id != preferred_pack,
3606 entry.pack_int_id,
3607 entry.offset,
3608 )
3609 });
3610 let bit_order: Vec<ObjectId> = pseudo
3611 .into_iter()
3612 .map(|slot| midx_entries[slot].oid)
3613 .collect();
3614 build_reachability_bitmap(
3615 db,
3616 BitmapObjectTypes::Database(db),
3617 format,
3618 midx_checksum,
3619 &bit_order,
3620 preferred_tips,
3621 pseudo_merge_groups,
3622 options,
3623 )
3624}
3625
3626#[allow(clippy::too_many_arguments)]
3631pub(crate) fn build_pack_bitmap_with_cached_objects<R: ObjectReader>(
3632 db: &R,
3633 format: ObjectFormat,
3634 index_entries: &[PackIndexEntry],
3635 pack_checksum: &ObjectId,
3636 preferred_tips: &HashSet<ObjectId>,
3637 pseudo_merge_groups: &[BitmapPseudoMergeGroup],
3638 known_object_types: &HashMap<ObjectId, ObjectType>,
3639 options: &ReachabilityBitmapOptions,
3640) -> Result<Option<Vec<u8>>> {
3641 let mut by_offset: Vec<usize> = (0..index_entries.len()).collect();
3642 by_offset.sort_by_key(|&slot| index_entries[slot].offset);
3643 let bit_order: Vec<ObjectId> = by_offset
3644 .into_iter()
3645 .map(|slot| index_entries[slot].oid)
3646 .collect();
3647 build_reachability_bitmap(
3648 db,
3649 BitmapObjectTypes::Cached(known_object_types),
3650 format,
3651 pack_checksum,
3652 &bit_order,
3653 preferred_tips,
3654 pseudo_merge_groups,
3655 options,
3656 )
3657}
3658
3659enum BitmapObjectTypes<'a> {
3660 Database(&'a FileObjectDatabase),
3661 Cached(&'a HashMap<ObjectId, ObjectType>),
3662}
3663
3664fn bitmap_num_maximal_commits(
3672 db: &impl ObjectReader,
3673 format: ObjectFormat,
3674 selected: &[ObjectId],
3675) -> Result<usize> {
3676 let mut first_parent: HashMap<ObjectId, Option<ObjectId>> = HashMap::new();
3678 let mut stack: Vec<ObjectId> = selected.to_vec();
3679 while let Some(oid) = stack.pop() {
3680 if first_parent.contains_key(&oid) {
3681 continue;
3682 }
3683 let object = db.read_object(&oid)?;
3684 let commit = Commit::parse_ref(format, &object.body)?;
3685 let parent = grafted_parents(db, &oid, commit.parents).first().copied();
3686 first_parent.insert(oid, parent);
3687 if let Some(parent) = parent {
3688 stack.push(parent);
3689 }
3690 }
3691 let mut pending_children: HashMap<ObjectId, usize> = HashMap::new();
3693 for parent in first_parent.values().flatten() {
3694 *pending_children.entry(*parent).or_default() += 1;
3695 }
3696 let word_count = selected.len().div_ceil(64);
3697 struct MaximalEnt {
3698 mask: Vec<u64>,
3699 maximal: bool,
3700 }
3701 let mut ents: HashMap<ObjectId, MaximalEnt> = HashMap::new();
3702 for (bit, oid) in selected.iter().enumerate() {
3703 let ent = ents.entry(*oid).or_insert_with(|| MaximalEnt {
3704 mask: vec![0u64; word_count],
3705 maximal: true,
3706 });
3707 ent.mask[bit / 64] |= 1u64 << (bit % 64);
3708 ent.maximal = true;
3709 }
3710 let mut queue: Vec<ObjectId> = first_parent
3711 .keys()
3712 .filter(|oid| pending_children.get(*oid).copied().unwrap_or(0) == 0)
3713 .copied()
3714 .collect();
3715 let mut num_maximal = 0usize;
3716 while let Some(oid) = queue.pop() {
3717 if let Some(ent) = ents.remove(&oid) {
3718 if ent.maximal {
3719 num_maximal += 1;
3720 }
3721 if let Some(Some(parent)) = first_parent.get(&oid) {
3722 match ents.entry(*parent) {
3723 std::collections::hash_map::Entry::Vacant(vacant) => {
3724 vacant.insert(MaximalEnt {
3726 mask: ent.mask.clone(),
3727 maximal: false,
3728 });
3729 }
3730 std::collections::hash_map::Entry::Occupied(mut occupied) => {
3731 let parent_ent = occupied.get_mut();
3732 let c_not_p = ent
3733 .mask
3734 .iter()
3735 .zip(&parent_ent.mask)
3736 .any(|(child, parent)| child & !parent != 0);
3737 if c_not_p {
3738 let p_not_c = parent_ent
3739 .mask
3740 .iter()
3741 .zip(&ent.mask)
3742 .any(|(parent, child)| parent & !child != 0);
3743 for (parent, child) in parent_ent.mask.iter_mut().zip(&ent.mask) {
3744 *parent |= child;
3745 }
3746 parent_ent.maximal = p_not_c;
3747 }
3748 }
3749 }
3750 }
3751 }
3752 if let Some(Some(parent)) = first_parent.get(&oid)
3753 && let Some(remaining) = pending_children.get_mut(parent)
3754 {
3755 *remaining -= 1;
3756 if *remaining == 0 {
3757 queue.push(*parent);
3758 }
3759 }
3760 }
3761 Ok(num_maximal)
3762}
3763
3764#[allow(clippy::too_many_arguments)]
3768fn build_reachability_bitmap<R: ObjectReader>(
3769 db: &R,
3770 object_types_source: BitmapObjectTypes<'_>,
3771 format: ObjectFormat,
3772 checksum: &ObjectId,
3773 bit_order: &[ObjectId],
3774 preferred_tips: &HashSet<ObjectId>,
3775 pseudo_merge_groups: &[BitmapPseudoMergeGroup],
3776 options: &ReachabilityBitmapOptions,
3777) -> Result<Option<Vec<u8>>> {
3778 if bit_order.is_empty() || bit_order.len() > u32::MAX as usize {
3779 return Ok(None);
3780 }
3781 let object_count = bit_order.len();
3782
3783 let mut oid_sorted: Vec<u32> = (0..object_count as u32).collect();
3786 oid_sorted.sort_by(|&left, &right| {
3787 bit_order[left as usize]
3788 .as_bytes()
3789 .cmp(bit_order[right as usize].as_bytes())
3790 });
3791 let mut index_position = vec![0u32; object_count];
3792 for (position, &slot) in oid_sorted.iter().enumerate() {
3793 index_position[slot as usize] = position as u32;
3794 }
3795 let mut oid_to_pack = HashMap::with_capacity(object_count);
3796 for (pack_pos, oid) in bit_order.iter().enumerate() {
3797 oid_to_pack.insert(*oid, pack_pos as u32);
3798 }
3799 let mut object_types = Vec::with_capacity(object_count);
3801 struct IndexedCommit {
3802 oid: ObjectId,
3803 pack_pos: u32,
3804 index_pos: u32,
3805 date: i64,
3806 parent_count: usize,
3807 }
3808 let mut indexed_commits = Vec::new();
3809 for (pack_pos, oid) in bit_order.iter().enumerate() {
3810 let object_type = match &object_types_source {
3813 BitmapObjectTypes::Database(database) => match database.read_object_header(oid)? {
3814 Some((object_type, _)) => object_type,
3815 None => db.read_object(oid)?.object_type,
3816 },
3817 BitmapObjectTypes::Cached(object_types) => {
3818 object_types.get(oid).copied().ok_or_else(|| {
3819 GitError::InvalidFormat(format!(
3820 "repack bitmap cache is missing packed object {oid}"
3821 ))
3822 })?
3823 }
3824 };
3825 object_types.push(object_type);
3826 if object_type == ObjectType::Commit {
3827 let object = db.read_object(oid)?;
3828 let commit = Commit::parse_ref(format, &object.body)?;
3829 indexed_commits.push(IndexedCommit {
3830 oid: *oid,
3831 pack_pos: pack_pos as u32,
3832 index_pos: index_position[pack_pos],
3833 date: commit_identity_timestamp(commit.committer),
3834 parent_count: grafted_parents(db, oid, commit.parents).len(),
3835 });
3836 }
3837 }
3838 if options.restrict_to_tips {
3839 let visible = bitmap_visible_commits(db, format, preferred_tips)?;
3840 indexed_commits.retain(|commit| visible.contains(&commit.oid));
3841 }
3842
3843 indexed_commits.sort_by_key(|commit| std::cmp::Reverse(commit.date));
3845 let mut selected: Vec<&IndexedCommit> = Vec::new();
3846 let commit_count = indexed_commits.len() as u32;
3847 if commit_count < 100 {
3848 selected.extend(indexed_commits.iter());
3849 } else {
3850 let mut i = 0u32;
3851 loop {
3852 let next = bitmap_next_commit_index(i);
3853 if i + next >= commit_count {
3854 break;
3855 }
3856 let mut chosen = &indexed_commits[(i + next) as usize];
3857 if next > 0 {
3858 for j in 0..=next {
3859 let candidate = &indexed_commits[(i + j) as usize];
3860 if preferred_tips.contains(&candidate.oid) {
3861 chosen = candidate;
3862 break;
3863 }
3864 if candidate.parent_count >= 2 {
3865 chosen = candidate;
3866 }
3867 }
3868 }
3869 selected.push(chosen);
3870 i += next + 1;
3871 }
3872 }
3873
3874 if std::env::var_os("GIT_TRACE2_EVENT").is_some() {
3879 let selected_oids: Vec<ObjectId> = selected.iter().map(|commit| commit.oid).collect();
3880 let num_maximal = bitmap_num_maximal_commits(db, format, &selected_oids)?;
3881 sley_core::trace2::data("pack-bitmap-write", "num_selected_commits", selected.len());
3882 sley_core::trace2::data("pack-bitmap-write", "num_maximal_commits", num_maximal);
3883 let reusable_pseudo_merges = pseudo_merge_groups
3884 .iter()
3885 .filter(|group| !group.exclude_selected)
3886 .count();
3887 sley_core::trace2::data(
3888 "pack-bitmap-write",
3889 "building_bitmaps_pseudo_merge_reused",
3890 reusable_pseudo_merges,
3891 );
3892 }
3893
3894 let word_count = object_count.div_ceil(64);
3897 let mut memo: HashMap<ObjectId, Arc<Vec<u64>>> = HashMap::new();
3898 for commit in selected.iter().rev() {
3899 let Some(acc) =
3900 bitmap_commit_closure(db, format, &[commit.oid], &oid_to_pack, word_count, &memo)?
3901 else {
3902 return Ok(None);
3903 };
3904 memo.insert(commit.oid, Arc::new(acc));
3905 }
3906
3907 let mut writer = PackBitmapWriter::new(format, *checksum, &object_types)?
3908 .with_lookup_table(options.write_lookup_table);
3909 if let Some(cache) = &options.name_hash_cache {
3910 writer = writer.with_name_hash_cache(cache.clone())?;
3911 }
3912 for commit in &selected {
3913 let words = match memo.get(&commit.oid) {
3914 Some(words) => words,
3915 None => continue,
3916 };
3917 writer.add_commit(commit.pack_pos, commit.index_pos, &bitset_positions(words))?;
3918 }
3919 if !pseudo_merge_groups.is_empty() {
3920 let selected_oids: HashSet<ObjectId> = selected.iter().map(|commit| commit.oid).collect();
3921 for group in pseudo_merge_groups {
3922 let mut commits = Vec::new();
3923 for oid in &group.commits {
3924 if group.exclude_selected && selected_oids.contains(oid) {
3925 continue;
3926 }
3927 let Some(&pack_pos) = oid_to_pack.get(oid) else {
3928 continue;
3929 };
3930 if object_types.get(pack_pos as usize) != Some(&ObjectType::Commit) {
3931 continue;
3932 }
3933 commits.push((*oid, pack_pos));
3934 }
3935 if commits.is_empty() {
3936 continue;
3937 }
3938 if let Some(partition) = &group.partition {
3939 let mut start = 0usize;
3940 for merge_index in 0..partition.max_merges {
3941 if start >= commits.len() {
3942 break;
3943 }
3944 let size = bitmap_pseudo_merge_group_size(
3945 partition.max_merges,
3946 partition.decay,
3947 commits.len(),
3948 merge_index,
3949 );
3950 let end = if size < 8 {
3951 commits.len()
3952 } else {
3953 start.saturating_add(size).min(commits.len())
3954 };
3955 let sample_stride = if partition.sample_rate <= 0.0 {
3956 usize::MAX
3957 } else {
3958 ((1.0 / partition.sample_rate) as usize).max(1)
3959 };
3960 let sampled: Vec<(ObjectId, u32)> = commits[start..end]
3961 .iter()
3962 .enumerate()
3963 .filter(|(offset, _candidate)| *offset % sample_stride == 0)
3964 .map(|(_offset, candidate)| *candidate)
3965 .collect();
3966 if !sampled.is_empty()
3967 && !bitmap_add_pseudo_merge(
3968 &mut writer,
3969 db,
3970 format,
3971 &sampled,
3972 &oid_to_pack,
3973 word_count,
3974 &memo,
3975 )?
3976 {
3977 return Ok(None);
3978 }
3979 start = end;
3980 if end >= commits.len() {
3981 break;
3982 }
3983 }
3984 } else if !bitmap_add_pseudo_merge(
3985 &mut writer,
3986 db,
3987 format,
3988 &commits,
3989 &oid_to_pack,
3990 word_count,
3991 &memo,
3992 )? {
3993 return Ok(None);
3994 }
3995 }
3996 }
3997 writer.write().map(Some)
3998}
3999
4000fn bitmap_visible_commits(
4001 db: &impl ObjectReader,
4002 format: ObjectFormat,
4003 tips: &HashSet<ObjectId>,
4004) -> Result<HashSet<ObjectId>> {
4005 let mut visible = HashSet::new();
4006 let mut pending: Vec<ObjectId> = tips.iter().copied().collect();
4007 while let Some(oid) = pending.pop() {
4008 if !visible.insert(oid) {
4009 continue;
4010 }
4011 let object = db.read_object(&oid)?;
4012 let commit = Commit::parse_ref(format, &object.body)?;
4013 pending.extend(grafted_parents(db, &oid, commit.parents));
4014 }
4015 Ok(visible)
4016}
4017
4018fn bitmap_pseudo_merge_group_size(
4019 max_merges: usize,
4020 decay: f64,
4021 unstable_len: usize,
4022 index: usize,
4023) -> usize {
4024 let mut scale = 0.0;
4025 for n in 0..max_merges {
4026 scale += 1.0 / ((n + 1) as f64).powf(decay);
4027 }
4028 if scale == 0.0 {
4029 return 0;
4030 }
4031 ((unstable_len as f64 / scale) / ((index + 1) as f64).powf(decay) + 0.5) as usize
4032}
4033
4034fn bitmap_add_pseudo_merge(
4035 writer: &mut PackBitmapWriter,
4036 db: &impl ObjectReader,
4037 format: ObjectFormat,
4038 commits: &[(ObjectId, u32)],
4039 oid_to_pack: &HashMap<ObjectId, u32>,
4040 word_count: usize,
4041 memo: &HashMap<ObjectId, Arc<Vec<u64>>>,
4042) -> Result<bool> {
4043 let roots: Vec<ObjectId> = commits.iter().map(|(oid, _position)| *oid).collect();
4044 let Some(words) = bitmap_commit_closure(db, format, &roots, oid_to_pack, word_count, memo)?
4045 else {
4046 return Ok(false);
4047 };
4048 let commit_positions: Vec<u32> = commits.iter().map(|(_oid, position)| *position).collect();
4049 writer.add_pseudo_merge(&commit_positions, &bitset_positions(&words))?;
4050 Ok(true)
4051}
4052
4053fn bitmap_commit_closure(
4054 db: &impl ObjectReader,
4055 format: ObjectFormat,
4056 roots: &[ObjectId],
4057 oid_to_pack: &HashMap<ObjectId, u32>,
4058 word_count: usize,
4059 memo: &HashMap<ObjectId, Arc<Vec<u64>>>,
4060) -> Result<Option<Vec<u64>>> {
4061 let mut acc = vec![0u64; word_count];
4062 let mut pending = roots.to_vec();
4063 while let Some(oid) = pending.pop() {
4064 let Some(&pack_pos) = oid_to_pack.get(&oid) else {
4065 sley_core::diagnostic!(
4066 Stderr,
4067 true,
4068 "warning: Failed to write bitmap index. Packfile doesn't have full closure (object {oid} is missing)"
4069 );
4070 return Ok(None);
4071 };
4072 if bitset_get(&acc, pack_pos) {
4073 continue;
4074 }
4075 if let Some(stored) = memo.get(&oid) {
4076 bitset_or(&mut acc, stored);
4077 continue;
4078 }
4079 bitset_set(&mut acc, pack_pos);
4080 let object = db.read_object(&oid)?;
4081 let parsed = Commit::parse_ref(format, &object.body)?;
4082 pending.extend(grafted_parents(db, &oid, parsed.parents));
4083 if !bitmap_mark_tree(db, format, &parsed.tree, oid_to_pack, &mut acc)? {
4084 return Ok(None);
4085 }
4086 }
4087 Ok(Some(acc))
4088}
4089
4090fn bitmap_mark_tree(
4094 db: &impl ObjectReader,
4095 format: ObjectFormat,
4096 tree: &ObjectId,
4097 oid_to_pack: &HashMap<ObjectId, u32>,
4098 acc: &mut [u64],
4099) -> Result<bool> {
4100 let Some(&pack_pos) = oid_to_pack.get(tree) else {
4101 sley_core::diagnostic!(
4102 Stderr,
4103 true,
4104 "warning: Failed to write bitmap index. Packfile doesn't have full closure (object {tree} is missing)"
4105 );
4106 return Ok(false);
4107 };
4108 if bitset_get(acc, pack_pos) {
4109 return Ok(true);
4110 }
4111 bitset_set(acc, pack_pos);
4112 let object = db.read_object(tree)?;
4113 for entry in TreeEntries::new(format, &object.body) {
4114 let entry = entry?;
4115 if entry.is_gitlink() {
4116 continue;
4117 }
4118 if entry.is_tree() {
4119 if !bitmap_mark_tree(db, format, &entry.oid, oid_to_pack, acc)? {
4120 return Ok(false);
4121 }
4122 } else {
4123 let Some(&blob_pos) = oid_to_pack.get(&entry.oid) else {
4124 sley_core::diagnostic!(
4125 Stderr,
4126 true,
4127 "warning: Failed to write bitmap index. Packfile doesn't have full closure (object {} is missing)",
4128 entry.oid
4129 );
4130 return Ok(false);
4131 };
4132 bitset_set(acc, blob_pos);
4133 }
4134 }
4135 Ok(true)
4136}
4137
4138pub struct LoadedPackBitmap {
4142 object_count: u32,
4143 oid_to_pack: HashMap<ObjectId, u32>,
4144 pack_to_oid: Vec<ObjectId>,
4145 commit_words: HashMap<ObjectId, Arc<Vec<u64>>>,
4146 pseudo_merges: Vec<LoadedPseudoMerge>,
4147 commits: Vec<u64>,
4148 trees: Vec<u64>,
4149 blobs: Vec<u64>,
4150 tags: Vec<u64>,
4151}
4152
4153struct LoadedPseudoMerge {
4154 commits: Arc<Vec<u64>>,
4155 bitmap: Arc<Vec<u64>>,
4156}
4157
4158impl LoadedPackBitmap {
4159 pub fn object_count(&self) -> u32 {
4160 self.object_count
4161 }
4162
4163 pub fn pack_position(&self, oid: &ObjectId) -> Option<u32> {
4165 self.oid_to_pack.get(oid).copied()
4166 }
4167
4168 pub fn oid_at(&self, position: u32) -> Option<&ObjectId> {
4169 self.pack_to_oid.get(position as usize)
4170 }
4171
4172 pub fn bitmap_for_commit(&self, oid: &ObjectId) -> Option<&Arc<Vec<u64>>> {
4175 self.commit_words.get(oid)
4176 }
4177
4178 pub fn bitmapped_commits(&self) -> impl Iterator<Item = &ObjectId> {
4180 self.commit_words.keys()
4181 }
4182
4183 pub fn pseudo_merge_count(&self) -> usize {
4184 self.pseudo_merges.len()
4185 }
4186
4187 pub fn pseudo_merge_words(&self, index: usize) -> Option<(&[u64], &[u64])> {
4188 self.pseudo_merges
4189 .get(index)
4190 .map(|merge| (merge.commits.as_slice(), merge.bitmap.as_slice()))
4191 }
4192
4193 pub fn type_words(&self, object_type: ObjectType) -> &[u64] {
4195 match object_type {
4196 ObjectType::Commit => &self.commits,
4197 ObjectType::Tree => &self.trees,
4198 ObjectType::Blob => &self.blobs,
4199 ObjectType::Tag => &self.tags,
4200 }
4201 }
4202
4203 fn word_count(&self) -> usize {
4204 (self.object_count as usize).div_ceil(64)
4205 }
4206}
4207
4208pub fn load_pack_bitmap(
4215 objects_dir: &Path,
4216 format: ObjectFormat,
4217) -> Result<Option<LoadedPackBitmap>> {
4218 let pack_dir = objects_dir.join("pack");
4219 if !pack_dir.exists() {
4220 return Ok(None);
4221 }
4222 if let Some(bitmap) = load_incremental_midx_bitmap(&pack_dir, format)? {
4225 return Ok(Some(bitmap));
4226 }
4227 if let Some(bitmap) = load_midx_bitmap(&pack_dir, format)? {
4228 return Ok(Some(bitmap));
4229 }
4230 let mut bitmap_paths = Vec::new();
4231 for entry in fs::read_dir(&pack_dir)? {
4232 let path = entry?.path();
4233 if path.extension().and_then(|ext| ext.to_str()) == Some("bitmap")
4234 && path
4235 .file_name()
4236 .and_then(|name| name.to_str())
4237 .is_some_and(|name| name.starts_with("pack-"))
4238 {
4239 bitmap_paths.push(path);
4240 }
4241 }
4242 bitmap_paths.sort();
4243 let has_extra_bitmap = bitmap_paths.len() > 1;
4244 for bitmap_path in bitmap_paths {
4245 match load_pack_bitmap_file(&bitmap_path, format) {
4246 Ok(Some(bitmap)) => {
4247 sley_core::trace2::data("bitmap", "message", "opened bitmap");
4248 if has_extra_bitmap {
4249 sley_core::trace2::data("bitmap", "message", "ignoring extra bitmap");
4250 }
4251 return Ok(Some(bitmap));
4252 }
4253 Ok(None) => continue,
4254 Err(err) => {
4255 let message = err.to_string();
4256 if message.contains("EWAH") {
4257 sley_core::diagnostic!(Stderr, true, "error: corrupt ewah bitmap: {message}");
4258 } else {
4259 sley_core::diagnostic!(
4260 Stderr,
4261 true,
4262 "error: corrupted bitmap index: {message}"
4263 );
4264 }
4265 continue;
4266 }
4267 }
4268 }
4269 Ok(None)
4270}
4271
4272fn load_incremental_midx_bitmap(
4273 pack_dir: &Path,
4274 format: ObjectFormat,
4275) -> Result<Option<LoadedPackBitmap>> {
4276 let chain = read_incremental_midx_chain(pack_dir)?;
4277 if chain.is_empty() {
4278 return Ok(None);
4279 }
4280 let midx_dir = pack_dir.join("multi-pack-index.d");
4281 if !chain.iter().any(|checksum| {
4282 midx_dir
4283 .join(format!("multi-pack-index-{checksum}.bitmap"))
4284 .exists()
4285 }) {
4286 return Ok(None);
4287 }
4288
4289 let mut pack_to_oid = Vec::new();
4290 for checksum in &chain {
4291 let path = midx_dir.join(format!("multi-pack-index-{checksum}.midx"));
4292 let Ok(bytes) = fs::read(path) else {
4293 return Ok(None);
4294 };
4295 let Ok(midx) = MultiPackIndex::parse(&bytes, format) else {
4296 return Ok(None);
4297 };
4298 let positions: Vec<usize> = match &midx.reverse_index {
4299 Some(reverse) => reverse.iter().map(|position| *position as usize).collect(),
4300 None => {
4301 let mut positions: Vec<usize> = (0..midx.objects.len()).collect();
4302 positions.sort_by_key(|&position| {
4303 let entry = &midx.objects[position];
4304 (entry.pack_int_id, entry.offset)
4305 });
4306 positions
4307 }
4308 };
4309 for position in positions {
4310 let Some(entry) = midx.objects.get(position) else {
4311 return Ok(None);
4312 };
4313 pack_to_oid.push(entry.oid);
4314 }
4315 }
4316
4317 let object_count = pack_to_oid.len();
4318 if object_count == 0 || object_count > u32::MAX as usize {
4319 return Ok(None);
4320 }
4321 let mut oid_to_pack = HashMap::with_capacity(object_count);
4322 for (position, oid) in pack_to_oid.iter().enumerate() {
4323 oid_to_pack.insert(*oid, position as u32);
4324 }
4325
4326 let Some(objects_dir) = pack_dir.parent() else {
4327 return Ok(None);
4328 };
4329 let db = FileObjectDatabase::new(objects_dir.to_path_buf(), format);
4330 let word_count = object_count.div_ceil(64);
4331 let mut commits = vec![0u64; word_count];
4332 let mut trees = vec![0u64; word_count];
4333 let mut blobs = vec![0u64; word_count];
4334 let mut tags = vec![0u64; word_count];
4335 let mut commit_oids = Vec::new();
4336 for (position, oid) in pack_to_oid.iter().enumerate() {
4337 let Ok(Some((object_type, _size))) = db.read_object_header(oid) else {
4338 return Ok(None);
4339 };
4340 let position = position as u32;
4341 match object_type {
4342 ObjectType::Commit => {
4343 bitset_set(&mut commits, position);
4344 commit_oids.push(*oid);
4345 }
4346 ObjectType::Tree => bitset_set(&mut trees, position),
4347 ObjectType::Blob => bitset_set(&mut blobs, position),
4348 ObjectType::Tag => bitset_set(&mut tags, position),
4349 }
4350 }
4351
4352 let mut loaded = LoadedPackBitmap {
4353 object_count: object_count as u32,
4354 oid_to_pack,
4355 pack_to_oid,
4356 commit_words: HashMap::new(),
4357 pseudo_merges: Vec::new(),
4358 commits,
4359 trees,
4360 blobs,
4361 tags,
4362 };
4363 for oid in commit_oids {
4364 let result = bitmap_reachable(&loaded, &db, format, &[oid], true)?;
4365 if result.extended.is_empty() {
4366 loaded.commit_words.insert(oid, Arc::new(result.words));
4367 }
4368 }
4369 Ok(Some(loaded))
4370}
4371
4372fn load_midx_bitmap(pack_dir: &Path, format: ObjectFormat) -> Result<Option<LoadedPackBitmap>> {
4377 let midx_path = pack_dir.join("multi-pack-index");
4378 if !midx_path.exists() {
4379 return Ok(None);
4380 }
4381 let Ok(midx_bytes) = fs::read(&midx_path) else {
4382 return Ok(None);
4383 };
4384 if midx_has_bad_ridx_chunk(&midx_bytes, format) {
4385 sley_core::diagnostic!(
4386 Stderr,
4387 true,
4388 "error: multi-pack-index reverse-index chunk is the wrong size"
4389 );
4390 sley_core::diagnostic!(
4391 Stderr,
4392 true,
4393 "warning: multi-pack bitmap is missing required reverse index"
4394 );
4395 return Ok(None);
4396 }
4397 let midx = match MultiPackIndex::parse(&midx_bytes, format) {
4398 Ok(midx) => midx,
4399 Err(GitError::InvalidFormat(message))
4400 if message == "multi-pack-index reverse-index chunk is the wrong size" =>
4401 {
4402 sley_core::diagnostic!(Stderr, true, "error: {message}");
4403 sley_core::diagnostic!(
4404 Stderr,
4405 true,
4406 "warning: multi-pack bitmap is missing required reverse index"
4407 );
4408 return Ok(None);
4409 }
4410 Err(_) => return Ok(None),
4411 };
4412 let bitmap_path = pack_dir.join(format!(
4413 "multi-pack-index-{}.bitmap",
4414 midx.checksum.to_hex()
4415 ));
4416 if !bitmap_path.exists() {
4417 return Ok(None);
4418 }
4419 let object_count = midx.objects.len();
4420 let read_ridx_chunk = env::var("GIT_TEST_MIDX_READ_RIDX")
4425 .map(|value| value != "0" && !value.eq_ignore_ascii_case("false"))
4426 .unwrap_or(true);
4427 let reverse_index: Vec<u32> = match (&midx.reverse_index, read_ridx_chunk) {
4428 (Some(chunk), true) => {
4429 sley_core::trace2::data("load_midx_revindex", "source", "midx");
4430 chunk.clone()
4431 }
4432 _ => {
4433 let rev_path =
4434 pack_dir.join(format!("multi-pack-index-{}.rev", midx.checksum.to_hex()));
4435 let Ok(rev_bytes) = fs::read(&rev_path) else {
4436 return Ok(None);
4438 };
4439 let Ok(parsed_rev) =
4440 sley_pack::PackReverseIndex::parse(&rev_bytes, format, object_count)
4441 else {
4442 return Ok(None);
4443 };
4444 sley_core::trace2::data("load_midx_revindex", "source", "rev");
4445 parsed_rev.positions
4446 }
4447 };
4448 let Ok(bitmap_bytes) = fs::read(&bitmap_path) else {
4449 return Ok(None);
4450 };
4451 let parsed = match PackBitmapIndex::parse(&bitmap_bytes, format, object_count) {
4452 Ok(parsed) => parsed,
4453 Err(_) => return Ok(None),
4454 };
4455 if parsed.pack_checksum != midx.checksum {
4456 return Ok(None);
4457 }
4458
4459 let mut pack_to_oid = Vec::with_capacity(object_count);
4462 for &midx_pos in &reverse_index {
4463 let Some(entry) = midx.objects.get(midx_pos as usize) else {
4464 return Ok(None);
4465 };
4466 pack_to_oid.push(entry.oid);
4467 }
4468 let mut oid_to_pack = HashMap::with_capacity(object_count);
4469 for (pack_pos, oid) in pack_to_oid.iter().enumerate() {
4470 oid_to_pack.insert(*oid, pack_pos as u32);
4471 }
4472 match assemble_loaded_bitmap(parsed, object_count, pack_to_oid, oid_to_pack, |position| {
4473 midx.objects.get(position).map(|entry| entry.oid)
4474 }) {
4475 Ok(loaded) => Ok(Some(loaded)),
4476 Err(_) => Ok(None),
4477 }
4478}
4479
4480fn midx_has_bad_ridx_chunk(bytes: &[u8], format: ObjectFormat) -> bool {
4481 let hash_len = format.raw_len();
4482 if bytes.len() < 12 + 12 + hash_len || &bytes[..4] != b"MIDX" {
4483 return false;
4484 }
4485 let chunk_count = bytes[6] as usize;
4486 let table_len = match (chunk_count + 1).checked_mul(12) {
4487 Some(table_len) => table_len,
4488 None => return false,
4489 };
4490 let table_end = match 12usize.checked_add(table_len) {
4491 Some(table_end) if table_end <= bytes.len().saturating_sub(hash_len) => table_end,
4492 _ => return false,
4493 };
4494 let mut entries = Vec::with_capacity(chunk_count + 1);
4495 let mut cursor = 12usize;
4496 while cursor < table_end {
4497 let id = [
4498 bytes[cursor],
4499 bytes[cursor + 1],
4500 bytes[cursor + 2],
4501 bytes[cursor + 3],
4502 ];
4503 let mut raw_offset = [0u8; 8];
4504 raw_offset.copy_from_slice(&bytes[cursor + 4..cursor + 12]);
4505 entries.push((id, u64::from_be_bytes(raw_offset) as usize));
4506 cursor += 12;
4507 }
4508 let mut oidf = None;
4509 let mut ridx = None;
4510 for pair in entries.windows(2) {
4511 let start = pair[0].1;
4512 let end = pair[1].1;
4513 if end < start || end > bytes.len().saturating_sub(hash_len) {
4514 return false;
4515 }
4516 match &pair[0].0 {
4517 b"OIDF" => oidf = Some((start, end)),
4518 b"RIDX" => ridx = Some((start, end)),
4519 _ => {}
4520 }
4521 }
4522 let Some((oidf_start, oidf_end)) = oidf else {
4523 return false;
4524 };
4525 let Some((ridx_start, ridx_end)) = ridx else {
4526 return false;
4527 };
4528 if oidf_end.saturating_sub(oidf_start) != 256 * 4 {
4529 return false;
4530 }
4531 let object_count_start = oidf_end - 4;
4532 let object_count = u32::from_be_bytes([
4533 bytes[object_count_start],
4534 bytes[object_count_start + 1],
4535 bytes[object_count_start + 2],
4536 bytes[object_count_start + 3],
4537 ]) as usize;
4538 ridx_end.saturating_sub(ridx_start) != object_count.saturating_mul(4)
4539}
4540
4541fn load_pack_bitmap_file(
4542 bitmap_path: &Path,
4543 format: ObjectFormat,
4544) -> Result<Option<LoadedPackBitmap>> {
4545 let index_path = bitmap_path.with_extension("idx");
4546 if !index_path.exists() {
4547 return Ok(None);
4548 }
4549 let index = PackIndex::parse(&fs::read(&index_path)?, format)?;
4550 let object_count = index.entries.len();
4551 let parsed = PackBitmapIndex::parse(&fs::read(bitmap_path)?, format, object_count)?;
4552 if parsed.pack_checksum != index.pack_checksum {
4553 return Ok(None);
4554 }
4555
4556 let mut pack_order: Vec<u32> = (0..object_count as u32).collect();
4557 pack_order.sort_by_key(|index_pos| index.entries[*index_pos as usize].offset);
4558 let mut pack_to_oid = Vec::with_capacity(object_count);
4559 for index_pos in &pack_order {
4560 pack_to_oid.push(index.entries[*index_pos as usize].oid);
4561 }
4562 let mut oid_to_pack = HashMap::with_capacity(object_count);
4563 for (pack_pos, oid) in pack_to_oid.iter().enumerate() {
4564 oid_to_pack.insert(*oid, pack_pos as u32);
4565 }
4566
4567 assemble_loaded_bitmap(parsed, object_count, pack_to_oid, oid_to_pack, |position| {
4568 index.entries.get(position).map(|entry| entry.oid)
4569 })
4570 .map(Some)
4571}
4572
4573fn assemble_loaded_bitmap(
4578 parsed: PackBitmapIndex,
4579 object_count: usize,
4580 pack_to_oid: Vec<ObjectId>,
4581 oid_to_pack: HashMap<ObjectId, u32>,
4582 lookup_oid: impl Fn(usize) -> Option<ObjectId>,
4583) -> Result<LoadedPackBitmap> {
4584 let word_count = object_count.div_ceil(64);
4585 let expand = |bitmap: &sley_pack::EwahBitmap| -> Result<Vec<u64>> {
4586 let mut words = bitmap.to_words()?;
4587 words.resize(word_count, 0);
4588 Ok(words)
4589 };
4590
4591 let mut resolved: Vec<Arc<Vec<u64>>> = Vec::with_capacity(parsed.entries.len());
4592 let mut commit_words = HashMap::with_capacity(parsed.entries.len());
4593 for (entry_index, entry) in parsed.entries.iter().enumerate() {
4594 let mut words = expand(&entry.bitmap)?;
4595 if entry.xor_offset > 0 {
4596 let base_index = entry_index - entry.xor_offset as usize;
4597 let base = &resolved[base_index];
4598 for (dst, src) in words.iter_mut().zip(base.iter()) {
4599 *dst ^= *src;
4600 }
4601 }
4602 let words = Arc::new(words);
4603 resolved.push(Arc::clone(&words));
4604 let commit_oid = lookup_oid(entry.object_position as usize)
4605 .ok_or_else(|| GitError::InvalidFormat("bitmap entry position out of range".into()))?;
4606 commit_words.insert(commit_oid, words);
4607 }
4608 let mut pseudo_merges = Vec::with_capacity(parsed.pseudo_merges.len());
4609 for merge in &parsed.pseudo_merges {
4610 pseudo_merges.push(LoadedPseudoMerge {
4611 commits: Arc::new(expand(&merge.commits)?),
4612 bitmap: Arc::new(expand(&merge.bitmap)?),
4613 });
4614 }
4615
4616 Ok(LoadedPackBitmap {
4617 object_count: object_count as u32,
4618 oid_to_pack,
4619 pack_to_oid,
4620 commit_words,
4621 pseudo_merges,
4622 commits: expand(&parsed.type_bitmaps.commits)?,
4623 trees: expand(&parsed.type_bitmaps.trees)?,
4624 blobs: expand(&parsed.type_bitmaps.blobs)?,
4625 tags: expand(&parsed.type_bitmaps.tags)?,
4626 })
4627}
4628
4629pub struct BitmapWalkResult {
4633 pub words: Vec<u64>,
4634 pub extended: Vec<(ObjectId, ObjectType)>,
4635 pub pseudo_merges_satisfied: usize,
4636 pub pseudo_merges_cascades: usize,
4637}
4638
4639#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4641pub enum BitmapHaveTraversal {
4642 #[default]
4644 Classic,
4645 Boundary,
4648}
4649
4650impl BitmapHaveTraversal {
4651 fn trace_label(self) -> &'static str {
4652 match self {
4653 Self::Classic => "haves/classic",
4654 Self::Boundary => "haves/boundary",
4655 }
4656 }
4657}
4658
4659impl BitmapWalkResult {
4660 pub fn subtract(&mut self, haves: &BitmapWalkResult) {
4662 for (dst, src) in self.words.iter_mut().zip(haves.words.iter()) {
4663 *dst &= !*src;
4664 }
4665 let have_ext: HashSet<ObjectId> = haves.extended.iter().map(|(oid, _)| *oid).collect();
4666 self.extended.retain(|(oid, _)| !have_ext.contains(oid));
4667 }
4668
4669 fn union(&mut self, other: &BitmapWalkResult) {
4670 for (dst, src) in self.words.iter_mut().zip(other.words.iter()) {
4671 *dst |= *src;
4672 }
4673 let mut seen = self
4674 .extended
4675 .iter()
4676 .map(|(oid, _)| *oid)
4677 .collect::<HashSet<_>>();
4678 self.extended.extend(
4679 other
4680 .extended
4681 .iter()
4682 .filter(|(oid, _)| seen.insert(*oid))
4683 .copied(),
4684 );
4685 self.pseudo_merges_satisfied += other.pseudo_merges_satisfied;
4686 self.pseudo_merges_cascades += other.pseudo_merges_cascades;
4687 }
4688}
4689
4690pub fn bitmap_reachable(
4701 bitmap: &LoadedPackBitmap,
4702 db: &impl ObjectReader,
4703 format: ObjectFormat,
4704 roots: &[ObjectId],
4705 include_objects: bool,
4706) -> Result<BitmapWalkResult> {
4707 bitmap_reachable_fill(bitmap, db, format, roots, include_objects)
4708}
4709
4710pub fn bitmap_reachable_excluding_haves(
4714 bitmap: &LoadedPackBitmap,
4715 db: &impl ObjectReader,
4716 format: ObjectFormat,
4717 want_roots: &[ObjectId],
4718 have_roots: &[ObjectId],
4719 include_objects: bool,
4720 have_traversal: BitmapHaveTraversal,
4721) -> Result<BitmapWalkResult> {
4722 let mut result = bitmap_reachable(bitmap, db, format, want_roots, include_objects)?;
4723 if have_roots.is_empty() {
4724 return Ok(result);
4725 }
4726 sley_core::trace2::region("bitmap", have_traversal.trace_label());
4727 let haves = match have_traversal {
4728 BitmapHaveTraversal::Classic => {
4729 bitmap_reachable(bitmap, db, format, have_roots, include_objects)?
4730 }
4731 BitmapHaveTraversal::Boundary => {
4732 bitmap_boundary_haves(bitmap, db, format, want_roots, have_roots, include_objects)?.0
4733 }
4734 };
4735 result.subtract(&haves);
4736 Ok(result)
4737}
4738
4739#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
4740struct BitmapBoundaryStats {
4741 have_commits_walked: usize,
4742 want_commits_walked: usize,
4743 boundary_tips_filled: usize,
4744}
4745
4746fn bitmap_boundary_haves(
4752 bitmap: &LoadedPackBitmap,
4753 db: &impl ObjectReader,
4754 format: ObjectFormat,
4755 want_roots: &[ObjectId],
4756 have_roots: &[ObjectId],
4757 include_objects: bool,
4758) -> Result<(BitmapWalkResult, BitmapBoundaryStats)> {
4759 let mut base = BitmapWalkResult {
4760 words: vec![0; bitmap.word_count()],
4761 extended: Vec::new(),
4762 pseudo_merges_satisfied: 0,
4763 pseudo_merges_cascades: 0,
4764 };
4765 let mut stats = BitmapBoundaryStats::default();
4766 let mut have_commits = HashSet::new();
4767 let mut pending = have_roots.to_vec();
4768 while let Some(oid) = pending.pop() {
4769 if bitmap_result_contains(&base, bitmap, &oid) || !have_commits.insert(oid) {
4770 continue;
4771 }
4772 let object = db.read_object(&oid)?;
4773 if object.object_type != ObjectType::Commit {
4774 return Ok((
4777 bitmap_reachable(bitmap, db, format, have_roots, include_objects)?,
4778 stats,
4779 ));
4780 }
4781 if bitmap.bitmap_for_commit(&oid).is_some() {
4782 let covered = bitmap_reachable(bitmap, db, format, &[oid], include_objects)?;
4783 base.union(&covered);
4784 continue;
4785 }
4786 stats.have_commits_walked += 1;
4787 let commit = Commit::parse_ref(format, &object.body)?;
4788 pending.extend(grafted_parents(db, &oid, commit.parents));
4789 }
4790
4791 let mut boundary = Vec::new();
4792 let mut seen_wants = HashSet::new();
4793 let mut pending = want_roots.to_vec();
4794 while let Some(oid) = pending.pop() {
4795 if !seen_wants.insert(oid) {
4796 continue;
4797 }
4798 if have_commits.contains(&oid) || bitmap_result_contains(&base, bitmap, &oid) {
4799 boundary.push(oid);
4800 continue;
4801 }
4802 let object = db.read_object(&oid)?;
4803 if object.object_type != ObjectType::Commit {
4804 return Ok((
4805 bitmap_reachable(bitmap, db, format, have_roots, include_objects)?,
4806 stats,
4807 ));
4808 }
4809 stats.want_commits_walked += 1;
4810 let commit = Commit::parse_ref(format, &object.body)?;
4811 pending.extend(grafted_parents(db, &oid, commit.parents));
4812 }
4813
4814 boundary.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
4815 boundary.dedup();
4816 boundary.retain(|oid| !bitmap_result_contains(&base, bitmap, oid));
4817 stats.boundary_tips_filled = boundary.len();
4818 if !boundary.is_empty() {
4819 let fill = bitmap_reachable(bitmap, db, format, &boundary, include_objects)?;
4820 base.union(&fill);
4821 }
4822 Ok((base, stats))
4823}
4824
4825fn bitmap_result_contains(
4826 result: &BitmapWalkResult,
4827 bitmap: &LoadedPackBitmap,
4828 oid: &ObjectId,
4829) -> bool {
4830 match bitmap.pack_position(oid) {
4831 Some(position) => bitset_get(&result.words, position),
4832 None => result
4833 .extended
4834 .iter()
4835 .any(|(candidate, _)| candidate == oid),
4836 }
4837}
4838
4839#[cfg(test)]
4840mod bitmap_boundary_tests {
4841 use super::*;
4842
4843 #[test]
4844 fn boundary_haves_match_classic_while_filling_only_intersection_tip() {
4845 for format in [ObjectFormat::Sha1, ObjectFormat::Sha256] {
4846 let root = unique_temp_path(&env::temp_dir());
4847 let db = FileObjectDatabase::new(root.join("objects"), format);
4848 let tree = db
4849 .write_object(EncodedObject::new(ObjectType::Tree, Vec::new()))
4850 .expect("write tree");
4851 let base = write_commit(&db, format, tree, &[], b"base\n");
4852 let shared = write_commit(&db, format, tree, &[base], b"shared\n");
4853 let want = write_commit(&db, format, tree, &[shared], b"want\n");
4854 let unrelated_have = write_commit(&db, format, tree, &[shared], b"have\n");
4855 let bitmap = empty_loaded_bitmap();
4856
4857 let classic = bitmap_reachable_excluding_haves(
4858 &bitmap,
4859 &db,
4860 format,
4861 &[want],
4862 &[unrelated_have],
4863 true,
4864 BitmapHaveTraversal::Classic,
4865 )
4866 .expect("classic have traversal");
4867 let boundary = bitmap_reachable_excluding_haves(
4868 &bitmap,
4869 &db,
4870 format,
4871 &[want],
4872 &[unrelated_have],
4873 true,
4874 BitmapHaveTraversal::Boundary,
4875 )
4876 .expect("boundary have traversal");
4877 assert_eq!(object_set(&classic), object_set(&boundary));
4878 assert_eq!(object_set(&boundary), HashSet::from([want]));
4879
4880 let (boundary_haves, stats) =
4881 bitmap_boundary_haves(&bitmap, &db, format, &[want], &[unrelated_have], true)
4882 .expect("boundary have set");
4883 assert_eq!(stats.have_commits_walked, 3);
4884 assert_eq!(stats.want_commits_walked, 1);
4885 assert_eq!(stats.boundary_tips_filled, 1);
4886 let covered = object_set(&boundary_haves);
4887 assert!(covered.contains(&shared));
4888 assert!(covered.contains(&base));
4889 assert!(!covered.contains(&unrelated_have));
4890 fs::remove_dir_all(root).expect("remove test repository");
4891 }
4892 }
4893
4894 fn write_commit(
4895 db: &FileObjectDatabase,
4896 format: ObjectFormat,
4897 tree: ObjectId,
4898 parents: &[ObjectId],
4899 message: &[u8],
4900 ) -> ObjectId {
4901 db.write_object(EncodedObject::new(
4902 ObjectType::Commit,
4903 Commit {
4904 tree,
4905 parents: parents.to_vec(),
4906 author: b"A <a@example.com> 1 +0000".to_vec(),
4907 committer: b"A <a@example.com> 1 +0000".to_vec(),
4908 encoding: None,
4909 message: message.to_vec(),
4910 }
4911 .write(),
4912 ))
4913 .unwrap_or_else(|error| panic!("write {format:?} commit: {error}"))
4914 }
4915
4916 fn empty_loaded_bitmap() -> LoadedPackBitmap {
4917 LoadedPackBitmap {
4918 object_count: 0,
4919 oid_to_pack: HashMap::new(),
4920 pack_to_oid: Vec::new(),
4921 commit_words: HashMap::new(),
4922 pseudo_merges: Vec::new(),
4923 commits: Vec::new(),
4924 trees: Vec::new(),
4925 blobs: Vec::new(),
4926 tags: Vec::new(),
4927 }
4928 }
4929
4930 fn object_set(result: &BitmapWalkResult) -> HashSet<ObjectId> {
4931 result.extended.iter().map(|(oid, _)| *oid).collect()
4932 }
4933}
4934
4935fn bitmap_reachable_fill(
4936 bitmap: &LoadedPackBitmap,
4937 db: &impl ObjectReader,
4938 format: ObjectFormat,
4939 roots: &[ObjectId],
4940 include_objects: bool,
4941) -> Result<BitmapWalkResult> {
4942 let mut walk = BitmapFillWalk {
4943 bitmap,
4944 words: vec![0u64; bitmap.word_count()],
4945 extended: Vec::new(),
4946 extended_seen: HashSet::new(),
4947 };
4948 let mut commit_stack = Vec::new();
4949
4950 for root in roots {
4951 let mut oid = *root;
4952 loop {
4954 let object = db.read_object(&oid)?;
4955 match object.object_type {
4956 ObjectType::Tag => {
4957 walk.mark(&oid, ObjectType::Tag);
4958 let tag = Tag::parse_ref(format, &object.body)?;
4959 oid = tag.object;
4960 }
4961 ObjectType::Commit => {
4962 commit_stack.push(oid);
4963 break;
4964 }
4965 ObjectType::Tree => {
4966 walk.mark_tree_closure(db, format, &oid)?;
4967 break;
4968 }
4969 ObjectType::Blob => {
4970 walk.mark(&oid, ObjectType::Blob);
4971 break;
4972 }
4973 }
4974 }
4975 }
4976
4977 while let Some(oid) = commit_stack.pop() {
4978 if let Some(position) = bitmap.pack_position(&oid) {
4979 if bitset_get(&walk.words, position) {
4980 continue;
4981 }
4982 if let Some(stored) = bitmap.bitmap_for_commit(&oid) {
4983 bitset_or(&mut walk.words, stored);
4984 continue;
4985 }
4986 bitset_set(&mut walk.words, position);
4987 } else {
4988 if walk.extended_seen.contains(&oid) {
4989 continue;
4990 }
4991 walk.extended_seen.insert(oid);
4992 walk.extended.push((oid, ObjectType::Commit));
4993 }
4994 let object = db.read_object(&oid)?;
4995 let commit = Commit::parse_ref(format, &object.body)?;
4996 commit_stack.extend(grafted_parents(db, &oid, commit.parents));
4997 if include_objects {
4998 walk.mark_tree_closure(db, format, &commit.tree)?;
4999 }
5000 }
5001
5002 let (pseudo_merges_satisfied, pseudo_merges_cascades) =
5003 bitmap_cascade_pseudo_merges(bitmap, &mut walk.words);
5004
5005 Ok(BitmapWalkResult {
5006 words: walk.words,
5007 extended: walk.extended,
5008 pseudo_merges_satisfied,
5009 pseudo_merges_cascades,
5010 })
5011}
5012
5013fn bitmap_cascade_pseudo_merges(bitmap: &LoadedPackBitmap, words: &mut [u64]) -> (usize, usize) {
5014 if bitmap.pseudo_merges.is_empty() {
5015 return (0, 0);
5016 }
5017 let mut satisfied = vec![false; bitmap.pseudo_merges.len()];
5018 let mut total = 0usize;
5019 loop {
5020 let mut any = false;
5021 for (index, merge) in bitmap.pseudo_merges.iter().enumerate() {
5022 if satisfied[index] || !bitset_is_subset(merge.commits.as_slice(), words) {
5023 continue;
5024 }
5025 bitset_or(words, merge.bitmap.as_slice());
5026 satisfied[index] = true;
5027 any = true;
5028 total += 1;
5029 }
5030 if !any {
5031 break;
5032 }
5033 }
5034 (total, usize::from(total > 0))
5035}
5036
5037struct BitmapFillWalk<'a> {
5038 bitmap: &'a LoadedPackBitmap,
5039 words: Vec<u64>,
5040 extended: Vec<(ObjectId, ObjectType)>,
5041 extended_seen: HashSet<ObjectId>,
5042}
5043
5044impl BitmapFillWalk<'_> {
5045 fn mark(&mut self, oid: &ObjectId, object_type: ObjectType) -> bool {
5047 if let Some(position) = self.bitmap.pack_position(oid) {
5048 if bitset_get(&self.words, position) {
5049 return false;
5050 }
5051 bitset_set(&mut self.words, position);
5052 true
5053 } else {
5054 if !self.extended_seen.insert(*oid) {
5055 return false;
5056 }
5057 self.extended.push((*oid, object_type));
5058 true
5059 }
5060 }
5061
5062 fn mark_tree_closure(
5066 &mut self,
5067 db: &impl ObjectReader,
5068 format: ObjectFormat,
5069 tree: &ObjectId,
5070 ) -> Result<()> {
5071 if !self.mark(tree, ObjectType::Tree) {
5072 return Ok(());
5073 }
5074 let object = db.read_object(tree)?;
5075 for entry in TreeEntries::new(format, &object.body) {
5076 let entry = entry?;
5077 if entry.is_gitlink() {
5078 continue;
5079 }
5080 if entry.is_tree() {
5081 self.mark_tree_closure(db, format, &entry.oid)?;
5082 } else {
5083 self.mark(&entry.oid, ObjectType::Blob);
5084 }
5085 }
5086 Ok(())
5087 }
5088}
5089
5090#[cfg(test)]
5091mod bitmap_name_hash_tests {
5092 use super::pack_name_hash;
5093
5094 #[test]
5095 fn v1_name_hash_matches_upstream_stability_vectors() {
5096 assert_eq!(pack_name_hash(b"first"), 2_582_249_472);
5097 assert_eq!(pack_name_hash(b"second"), 2_289_942_528);
5098 assert_eq!(pack_name_hash(b"third"), 2_300_837_888);
5099 assert_eq!(
5100 pack_name_hash(b"a/one-long-enough-for-collisions"),
5101 2_544_516_325
5102 );
5103 }
5104}
5105
5106#[cfg(test)]
5107mod reachable_pack_staging_tests {
5108 use super::*;
5109
5110 #[derive(Default)]
5111 struct MapReader {
5112 objects: HashMap<ObjectId, Arc<EncodedObject>>,
5113 }
5114
5115 impl ObjectReader for MapReader {
5116 fn read_object(&self, oid: &ObjectId) -> Result<Arc<EncodedObject>> {
5117 self.objects
5118 .get(oid)
5119 .cloned()
5120 .ok_or_else(|| GitError::object_not_found_in(*oid, MissingObjectContext::Read))
5121 }
5122 }
5123
5124 fn blob_reader(count: usize) -> (MapReader, Vec<ObjectId>) {
5125 let format = ObjectFormat::Sha1;
5126 let mut reader = MapReader::default();
5127 let mut starts = Vec::new();
5128 for index in 0..count {
5129 let object = Arc::new(EncodedObject::new(
5130 ObjectType::Blob,
5131 format!("blob-{index}").into_bytes(),
5132 ));
5133 let oid = object.object_id(format).expect("blob object id");
5134 reader.objects.insert(oid, object);
5135 starts.push(oid);
5136 }
5137 (reader, starts)
5138 }
5139
5140 fn insert_object(reader: &mut MapReader, object: EncodedObject) -> ObjectId {
5141 let object = Arc::new(object);
5142 let oid = object.object_id(ObjectFormat::Sha1).expect("object id");
5143 reader.objects.insert(oid, object);
5144 oid
5145 }
5146
5147 fn one_entry_tree(name: &[u8], oid: ObjectId) -> EncodedObject {
5148 let mut body = b"100644 ".to_vec();
5149 body.extend_from_slice(name);
5150 body.push(0);
5151 body.extend_from_slice(oid.as_bytes());
5152 EncodedObject::new(ObjectType::Tree, body)
5153 }
5154
5155 fn commit_for_tree(tree: ObjectId, message: &str) -> EncodedObject {
5156 EncodedObject::new(
5157 ObjectType::Commit,
5158 format!(
5159 "tree {}\nauthor A <a@example.com> 1 +0000\ncommitter A <a@example.com> 1 +0000\n\n{message}\n",
5160 tree.to_hex()
5161 )
5162 .into_bytes(),
5163 )
5164 }
5165
5166 #[test]
5167 fn natural_and_repack_traversal_select_their_established_first_path() {
5168 let mut reader = MapReader::default();
5169 let shared = insert_object(
5170 &mut reader,
5171 EncodedObject::new(ObjectType::Blob, b"shared".to_vec()),
5172 );
5173 let first_tree = insert_object(&mut reader, one_entry_tree(b"first", shared));
5174 let second_tree = insert_object(&mut reader, one_entry_tree(b"second", shared));
5175 let first = insert_object(&mut reader, commit_for_tree(first_tree, "first"));
5176 let second = insert_object(&mut reader, commit_for_tree(second_tree, "second"));
5177
5178 let natural = collect_reachable_pack_objects_for_write_with_order(
5179 &reader,
5180 ObjectFormat::Sha1,
5181 [first, second],
5182 &HashSet::new(),
5183 false,
5184 ReachablePackTraversal::Natural,
5185 )
5186 .expect("natural traversal");
5187 let legacy = collect_reachable_pack_objects_for_write_with_order(
5188 &reader,
5189 ObjectFormat::Sha1,
5190 [first, second],
5191 &HashSet::new(),
5192 false,
5193 ReachablePackTraversal::RepackLegacy,
5194 )
5195 .expect("legacy traversal");
5196
5197 let ReachablePackObjectsForWrite::Buffered {
5198 objects: natural_objects,
5199 name_hashes: natural_hashes,
5200 } = natural
5201 else {
5202 panic!("small natural traversal must stay buffered");
5203 };
5204 let ReachablePackObjectsForWrite::Buffered {
5205 objects: legacy_objects,
5206 name_hashes: legacy_hashes,
5207 } = legacy
5208 else {
5209 panic!("small legacy traversal must stay buffered");
5210 };
5211 assert_eq!(
5212 natural_objects
5213 .iter()
5214 .map(|entry| entry.oid)
5215 .collect::<Vec<_>>(),
5216 vec![first, first_tree, shared, second, second_tree]
5217 );
5218 assert_eq!(
5219 legacy_objects
5220 .iter()
5221 .map(|entry| entry.oid)
5222 .collect::<Vec<_>>(),
5223 vec![second, second_tree, shared, first, first_tree]
5224 );
5225 assert_eq!(natural_hashes[&shared], pack_name_hash(b"first"));
5226 assert_eq!(legacy_hashes[&shared], pack_name_hash(b"second"));
5227 }
5228
5229 #[test]
5230 fn reachable_pack_streaming_threshold_is_exact_and_canonical() {
5231 assert_eq!(REACHABLE_PACK_STREAMING_MIN_OBJECTS, 32);
5232 let (reader, starts) = blob_reader(REACHABLE_PACK_STREAMING_MIN_OBJECTS - 1);
5233 let buffered = collect_reachable_pack_objects_for_write(
5234 &reader,
5235 ObjectFormat::Sha1,
5236 starts,
5237 &HashSet::new(),
5238 )
5239 .expect("collect buffered objects");
5240 assert!(matches!(
5241 buffered,
5242 ReachablePackObjectsForWrite::Buffered { objects, .. }
5243 if objects.len() == REACHABLE_PACK_STREAMING_MIN_OBJECTS - 1
5244 ));
5245
5246 let (reader, starts) = blob_reader(REACHABLE_PACK_STREAMING_MIN_OBJECTS);
5247 let traversal_order = starts.clone();
5248 let streaming = collect_reachable_pack_objects_for_write(
5249 &reader,
5250 ObjectFormat::Sha1,
5251 starts.iter().copied(),
5252 &HashSet::new(),
5253 )
5254 .expect("collect streaming metadata");
5255 assert!(matches!(
5256 streaming,
5257 ReachablePackObjectsForWrite::Streaming(metadata)
5258 if metadata.len() == REACHABLE_PACK_STREAMING_MIN_OBJECTS
5259 ));
5260
5261 let mut pack = Vec::new();
5262 write_reachable_pack_to_writer_with_options_and_cancel(
5263 &reader,
5264 ObjectFormat::Sha1,
5265 starts,
5266 &HashSet::new(),
5267 &PackWriteOptions::new().with_reorder(false),
5268 &mut pack,
5269 CancelFlag::never(),
5270 )
5271 .expect("write traversal-ordered pack")
5272 .expect("non-empty traversal-ordered pack");
5273 let parsed = PackFile::parse(&pack, ObjectFormat::Sha1).expect("parse traversal pack");
5274 assert_eq!(
5275 parsed
5276 .entries
5277 .iter()
5278 .map(|entry| entry.entry.oid)
5279 .collect::<Vec<_>>(),
5280 traversal_order,
5281 "disabling reorder must preserve traversal order across the streaming threshold"
5282 );
5283
5284 let oid = |suffix: &str| {
5285 ObjectId::from_hex(
5286 ObjectFormat::Sha1,
5287 &format!("000000000000000000000000000000000000000{suffix}"),
5288 )
5289 .expect("planning object id")
5290 };
5291 let mut metadata = vec![
5292 ReachablePackObjectMeta {
5293 oid: oid("4"),
5294 object_type: ObjectType::Commit,
5295 size: 1,
5296 name_hash: 0,
5297 },
5298 ReachablePackObjectMeta {
5299 oid: oid("3"),
5300 object_type: ObjectType::Tree,
5301 size: 1,
5302 name_hash: 0,
5303 },
5304 ReachablePackObjectMeta {
5305 oid: oid("2"),
5306 object_type: ObjectType::Blob,
5307 size: 1,
5308 name_hash: 0,
5309 },
5310 ReachablePackObjectMeta {
5311 oid: oid("1"),
5312 object_type: ObjectType::Tag,
5313 size: 1,
5314 name_hash: 0,
5315 },
5316 ];
5317 sort_reachable_pack_metadata(&mut metadata);
5318 assert_eq!(
5319 metadata
5320 .iter()
5321 .map(|entry| entry.object_type)
5322 .collect::<Vec<_>>(),
5323 vec![
5324 ObjectType::Tag,
5325 ObjectType::Blob,
5326 ObjectType::Tree,
5327 ObjectType::Commit,
5328 ]
5329 );
5330 }
5331
5332 #[test]
5333 fn staged_reachable_pack_preserves_target_on_empty_and_failure() {
5334 let format = ObjectFormat::Sha1;
5335 let root = unique_temp_path(&env::temp_dir());
5336 let database = FileObjectDatabase::new(root.join("objects"), format);
5337 let pack_dir = root.join("prepared");
5338 fs::create_dir_all(&pack_dir).expect("create prepared directory");
5339 let target = pack_dir.join("target.pack");
5340 fs::write(&target, b"previous-pack").expect("write existing target");
5341
5342 let empty = build_reachable_pack_file(
5343 &database,
5344 format,
5345 std::iter::empty(),
5346 &HashSet::new(),
5347 &target,
5348 )
5349 .expect("empty walk");
5350 assert!(empty.is_none());
5351 assert_eq!(fs::read(&target).expect("read target"), b"previous-pack");
5352
5353 let missing_tree = ObjectId::from_hex(format, "ffffffffffffffffffffffffffffffffffffffff")
5354 .expect("missing tree id");
5355 let commit = database
5356 .write_object(EncodedObject::new(
5357 ObjectType::Commit,
5358 Commit {
5359 tree: missing_tree,
5360 parents: Vec::new(),
5361 author: b"A <a@example.com> 1 +0000".to_vec(),
5362 committer: b"A <a@example.com> 1 +0000".to_vec(),
5363 encoding: None,
5364 message: b"missing tree\n".to_vec(),
5365 }
5366 .write(),
5367 ))
5368 .expect("write commit");
5369 build_reachable_pack_file(&database, format, [commit], &HashSet::new(), &target)
5370 .expect_err("missing tree must abort staging");
5371 assert_eq!(fs::read(&target).expect("read target"), b"previous-pack");
5372 assert!(
5373 fs::read_dir(&pack_dir)
5374 .expect("list prepared directory")
5375 .filter_map(std::result::Result::ok)
5376 .all(|entry| !entry.file_name().to_string_lossy().starts_with("tmp_obj_")),
5377 "failed and empty builds must remove temporary packs"
5378 );
5379
5380 let blob = database
5381 .write_object(EncodedObject::new(
5382 ObjectType::Blob,
5383 b"replacement".to_vec(),
5384 ))
5385 .expect("write replacement blob");
5386 let prepared =
5387 build_reachable_pack_file(&database, format, [blob], &HashSet::new(), &target)
5388 .expect("stage replacement")
5389 .expect("non-empty replacement");
5390 let bytes = fs::read(&target).expect("read prepared pack");
5391 let parsed = PackFile::parse(&bytes, format).expect("parse prepared pack");
5392 assert_eq!(prepared.pack_size, bytes.len() as u64);
5393 assert_eq!(prepared.checksum, parsed.checksum);
5394 assert_eq!(prepared.object_count, 1);
5395 fs::remove_dir_all(root).expect("remove staging fixture");
5396 }
5397}