1use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
4use std::fmt;
5use std::sync::Arc;
6
7use crate::alternate_boundary::AlternateSources;
8use crate::bitmap_walk::{BitmapWalkError, ReachabilityQuery, ReachableSet};
9use crate::commit_graph_file::{CommitGraphChain, UNRANKED_GENERATION};
10use crate::environment::RepositoryOptions;
11use crate::error::{Error, Result};
12use crate::objects::{
13 oid_hash_map, oid_hash_set, oid_hash_set_with_capacity, parse_tag, tag_object_line_oid,
14 tree_entry_span, CommitRef, Object, ObjectId, ObjectKind, OidHashMap, OidHashSet, TreeEntryRef,
15 TreeRefIter,
16};
17use crate::odb::store::{CompositeStore, ObjectStore};
18use crate::odb::Odb;
19use crate::pack::SingleLocalPackReader;
20use crate::pack_bitmap::BitmapIndex;
21use crate::pack_name_hash::{pack_name_hash, pack_name_hash_child};
22use crate::pack_tree_read_ahead::{ReadAheadEntry, TreeReadAhead};
23use crate::progress::CancelSignal;
24use crate::repo::Repository;
25use crate::rev_list::{MissingAction, ObjectFilter};
26
27#[derive(Clone)]
29#[allow(clippy::type_complexity)]
30pub struct PackObjectSelection {
31 pub send: Vec<ObjectId>,
33 pub have_closure: HashSet<ObjectId>,
38 pub peer_held: Option<Arc<ReachableSet>>,
40 pub name_hashes: HashMap<ObjectId, u32>,
44}
45
46impl fmt::Debug for PackObjectSelection {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.debug_struct("PackObjectSelection")
49 .field("send", &self.send)
50 .field("have_closure", &self.have_closure)
51 .field(
52 "peer_held",
53 &self
54 .peer_held
55 .as_ref()
56 .map(|held| held.count())
57 .unwrap_or(0),
58 )
59 .field("name_hashes", &self.name_hashes)
60 .finish()
61 }
62}
63
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
66pub enum HaveBoundary {
67 #[default]
69 CommitClosure,
70 TipsOnly,
74}
75
76#[derive(Clone, Copy, Debug)]
78pub struct PackEnumerateOptions<'a> {
79 pub filter: Option<&'a ObjectFilter>,
81 pub shallow_grafts: &'a HashSet<ObjectId>,
85 pub have_shallow_grafts: Option<&'a HashSet<ObjectId>>,
90 pub use_bitmaps: bool,
92 pub exclude_objects: Option<&'a HashSet<ObjectId>>,
94 pub peer_odb: Option<&'a Odb>,
96 pub have_boundary: HaveBoundary,
98}
99
100impl PackEnumerateOptions<'_> {
101 fn has_shallow_grafts(&self) -> bool {
104 !self.shallow_grafts.is_empty() || self.have_shallow_grafts.is_some_and(|g| !g.is_empty())
105 }
106}
107
108pub fn enumerate_pack_objects(
114 odb: &Odb,
115 wants: &[ObjectId],
116 haves: &[ObjectId],
117 opts: &PackEnumerateOptions<'_>,
118) -> Result<PackObjectSelection> {
119 enumerate_pack_objects_naming(
120 odb,
121 wants,
122 haves,
123 opts,
124 NameTrees::No,
125 false,
126 &CancelSignal::never(),
127 )
128}
129
130#[derive(Clone, Copy, PartialEq, Eq)]
132pub(crate) enum NameTrees {
133 No,
135 Yes,
137}
138
139impl NameTrees {
140 pub(crate) fn for_delta_search(opts: &crate::pack_objects::PackBuildOptions) -> Self {
143 if opts.delta && (opts.thin || (opts.window > 0 && opts.max_depth > 0)) {
144 Self::Yes
145 } else {
146 Self::No
147 }
148 }
149}
150
151pub(crate) fn enumerate_pack_objects_naming(
155 odb: &Odb,
156 wants: &[ObjectId],
157 haves: &[ObjectId],
158 opts: &PackEnumerateOptions<'_>,
159 name_trees: NameTrees,
160 local_only: bool,
161 stop: &CancelSignal<'_>,
162) -> Result<PackObjectSelection> {
163 crate::trace::trace_span!("grit.pack.enumerate");
164 stop.poll()?;
165 if !local_only && opts.use_bitmaps {
166 if let Some(sel) = try_bitmap_enumeration(odb, wants, haves, opts)? {
167 stop.poll()?;
168 return Ok(sel);
169 }
170 }
171 let alternate_sources = if local_only {
172 AlternateSources::load(odb)?.composite()
173 } else {
174 None
175 };
176 walk_enumeration(
177 odb,
178 wants,
179 haves,
180 opts,
181 name_trees,
182 alternate_sources,
183 MissingBlobMode::Fail,
184 stop,
185 )
186 .map(|(selection, _)| selection)
187}
188
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub(crate) enum MissingBlobMode {
192 Fail,
194 Probe,
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201pub(crate) struct MissingBlob {
202 pub(crate) oid: ObjectId,
204 pub(crate) referrer: Option<ObjectId>,
206}
207
208pub(crate) fn enumerate_pack_objects_probing(
214 odb: &Odb,
215 wants: &[ObjectId],
216 haves: &[ObjectId],
217 opts: &PackEnumerateOptions<'_>,
218 name_trees: NameTrees,
219 stop: &CancelSignal<'_>,
220) -> Result<(PackObjectSelection, Vec<MissingBlob>)> {
221 stop.poll()?;
222 let opts = PackEnumerateOptions {
223 use_bitmaps: false,
224 ..*opts
225 };
226 walk_enumeration(
227 odb,
228 wants,
229 haves,
230 &opts,
231 name_trees,
232 None,
233 MissingBlobMode::Probe,
234 stop,
235 )
236}
237
238pub fn try_bitmap_reachable_for_upload(
242 odb: &Odb,
243 wants: &[ObjectId],
244 haves: &[ObjectId],
245 filter: Option<&ObjectFilter>,
246 shallow_grafts: &HashSet<ObjectId>,
247) -> Result<Option<(ReachableSet, ReachableSet)>> {
248 if !shallow_grafts.is_empty() || filter.is_some_and(selects_object_type) {
249 return Ok(None);
250 }
251 let Some(git_dir) = odb.config_git_dir() else {
252 return Ok(None);
253 };
254 let repo = match Repository::open(git_dir, &RepositoryOptions::empty()) {
255 Ok(r) => r,
256 Err(_) => return Ok(None),
257 };
258 let Some(index) = BitmapIndex::open(&repo).ok().flatten() else {
259 return Ok(None);
260 };
261 let index = Arc::new(index);
262 match index.reachability_and_haves(
263 &repo,
264 ReachabilityQuery {
265 wants,
266 haves,
267 filter,
268 },
269 MissingAction::Error,
270 MissingAction::Allow,
271 ) {
272 Ok(sets) => Ok(Some(sets)),
273 Err(BitmapWalkError::Unsupported(_)) => Ok(None),
274 Err(BitmapWalkError::Bitmap(_)) => Ok(None),
275 Err(BitmapWalkError::Repository(e)) => Err(e),
276 }
277}
278
279fn selects_object_type(filter: &ObjectFilter) -> bool {
282 match filter {
283 ObjectFilter::ObjectType(_) => true,
284 ObjectFilter::Combine(parts) => parts.iter().any(selects_object_type),
285 _ => false,
286 }
287}
288
289fn try_bitmap_enumeration(
290 odb: &Odb,
291 wants: &[ObjectId],
292 haves: &[ObjectId],
293 opts: &PackEnumerateOptions<'_>,
294) -> Result<Option<PackObjectSelection>> {
295 if opts.has_shallow_grafts() {
296 return Ok(None);
297 }
298 let Some((send_set, peer_held)) =
299 try_bitmap_reachable_for_upload(odb, wants, haves, opts.filter, opts.shallow_grafts)?
300 else {
301 return Ok(None);
302 };
303 let index = send_set.bitmap_index();
304 let send: Vec<ObjectId> = send_set.iter_grouped_by_kind().collect();
305 let have_closure: HashSet<ObjectId> = haves.iter().copied().collect();
306 let name_hashes = name_hashes_from_bitmap(index, &send, &HashSet::new());
307 Ok(Some(PackObjectSelection {
308 send,
309 have_closure,
310 peer_held: Some(Arc::new(peer_held)),
311 name_hashes,
312 }))
313}
314
315pub(crate) fn name_hashes_for_send(
316 index: &BitmapIndex,
317 send: &[ObjectId],
318 have_closure: &HashSet<ObjectId>,
319) -> HashMap<ObjectId, u32> {
320 name_hashes_from_bitmap(index, send, have_closure)
321}
322
323fn name_hashes_from_bitmap(
324 index: &BitmapIndex,
325 send: &[ObjectId],
326 have_closure: &HashSet<ObjectId>,
327) -> HashMap<ObjectId, u32> {
328 let mut out = HashMap::new();
329 for oid in send.iter().chain(have_closure.iter()) {
330 let Some(pos) = index.position_of(oid) else {
331 continue;
332 };
333 if let Some(h) = index.name_hash(pos) {
334 out.insert(*oid, h);
335 }
336 }
337 out
338}
339
340#[allow(clippy::too_many_arguments)]
341fn walk_enumeration(
342 odb: &Odb,
343 wants: &[ObjectId],
344 haves: &[ObjectId],
345 opts: &PackEnumerateOptions<'_>,
346 name_trees: NameTrees,
347 alternate_sources: Option<Arc<CompositeStore>>,
348 missing_mode: MissingBlobMode,
349 stop: &CancelSignal<'_>,
350) -> Result<(PackObjectSelection, Vec<MissingBlob>)> {
351 let have_shallow = opts.have_shallow_grafts.unwrap_or(opts.shallow_grafts);
352 let have_commits = match opts.have_boundary {
353 HaveBoundary::CommitClosure => {
354 commits_reachable_from_haves(odb, haves, have_shallow, true, stop)?
355 }
356 HaveBoundary::TipsOnly => {
357 send_pack_uninteresting_commits(odb, wants, haves, have_shallow, stop)?
358 }
359 };
360 let mut boundary: HashSet<ObjectId> = HashSet::new();
361 if let Some(extra) = opts.exclude_objects {
362 boundary.extend(extra.iter().copied());
363 }
364 let peer_held = try_peer_held_bitmap(odb, haves, opts)?;
365 stop.poll()?;
366 let mut missing = Vec::new();
367 let (send, name_hashes, edges) = match opts.filter {
368 Some(filter) if filter.uses_paths() => {
369 let (mut send, name_hashes) = rev_list_filtered_objects(
370 odb,
371 wants,
372 haves,
373 filter,
374 &have_commits,
375 missing_mode,
376 stop,
377 )?;
378 if missing_mode == MissingBlobMode::Probe {
379 missing = take_unstored_blobs(odb, &mut send)?;
380 }
381 (send, name_hashes, None)
382 }
383 filter => {
384 let walked = collect_reachable_excluding_with_name_hashes(
385 odb,
386 wants,
387 &HashSet::new(),
388 &mut boundary,
389 &have_commits,
390 opts.peer_odb,
391 alternate_sources,
392 false,
393 opts.shallow_grafts,
394 filter,
395 name_trees,
396 missing_mode,
397 stop,
398 )?;
399 missing = walked.missing;
400 (walked.send, walked.name_hashes, Some(walked.edges))
401 }
402 };
403 let have_closure = match edges {
406 Some(edges) => edges,
407 None => haves.iter().copied().collect(),
408 };
409 Ok((
410 PackObjectSelection {
411 send,
412 have_closure,
413 peer_held,
414 name_hashes,
415 },
416 missing,
417 ))
418}
419
420fn take_unstored_blobs(odb: &Odb, send: &mut Vec<ObjectId>) -> Result<Vec<MissingBlob>> {
424 let present = odb.contains_many(send)?;
425 let mut missing = Vec::new();
426 let mut present = present.into_iter();
427 send.retain(|oid| {
428 let keep = present.next().unwrap_or(true);
429 if !keep {
430 missing.push(MissingBlob {
431 oid: *oid,
432 referrer: None,
433 });
434 }
435 keep
436 });
437 Ok(missing)
438}
439
440#[derive(Clone, Copy, Debug, PartialEq, Eq)]
442pub(crate) struct TagCandidate {
443 pub(crate) tag: ObjectId,
445 pub(crate) peeled: ObjectId,
447}
448
449pub(crate) fn append_included_tags(
467 odb: &Odb,
468 send: &mut Vec<ObjectId>,
469 candidates: &[TagCandidate],
470) -> Result<()> {
471 if candidates.is_empty() {
472 return Ok(());
473 }
474 let mut named = oid_hash_set_with_capacity(candidates.len() * 2);
475 for c in candidates {
476 named.insert(c.tag);
477 named.insert(c.peeled);
478 }
479 let present: OidHashSet = send
480 .iter()
481 .filter(|o| named.contains(*o))
482 .copied()
483 .collect();
484 let mut added = included_tag_chains(odb, candidates, |oid| present.contains(oid))?;
485 let unnamed: OidHashSet = added
486 .iter()
487 .filter(|o| !named.contains(*o))
488 .copied()
489 .collect();
490 if !unnamed.is_empty() {
491 let already_sent: OidHashSet = send
492 .iter()
493 .filter(|o| unnamed.contains(*o))
494 .copied()
495 .collect();
496 added.retain(|oid| !already_sent.contains(oid));
497 }
498 send.extend(added);
499 Ok(())
500}
501
502pub(crate) fn included_tag_chains(
515 odb: &Odb,
516 candidates: &[TagCandidate],
517 in_pack: impl Fn(&ObjectId) -> bool,
518) -> Result<Vec<ObjectId>> {
519 let mut seen = oid_hash_set();
520 let mut out = Vec::new();
521 for c in candidates {
522 if !in_pack(&c.peeled) || in_pack(&c.tag) {
523 continue;
524 }
525 let mut current = c.tag;
526 while current != c.peeled && seen.insert(current) {
527 if !in_pack(¤t) {
528 out.push(current);
529 }
530 let tag = odb.read(¤t)?;
531 current = tag_object_line_oid(&tag.data).ok_or_else(|| {
532 Error::CorruptObject(format!("tag {current} has no valid object header"))
533 })?;
534 }
535 }
536 Ok(out)
537}
538
539const SEND_PACK_REACH_WANT: u8 = 1;
540const SEND_PACK_REACH_HAVE: u8 = 2;
541
542#[cfg(test)]
543pub(crate) mod send_pack_walk_test_hooks {
544 use std::cell::Cell;
545
546 thread_local! {
547 static COMMIT_READS: Cell<usize> = const { Cell::new(0) };
548 }
549
550 pub fn reset_commit_reads() {
551 COMMIT_READS.with(|c| c.set(0));
552 }
553
554 pub fn commit_reads() -> usize {
555 COMMIT_READS.with(|c| c.get())
556 }
557
558 pub(crate) fn record_commit_read() {
559 COMMIT_READS.with(|c| c.set(c.get().saturating_add(1)));
560 }
561}
562
563fn send_pack_reach_uninteresting(reach: u8) -> bool {
564 reach & SEND_PACK_REACH_HAVE != 0
565}
566
567fn send_pack_reach_interesting(reach: u8) -> bool {
568 reach & SEND_PACK_REACH_WANT != 0 && !send_pack_reach_uninteresting(reach)
569}
570
571fn send_pack_uninteresting_commits(
577 odb: &Odb,
578 wants: &[ObjectId],
579 haves: &[ObjectId],
580 shallow_grafts: &HashSet<ObjectId>,
581 stop: &CancelSignal<'_>,
582) -> Result<HashSet<ObjectId>> {
583 if haves.is_empty() {
586 return Ok(HashSet::new());
587 }
588 struct SendPackRevWalk<'a> {
589 odb: &'a Odb,
590 graph: Option<Arc<CommitGraphChain>>,
591 shallow_grafts: &'a HashSet<ObjectId>,
592 reach: HashMap<ObjectId, u8>,
593 heap: BinaryHeap<QueuedCommit>,
594 on_heap: HashSet<ObjectId>,
595 processed: HashSet<ObjectId>,
596 uninteresting: HashSet<ObjectId>,
597 interesting_on_heap: usize,
598 next_seq: u64,
599 header_buf: Vec<u8>,
600 }
601
602 impl<'a> SendPackRevWalk<'a> {
603 fn note_reach(&mut self, oid: ObjectId, flag: u8) {
604 let entry = self.reach.entry(oid).or_insert(0);
605 let was_interesting = send_pack_reach_interesting(*entry);
606 *entry |= flag;
607 let is_interesting = send_pack_reach_interesting(*entry);
608 if self.on_heap.contains(&oid) {
609 match (was_interesting, is_interesting) {
610 (false, true) => self.interesting_on_heap += 1,
611 (true, false) => self.interesting_on_heap -= 1,
612 _ => {}
613 }
614 }
615 if send_pack_reach_uninteresting(*entry) {
616 self.uninteresting.insert(oid);
617 }
618 }
619
620 fn can_stop_after(&self) -> bool {
625 !self.heap.is_empty() && self.interesting_on_heap == 0
628 }
629
630 fn reach_unqueued(&mut self, oid: ObjectId, flag: u8) -> bool {
632 self.note_reach(oid, flag);
633 !(self.processed.contains(&oid)
634 || self.on_heap.contains(&oid)
635 || self.shallow_grafts.contains(&oid))
636 }
637
638 fn try_enqueue(&mut self, oid: ObjectId, flag: u8) -> Result<()> {
639 if !self.reach_unqueued(oid, flag) {
640 return Ok(());
641 }
642 if self.queue_from_graph(oid) {
643 return Ok(());
644 }
645 let mut header = std::mem::take(&mut self.header_buf);
649 let queued = if matches!(
650 self.odb
651 .read_packed_commit_header_block_into(&oid, &mut header),
652 Ok(true)
653 ) {
654 self.queue_commit_data(oid, &header)
655 } else {
656 match self.odb.read(&oid) {
657 Ok(obj) => self.queue_commit(oid, &obj),
658 Err(_) => Ok(()),
659 }
660 };
661 self.header_buf = header;
662 queued
663 }
664
665 fn enqueue_tip(&mut self, tip: ObjectId, flag: u8) -> Result<()> {
670 let mut oid = tip;
671 let mut tags_seen: Option<HashSet<ObjectId>> = None;
672 loop {
673 if self.graph_lists(&oid) {
674 if self.reach_unqueued(oid, flag) {
675 self.queue_from_graph(oid);
676 }
677 return Ok(());
678 }
679 match self.odb.read_info(&oid) {
682 Ok(info) if matches!(info.kind, ObjectKind::Commit | ObjectKind::Tag) => {}
683 _ => return Ok(()),
684 }
685 let Ok(obj) = self.odb.read(&oid) else {
686 return Ok(());
687 };
688 if obj.kind != ObjectKind::Tag {
689 if obj.kind != ObjectKind::Commit || !self.reach_unqueued(oid, flag) {
690 return Ok(());
691 }
692 return self.queue_commit(oid, &obj);
693 }
694 if !tags_seen.get_or_insert_with(HashSet::new).insert(oid) {
695 return Ok(());
696 }
697 let Some(target) = tag_object_line_oid(&obj.data) else {
698 return Ok(());
699 };
700 if self.processed.contains(&target) || self.on_heap.contains(&target) {
701 self.note_reach(target, flag);
702 return Ok(());
703 }
704 oid = target;
705 }
706 }
707
708 fn graph_lists(&self, oid: &ObjectId) -> bool {
709 self.graph
710 .as_deref()
711 .is_some_and(|g| g.find_commit(oid).is_some())
712 }
713
714 fn queue_from_graph(&mut self, oid: ObjectId) -> bool {
717 let Some(entry) = self.graph.as_deref().and_then(|g| g.graph_walk_entry(&oid)) else {
718 return false;
719 };
720 self.push_queued(QueuedCommit {
721 generation: entry.generation,
722 time: entry.time,
723 seq: std::cmp::Reverse(self.next_seq),
724 oid,
725 tree: entry.tree,
726 parents: entry.parents,
727 });
728 true
729 }
730
731 fn queue_commit(&mut self, oid: ObjectId, obj: &Object) -> Result<()> {
732 if obj.kind != ObjectKind::Commit {
733 return Ok(());
734 }
735 self.queue_commit_data(oid, &obj.data)
736 }
737
738 fn queue_commit_data(&mut self, oid: ObjectId, data: &[u8]) -> Result<()> {
740 let parsed = CommitRef::parse(data)?;
741 #[cfg(test)]
742 send_pack_walk_test_hooks::record_commit_read();
743 self.push_queued(QueuedCommit {
744 generation: UNRANKED_GENERATION,
745 time: parsed.committer_unix_seconds_for_ordering(),
746 seq: std::cmp::Reverse(self.next_seq),
747 oid,
748 tree: parsed.tree(),
749 parents: parsed.parents().to_vec(),
750 });
751 Ok(())
752 }
753
754 fn push_queued(&mut self, queued: QueuedCommit) {
755 self.on_heap.insert(queued.oid);
756 if send_pack_reach_interesting(*self.reach.get(&queued.oid).unwrap_or(&0)) {
757 self.interesting_on_heap += 1;
758 }
759 self.heap.push(queued);
760 self.next_seq += 1;
761 }
762 }
763
764 let graph = if shallow_grafts.is_empty() {
770 odb.commit_graph_chain()
771 } else {
772 None
773 };
774 let mut walk = SendPackRevWalk {
775 odb,
776 graph,
777 shallow_grafts,
778 reach: HashMap::new(),
779 heap: BinaryHeap::new(),
780 on_heap: HashSet::new(),
781 processed: HashSet::new(),
782 uninteresting: HashSet::new(),
783 interesting_on_heap: 0,
784 next_seq: 0,
785 header_buf: Vec::new(),
786 };
787
788 for &want in wants {
789 walk.enqueue_tip(want, SEND_PACK_REACH_WANT)?;
790 }
791 for &have in haves {
792 walk.enqueue_tip(have, SEND_PACK_REACH_HAVE)?;
793 }
794
795 while let Some(commit) = walk.heap.pop() {
796 stop.poll()?;
797 walk.on_heap.remove(&commit.oid);
798 if send_pack_reach_interesting(*walk.reach.get(&commit.oid).unwrap_or(&0)) {
799 walk.interesting_on_heap -= 1;
800 }
801 if !walk.processed.insert(commit.oid) {
802 continue;
803 }
804 let flags = *walk.reach.get(&commit.oid).unwrap_or(&0);
805 if send_pack_reach_uninteresting(flags) {
806 walk.uninteresting.insert(commit.oid);
807 }
808 let parent_flag = if send_pack_reach_uninteresting(flags) {
809 SEND_PACK_REACH_HAVE
810 } else {
811 SEND_PACK_REACH_WANT
812 };
813 if !walk.shallow_grafts.contains(&commit.oid) {
814 for parent in commit.parents {
815 walk.try_enqueue(parent, parent_flag)?;
816 }
817 }
818 if walk.can_stop_after() {
819 break;
820 }
821 }
822 Ok(walk.uninteresting)
823}
824
825fn commits_reachable_from_haves(
827 odb: &Odb,
828 haves: &[ObjectId],
829 shallow_grafts: &HashSet<ObjectId>,
830 skip_missing: bool,
831 stop: &CancelSignal<'_>,
832) -> Result<HashSet<ObjectId>> {
833 let mut seen = HashSet::new();
834 let mut queue: VecDeque<ObjectId> = haves.iter().copied().collect();
835 while let Some(oid) = queue.pop_front() {
836 stop.poll()?;
837 if !seen.insert(oid) {
838 continue;
839 }
840 if shallow_grafts.contains(&oid) {
841 continue;
842 }
843 let obj = match odb.read(&oid) {
844 Ok(o) => o,
845 Err(_) if skip_missing => continue,
846 Err(e) => return Err(e),
847 };
848 if obj.kind != ObjectKind::Commit {
849 continue;
850 }
851 let parsed = CommitRef::parse(&obj.data)?;
852 for parent in parsed.parents() {
853 if !seen.contains(parent) {
854 queue.push_back(*parent);
855 }
856 }
857 }
858 Ok(seen)
859}
860
861fn objects_in_commit_snapshot(odb: &Odb, commit_oid: ObjectId) -> Result<HashSet<ObjectId>> {
863 let mut out = HashSet::new();
864 out.insert(commit_oid);
865 let commit_obj = match odb.read(&commit_oid) {
866 Ok(o) => o,
867 Err(_) => return Ok(out),
868 };
869 if commit_obj.kind != ObjectKind::Commit {
870 return Ok(out);
871 }
872 let commit = CommitRef::parse(&commit_obj.data)?;
873 let oid_len = odb.hash_algo().len();
874 let mut queue = VecDeque::from([commit.tree()]);
875 while let Some(tree_oid) = queue.pop_front() {
876 if !out.insert(tree_oid) {
877 continue;
878 }
879 let tree_obj = match odb.read(&tree_oid) {
880 Ok(o) => o,
881 Err(_) => continue,
882 };
883 for entry in TreeRefIter::with_oid_len(&tree_obj.data, oid_len) {
884 let entry = entry?;
885 if entry.mode == 0o160000 {
886 continue;
887 }
888 if entry.mode & 0o170000 == 0o040000 {
889 queue.push_back(entry.oid);
890 } else {
891 out.insert(entry.oid);
892 }
893 }
894 }
895 Ok(out)
896}
897
898fn try_peer_held_bitmap(
899 odb: &Odb,
900 haves: &[ObjectId],
901 opts: &PackEnumerateOptions<'_>,
902) -> Result<Option<Arc<ReachableSet>>> {
903 if haves.is_empty() || !opts.use_bitmaps || opts.has_shallow_grafts() {
904 return Ok(None);
905 }
906 let Some(peer) = try_bitmap_peer_held(odb, haves, opts.shallow_grafts)? else {
907 return Ok(None);
908 };
909 Ok(Some(Arc::new(peer)))
910}
911
912fn try_bitmap_peer_held(
913 odb: &Odb,
914 haves: &[ObjectId],
915 shallow_grafts: &HashSet<ObjectId>,
916) -> Result<Option<ReachableSet>> {
917 if !shallow_grafts.is_empty() {
918 return Ok(None);
919 }
920 let Some(git_dir) = odb.config_git_dir() else {
921 return Ok(None);
922 };
923 let repo = match Repository::open(git_dir, &RepositoryOptions::empty()) {
924 Ok(r) => r,
925 Err(_) => return Ok(None),
926 };
927 let Some(index) = BitmapIndex::open(&repo).ok().flatten() else {
928 return Ok(None);
929 };
930 let index = Arc::new(index);
931 match index.reachability(
932 &repo,
933 ReachabilityQuery {
934 wants: haves,
935 haves: &[],
936 filter: None,
937 },
938 MissingAction::Allow,
939 ) {
940 Ok(set) => Ok(Some(set)),
941 Err(BitmapWalkError::Unsupported(_)) => Ok(None),
942 Err(BitmapWalkError::Bitmap(_)) => Ok(None),
943 Err(BitmapWalkError::Repository(e)) => Err(e),
944 }
945}
946
947fn rev_list_filtered_objects(
949 odb: &Odb,
950 wants: &[ObjectId],
951 haves: &[ObjectId],
952 filter: &ObjectFilter,
953 have_commits: &HashSet<ObjectId>,
954 missing_mode: MissingBlobMode,
955 stop: &CancelSignal<'_>,
956) -> Result<(Vec<ObjectId>, HashMap<ObjectId, u32>)> {
957 let _ = have_commits;
958 stop.poll()?;
959 let Some(git_dir) = odb.config_git_dir() else {
960 return Ok((Vec::new(), HashMap::new()));
961 };
962 let repo = Repository::open(git_dir, &RepositoryOptions::empty())?;
963 let wants: Vec<String> = wants.iter().map(ObjectId::to_hex).collect();
964 let haves: Vec<String> = haves.iter().map(ObjectId::to_hex).collect();
965 let opts = crate::rev_list::RevListOptions {
966 objects: true,
967 no_object_names: false,
968 quiet: true,
969 filter: Some(filter.clone()),
970 cancel_watch: Some(stop.watch_arc()),
971 missing_action: match missing_mode {
972 MissingBlobMode::Fail => MissingAction::Error,
973 MissingBlobMode::Probe => MissingAction::Allow,
974 },
975 ..Default::default()
976 };
977 let result = crate::rev_list::rev_list(&repo, &wants, &haves, &opts)?;
978 stop.poll()?;
979 let mut ordered = Vec::new();
980 let mut seen = HashSet::new();
981 for oid in result
982 .commits
983 .iter()
984 .chain(result.objects.iter().map(|(o, _)| o))
985 {
986 if seen.insert(*oid) {
987 ordered.push(*oid);
988 }
989 }
990 let mut name_hashes = HashMap::new();
991 for (oid, path) in &result.objects {
992 if !path.is_empty() {
993 name_hashes
994 .entry(*oid)
995 .or_insert_with(|| pack_name_hash(path));
996 }
997 }
998 Ok((ordered, name_hashes))
999}
1000
1001#[allow(clippy::too_many_arguments)]
1022fn collect_reachable_excluding_with_name_hashes(
1023 odb: &Odb,
1024 roots: &[ObjectId],
1025 exclude: &HashSet<ObjectId>,
1026 boundary: &mut HashSet<ObjectId>,
1027 have_commits: &HashSet<ObjectId>,
1028 peer_odb: Option<&Odb>,
1029 alternate_sources: Option<Arc<CompositeStore>>,
1030 skip_missing: bool,
1031 shallow_grafts: &HashSet<ObjectId>,
1032 filter: Option<&ObjectFilter>,
1033 name_trees: NameTrees,
1034 missing_mode: MissingBlobMode,
1035 stop: &CancelSignal<'_>,
1036) -> Result<WalkedObjects> {
1037 let oid_len = odb.hash_algo().len();
1038 let mut walk = EnumerationWalk {
1039 odb,
1040 exclude,
1041 boundary,
1042 have_commits,
1043 peer_odb,
1044 alternate_sources,
1045 skip_missing,
1046 filter,
1047 depth_sensitive: filter.is_some_and(ObjectFilter::is_depth_sensitive),
1048 name_trees,
1049 visited: oid_hash_set(),
1050 tree_depths: oid_hash_map(),
1051 unlisted: oid_hash_set(),
1052 ordered: Vec::new(),
1053 commits: BinaryHeap::new(),
1054 next_seq: 0,
1055 queue: VecDeque::new(),
1056 name_hashes: HashMap::new(),
1057 edges: HashSet::new(),
1058 missing: (missing_mode == MissingBlobMode::Probe).then(Vec::new),
1059 session_pack: odb.open_session_pack_reader().ok().flatten(),
1060 expanded: filter.is_none().then(HashMap::new),
1061 };
1062 let mut dropped_root_trees: Option<OidHashSet> = filter
1064 .filter(|f| !f.includes_tree(0))
1065 .map(|_| oid_hash_set());
1066
1067 let mut path_less_roots = Vec::new();
1068 for &root in roots {
1069 match odb.read_info(&root) {
1070 Ok(info) if matches!(info.kind, ObjectKind::Tree | ObjectKind::Blob) => {
1071 path_less_roots.push(root);
1072 }
1073 _ => walk.push_root(root)?,
1074 }
1075 }
1076 let mut path_less_roots = path_less_roots.into_iter();
1077 let read_ahead_workers = crate::pack_delta::pack_delta_search_parallel_options(odb, None)
1079 .map_or(0, |par| par.threads.get() - 1);
1080
1081 std::thread::scope(|scope| -> Result<()> {
1082 let mut read_ahead = TreeReadAhead::new(scope, odb, read_ahead_workers);
1083 loop {
1084 stop.poll()?;
1085 if let Some(commit) = walk.commits.pop() {
1086 if !shallow_grafts.contains(&commit.oid) {
1087 for parent in commit.parents {
1088 walk.push_parent(parent)?;
1089 }
1090 }
1091 if let Some(dropped) = dropped_root_trees.as_mut() {
1092 dropped.insert(commit.tree);
1093 }
1094 walk.push_tree(commit.tree, None, 0);
1095 read_ahead.top_up(&walk.queue);
1096 continue;
1097 }
1098 let Some(QueuedObject {
1099 oid,
1100 path_hash,
1101 depth,
1102 found_as_tree,
1103 }) = walk.queue.pop_front()
1104 else {
1105 match path_less_roots.next() {
1106 Some(root) => {
1107 walk.push_object(root, None, false);
1108 continue;
1109 }
1110 None => break,
1111 }
1112 };
1113 let read_ahead_result = read_ahead.take_front();
1114 read_ahead.top_up(&walk.queue);
1115 if !found_as_tree {
1119 match odb.read_info(&oid) {
1120 Ok(info) if info.kind == ObjectKind::Blob => continue,
1121 Ok(_) => {}
1122 Err(_) if skip_missing => continue,
1123 Err(e) => return Err(e),
1124 }
1125 }
1126 let read = match read_ahead_result {
1127 Some(obj) => Ok(obj),
1128 None => walk.read_object(&oid),
1129 };
1130 let obj = match read {
1131 Ok(o) => o,
1132 Err(_) if skip_missing => continue,
1133 Err(e) => return Err(e),
1134 };
1135 walk.expand(oid, obj, path_hash, depth, oid_len)?;
1136 }
1137 Ok(())
1138 })?;
1139 odb.clear_delta_base_cache();
1142
1143 if let Some(dropped) = dropped_root_trees.filter(|d| !d.is_empty()) {
1144 walk.ordered.retain(|oid| !dropped.contains(oid));
1145 }
1146 let mut send = walk.ordered;
1147 if let Some(peer) = peer_odb {
1148 if !peer.uses_files_primary() && !peer.custom_primary_is_empty()? {
1149 send = peer.missing_materialized_many(&send)?;
1150 }
1151 }
1152 Ok(WalkedObjects {
1153 send,
1154 name_hashes: walk.name_hashes,
1155 edges: walk.edges,
1156 missing: walk.missing.unwrap_or_default(),
1157 })
1158}
1159
1160struct WalkedObjects {
1162 send: Vec<ObjectId>,
1164 name_hashes: HashMap<ObjectId, u32>,
1166 edges: HashSet<ObjectId>,
1168 missing: Vec<MissingBlob>,
1170}
1171
1172struct QueuedObject {
1174 oid: ObjectId,
1175 path_hash: Option<u32>,
1178 depth: u64,
1180 found_as_tree: bool,
1183}
1184
1185impl ReadAheadEntry for QueuedObject {
1186 fn read_ahead_oid(&self) -> Option<ObjectId> {
1187 self.found_as_tree.then_some(self.oid)
1188 }
1189}
1190
1191struct EnumerationWalk<'a> {
1193 odb: &'a Odb,
1194 exclude: &'a HashSet<ObjectId>,
1195 boundary: &'a mut HashSet<ObjectId>,
1197 have_commits: &'a HashSet<ObjectId>,
1199 peer_odb: Option<&'a Odb>,
1201 alternate_sources: Option<Arc<CompositeStore>>,
1203 skip_missing: bool,
1204 filter: Option<&'a ObjectFilter>,
1205 depth_sensitive: bool,
1207 name_trees: NameTrees,
1208 visited: OidHashSet,
1212 tree_depths: OidHashMap<u64>,
1214 unlisted: OidHashSet,
1217 ordered: Vec<ObjectId>,
1218 commits: BinaryHeap<QueuedCommit>,
1220 next_seq: u64,
1221 queue: VecDeque<QueuedObject>,
1223 name_hashes: HashMap<ObjectId, u32>,
1224 edges: HashSet<ObjectId>,
1226 missing: Option<Vec<MissingBlob>>,
1228 session_pack: Option<SingleLocalPackReader>,
1231 expanded: Option<HashMap<Option<u32>, ExpandedTree>>,
1234}
1235
1236struct ExpandedTree {
1248 data: Vec<u8>,
1249 entry_starts: Vec<usize>,
1251}
1252
1253impl ExpandedTree {
1254 fn starts_from(&self, pos: usize) -> Option<&[usize]> {
1256 let at = self.entry_starts.binary_search(&pos).ok()?;
1257 Some(&self.entry_starts[at..])
1258 }
1259
1260 fn entry(&self, index: usize) -> Option<&[u8]> {
1262 let start = *self.entry_starts.get(index)?;
1263 let end = *self.entry_starts.get(index + 1)?;
1264 self.data.get(start..end)
1265 }
1266
1267 fn repeats_entry(&self, cursor: &mut usize, bytes: &[u8], name: &[u8], oid_len: usize) -> bool {
1276 while let Some(old) = self.entry(*cursor) {
1277 if old == bytes {
1278 *cursor += 1;
1279 return true;
1280 }
1281 let old_name = memchr::memchr(b' ', old)
1282 .and_then(|space| old.get(space + 1..old.len().checked_sub(oid_len + 1)?));
1283 match old_name.map(|old_name| old_name.cmp(name)) {
1284 Some(std::cmp::Ordering::Less) => *cursor += 1,
1285 Some(std::cmp::Ordering::Equal) => {
1286 *cursor += 1;
1287 return false;
1288 }
1289 _ => return false,
1290 }
1291 }
1292 false
1293 }
1294}
1295
1296fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
1298 let (a_chunks, _) = a.as_chunks::<16>();
1299 let (b_chunks, _) = b.as_chunks::<16>();
1300 let whole = a_chunks
1301 .iter()
1302 .zip(b_chunks)
1303 .take_while(|(x, y)| x == y)
1304 .count()
1305 * 16;
1306 whole
1307 + a[whole..]
1308 .iter()
1309 .zip(&b[whole..])
1310 .take_while(|(x, y)| x == y)
1311 .count()
1312}
1313
1314fn common_suffix_len(a: &[u8], b: &[u8]) -> usize {
1316 let (_, a_chunks) = a.as_rchunks::<16>();
1317 let (_, b_chunks) = b.as_rchunks::<16>();
1318 let whole = a_chunks
1319 .iter()
1320 .rev()
1321 .zip(b_chunks.iter().rev())
1322 .take_while(|(x, y)| x == y)
1323 .count()
1324 * 16;
1325 whole
1326 + a[..a.len() - whole]
1327 .iter()
1328 .rev()
1329 .zip(b[..b.len() - whole].iter().rev())
1330 .take_while(|(x, y)| x == y)
1331 .count()
1332}
1333
1334struct QueuedCommit {
1337 generation: u64,
1339 time: i64,
1340 seq: std::cmp::Reverse<u64>,
1341 oid: ObjectId,
1342 tree: ObjectId,
1343 parents: Vec<ObjectId>,
1344}
1345
1346impl PartialEq for QueuedCommit {
1347 fn eq(&self, other: &Self) -> bool {
1348 (self.generation, self.time, self.seq) == (other.generation, other.time, other.seq)
1349 }
1350}
1351
1352impl Eq for QueuedCommit {}
1353
1354impl PartialOrd for QueuedCommit {
1355 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1356 Some(self.cmp(other))
1357 }
1358}
1359
1360impl Ord for QueuedCommit {
1361 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1362 (self.generation, self.time, self.seq).cmp(&(other.generation, other.time, other.seq))
1363 }
1364}
1365
1366impl<'a> EnumerationWalk<'a> {
1367 fn list(&mut self, oid: ObjectId) {
1368 if !self.exclude.contains(&oid) {
1369 self.ordered.push(oid);
1370 }
1371 }
1372
1373 fn note_boundary(&mut self, oid: ObjectId) {
1374 self.visited.insert(oid);
1375 }
1376
1377 fn at_object_boundary(&self, oid: ObjectId) -> bool {
1378 if self
1379 .alternate_sources
1380 .as_ref()
1381 .is_some_and(|s| s.contains(&oid).unwrap_or(false))
1382 {
1383 return true;
1384 }
1385 if self.boundary.contains(&oid) {
1386 return true;
1387 }
1388 self.peer_odb.is_some_and(|peer| {
1389 peer.uses_files_primary() && peer.exists_materialized_local_without_rescan(&oid)
1390 })
1391 }
1392
1393 fn seal_have_reachable_commit(&mut self, oid: ObjectId) -> Result<()> {
1394 if !self.visited.insert(oid) {
1395 return Ok(());
1396 }
1397 self.edges.insert(oid);
1398 self.boundary
1399 .extend(objects_in_commit_snapshot(self.odb, oid)?);
1400 Ok(())
1401 }
1402
1403 fn commit_on_have_side(&mut self, oid: ObjectId) -> Result<bool> {
1404 Ok(self.have_commits.contains(&oid))
1405 }
1406
1407 fn push_object(&mut self, oid: ObjectId, path_hash: Option<u32>, found_as_tree: bool) {
1409 if let Ok(true) = self.commit_on_have_side(oid) {
1410 let _ = self.seal_have_reachable_commit(oid);
1411 return;
1412 }
1413 if self.at_object_boundary(oid) {
1414 self.note_boundary(oid);
1415 return;
1416 }
1417 if self.visited.insert(oid) {
1418 self.list(oid);
1419 self.queue.push_back(QueuedObject {
1420 oid,
1421 path_hash,
1422 depth: 0,
1423 found_as_tree,
1424 });
1425 } else if self.unlisted.remove(&oid) {
1426 self.list(oid);
1427 }
1428 }
1429
1430 fn push_root(&mut self, oid: ObjectId) -> Result<()> {
1433 if self.commit_on_have_side(oid)? {
1434 return self.seal_have_reachable_commit(oid);
1435 }
1436 if self.at_object_boundary(oid) {
1437 self.note_boundary(oid);
1438 return Ok(());
1439 }
1440 if !self.visited.insert(oid) {
1441 if self.unlisted.remove(&oid) {
1442 self.list(oid);
1443 }
1444 return Ok(());
1445 }
1446 self.list(oid);
1447 match self.odb.read_info(&oid) {
1450 Ok(info) if info.kind != ObjectKind::Commit => {
1451 self.queue_unnamed(oid, info.kind == ObjectKind::Tree);
1452 return Ok(());
1453 }
1454 Ok(_) => {}
1455 Err(_) if self.skip_missing => return Ok(()),
1456 Err(e) => return Err(e),
1457 }
1458 match self.read(&oid)? {
1459 Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1460 Some(obj) => {
1461 self.queue_unnamed(oid, obj.kind == ObjectKind::Tree);
1462 Ok(())
1463 }
1464 None => Ok(()),
1465 }
1466 }
1467
1468 fn push_parent(&mut self, oid: ObjectId) -> Result<()> {
1471 if self.commit_on_have_side(oid)? {
1472 return self.seal_have_reachable_commit(oid);
1473 }
1474 if self.at_object_boundary(oid) {
1475 self.note_boundary(oid);
1476 return Ok(());
1477 }
1478 let listed = self
1479 .filter
1480 .is_none_or(|f| f.includes_commit_or_tag_object(ObjectKind::Commit));
1481 if !self.visited.insert(oid) {
1482 if listed && self.unlisted.remove(&oid) {
1483 self.list(oid);
1484 }
1485 return Ok(());
1486 }
1487 if listed {
1488 self.list(oid);
1489 } else {
1490 self.unlisted.insert(oid);
1491 }
1492 match self.read(&oid)? {
1493 Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1494 Some(obj) => {
1495 self.queue_unnamed(oid, obj.kind == ObjectKind::Tree);
1496 Ok(())
1497 }
1498 None => Ok(()),
1499 }
1500 }
1501
1502 fn queue_unnamed(&mut self, oid: ObjectId, found_as_tree: bool) {
1504 self.queue.push_back(QueuedObject {
1505 oid,
1506 path_hash: None,
1507 depth: 0,
1508 found_as_tree,
1509 });
1510 }
1511
1512 fn expand(
1515 &mut self,
1516 oid: ObjectId,
1517 obj: Object,
1518 path_hash: Option<u32>,
1519 depth: u64,
1520 oid_len: usize,
1521 ) -> Result<()> {
1522 match obj.kind {
1523 ObjectKind::Commit => {
1524 self.queue_commit(oid, obj)?;
1525 }
1526 ObjectKind::Tree => self.expand_tree(oid, obj.data, path_hash, depth, oid_len)?,
1527 ObjectKind::Tag => {
1528 let tag = parse_tag(&obj.data)?;
1529 self.push_object(tag.object, path_hash, false);
1530 }
1531 ObjectKind::Blob => {}
1532 }
1533 Ok(())
1534 }
1535
1536 fn expand_tree(
1538 &mut self,
1539 oid: ObjectId,
1540 data: Vec<u8>,
1541 path_hash: Option<u32>,
1542 depth: u64,
1543 oid_len: usize,
1544 ) -> Result<()> {
1545 let prev = self
1546 .expanded
1547 .as_mut()
1548 .and_then(|by_path| by_path.remove(&path_hash));
1549 let mut entry_starts = Vec::new();
1550 let mut pos = 0;
1551 let mut shared_tail = data.len();
1554 let mut prev_entry = 0;
1556 if let Some(prev) = &prev {
1557 let prefix = common_prefix_len(&data, &prev.data);
1558 let shared = prev.entry_starts.partition_point(|&start| start <= prefix);
1559 if let Some(&last) = shared.checked_sub(1).and_then(|i| prev.entry_starts.get(i)) {
1560 pos = last;
1561 prev_entry = shared - 1;
1562 entry_starts.extend_from_slice(&prev.entry_starts[..shared - 1]);
1563 }
1564 shared_tail = data.len() - common_suffix_len(&data[pos..], &prev.data[pos..]);
1565 }
1566 loop {
1567 let at = pos;
1568 if let Some(prev) = prev.as_ref().filter(|_| at >= shared_tail) {
1569 let prev_at = at + prev.data.len() - data.len();
1572 if let Some(rest) = prev.starts_from(prev_at) {
1573 entry_starts.extend(rest.iter().map(|&s| s + data.len() - prev.data.len()));
1574 break;
1575 }
1576 }
1577 entry_starts.push(at);
1578 if at >= data.len() {
1579 break;
1580 }
1581 let entry = match tree_entry_span(&data, at, oid_len) {
1582 Some(span) => {
1583 pos = span.end;
1584 if let Some(prev) = &prev {
1585 let name = &data[span.name.clone()];
1586 if prev.repeats_entry(&mut prev_entry, &data[at..pos], name, oid_len) {
1587 continue;
1588 }
1589 }
1590 TreeEntryRef {
1591 mode: span.mode,
1592 name: &data[span.name],
1593 oid: ObjectId::from_bytes(&data[pos - oid_len..pos])?,
1594 }
1595 }
1596 None => {
1597 let mut entries = TreeRefIter::with_oid_len_at(&data, oid_len, at);
1598 let Some(entry) = entries.next().transpose()? else {
1599 break;
1600 };
1601 pos = entries.position();
1602 entry
1603 }
1604 };
1605 self.push_tree_entry(entry, oid, path_hash, depth)?;
1606 }
1607 if let Some(by_path) = self.expanded.as_mut() {
1608 by_path.insert(path_hash, ExpandedTree { data, entry_starts });
1609 }
1610 Ok(())
1611 }
1612
1613 fn push_tree_entry(
1614 &mut self,
1615 entry: TreeEntryRef<'_>,
1616 tree: ObjectId,
1617 path_hash: Option<u32>,
1618 depth: u64,
1619 ) -> Result<()> {
1620 if entry.mode == 0o160000 {
1621 return Ok(());
1622 }
1623 if self.filter.is_none() && self.visited.contains(&entry.oid) {
1626 return Ok(());
1627 }
1628 match entry.mode & 0o170000 {
1634 0o120000 => self.push_blob(entry.oid, None, tree, depth)?,
1635 0o100000 => {
1636 let name = pack_name_hash_child(path_hash, entry.name);
1637 self.push_blob(entry.oid, Some(name), tree, depth)?;
1638 }
1639 file_type => {
1640 let name = Some(pack_name_hash_child(path_hash, entry.name));
1641 if file_type == 0o040000 {
1642 self.push_tree(entry.oid, name, depth + 1);
1643 } else {
1644 self.push_object(entry.oid, name, false);
1645 }
1646 }
1647 }
1648 Ok(())
1649 }
1650
1651 fn read_object(&mut self, oid: &ObjectId) -> Result<Object> {
1654 if let Some(Ok(obj)) = self.session_pack.as_mut().map(|r| r.read_object(oid)) {
1655 return Ok(obj);
1656 }
1657 self.odb.read_once(oid)
1658 }
1659
1660 fn read(&mut self, oid: &ObjectId) -> Result<Option<Object>> {
1661 match self.read_object(oid) {
1662 Ok(obj) => Ok(Some(obj)),
1663 Err(_) if self.skip_missing => Ok(None),
1664 Err(e) => Err(e),
1665 }
1666 }
1667
1668 fn queue_commit(&mut self, oid: ObjectId, obj: Object) -> Result<()> {
1670 let parsed = CommitRef::parse(&obj.data)?;
1671 let queued = QueuedCommit {
1672 generation: UNRANKED_GENERATION,
1673 time: parsed.committer_unix_seconds_for_ordering(),
1674 seq: std::cmp::Reverse(self.next_seq),
1675 oid,
1676 tree: parsed.tree(),
1677 parents: parsed.parents().to_vec(),
1678 };
1679 self.next_seq += 1;
1680 self.commits.push(queued);
1681 Ok(())
1682 }
1683
1684 fn push_tree(&mut self, oid: ObjectId, path_hash: Option<u32>, depth: u64) {
1687 if let Some(hash) = path_hash.filter(|_| self.name_trees == NameTrees::Yes) {
1688 if !self.visited.contains(&oid) {
1689 self.name_hashes.entry(oid).or_insert(hash);
1690 }
1691 }
1692 let Some(filter) = self.filter else {
1693 self.push_object(oid, path_hash, true);
1694 return;
1695 };
1696 if self.at_object_boundary(oid) {
1698 self.note_boundary(oid);
1699 return;
1700 }
1701 let first_walk = if self.depth_sensitive {
1702 match self.tree_depths.get(&oid) {
1703 Some(&walked_at) if walked_at <= depth => false,
1704 _ => {
1705 self.tree_depths.insert(oid, depth);
1706 true
1707 }
1708 }
1709 } else {
1710 self.visited.insert(oid)
1711 };
1712 if !first_walk {
1713 return;
1714 }
1715 if filter.includes_tree(depth) {
1716 if !self.depth_sensitive || self.visited.insert(oid) {
1717 self.list(oid);
1718 }
1719 } else if !self.depth_sensitive {
1720 self.unlisted.insert(oid);
1721 }
1722 if !filter.prunes_tree_entries(depth) {
1723 self.queue.push_back(QueuedObject {
1724 oid,
1725 path_hash,
1726 depth,
1727 found_as_tree: true,
1728 });
1729 }
1730 }
1731
1732 fn push_blob(
1735 &mut self,
1736 oid: ObjectId,
1737 name_hash: Option<u32>,
1738 referrer: ObjectId,
1739 tree_depth: u64,
1740 ) -> Result<()> {
1741 if self.at_object_boundary(oid) {
1742 self.note_boundary(oid);
1743 return Ok(());
1744 }
1745 if self.visited.contains(&oid) {
1746 return Ok(());
1747 }
1748 let (passes, size_checked) = match self.filter {
1749 None => (true, false),
1750 Some(filter) => self
1753 .blob_passes(filter, &oid, tree_depth)?
1754 .unwrap_or((true, true)),
1755 };
1756 if passes && self.missing.is_some() && !self.odb.contains(&oid)? {
1757 self.visited.insert(oid);
1758 if let Some(missing) = self.missing.as_mut() {
1759 missing.push(MissingBlob {
1760 oid,
1761 referrer: Some(referrer),
1762 });
1763 }
1764 return Ok(());
1765 }
1766 if passes {
1767 self.visited.insert(oid);
1768 self.list(oid);
1769 if let Some(hash) = name_hash {
1770 self.name_hashes.insert(oid, hash);
1771 }
1772 } else if size_checked && !self.depth_sensitive {
1773 self.visited.insert(oid);
1774 self.unlisted.insert(oid);
1775 }
1776 Ok(())
1777 }
1778
1779 fn blob_passes(
1786 &self,
1787 filter: &ObjectFilter,
1788 oid: &ObjectId,
1789 tree_depth: u64,
1790 ) -> Result<Option<(bool, bool)>> {
1791 if !filter.includes_blob_under_tree(0, tree_depth) {
1793 return Ok(Some((false, false)));
1794 }
1795 if filter.includes_blob_under_tree(u64::MAX, tree_depth) {
1796 return Ok(Some((true, false)));
1797 }
1798 let info = if self.missing.is_some() {
1799 match self.odb.read_info_without_fetch(oid) {
1800 Err(crate::error::Error::ObjectNotFound(_)) => return Ok(None),
1801 other => other,
1802 }
1803 } else {
1804 self.odb.read_info(oid)
1805 };
1806 match info {
1807 Ok(info) => Ok(Some((
1808 filter.includes_blob_under_tree(info.size, tree_depth),
1809 true,
1810 ))),
1811 Err(_) if self.skip_missing => Ok(Some((false, true))),
1812 Err(e) => Err(e),
1813 }
1814 }
1815}
1816
1817#[cfg(test)]
1818mod tests {
1819 #![allow(clippy::unwrap_used, clippy::expect_used)]
1820
1821 use super::*;
1822
1823 use crate::repo::{InitOptions, Repository};
1824 use tempfile::TempDir;
1825
1826 fn walk(repo: &Repository, roots: &[ObjectId], name_trees: NameTrees) -> PackObjectSelection {
1827 let empty = HashSet::new();
1828 enumerate_pack_objects_naming(
1829 repo.odb(),
1830 roots,
1831 &[],
1832 &PackEnumerateOptions {
1833 filter: None,
1834 shallow_grafts: &empty,
1835 have_shallow_grafts: Some(&empty),
1836 use_bitmaps: false,
1837 exclude_objects: None,
1838 peer_odb: None,
1839 have_boundary: HaveBoundary::CommitClosure,
1840 },
1841 name_trees,
1842 false,
1843 &CancelSignal::never(),
1844 )
1845 .expect("walk")
1846 }
1847
1848 fn write_tree(repo: &Repository, entries: &[(u32, &str, ObjectId)]) -> ObjectId {
1849 let entries: Vec<crate::objects::TreeEntry> = entries
1850 .iter()
1851 .map(|&(mode, name, oid)| crate::objects::TreeEntry {
1852 mode,
1853 name: name.as_bytes().to_vec(),
1854 oid,
1855 })
1856 .collect();
1857 repo.odb()
1858 .write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
1859 .expect("tree")
1860 }
1861
1862 fn write_commit(
1863 repo: &Repository,
1864 tree: ObjectId,
1865 parents: &[ObjectId],
1866 time: i64,
1867 ) -> ObjectId {
1868 let mut body = format!("tree {}\n", tree.to_hex());
1869 for parent in parents {
1870 body.push_str(&format!("parent {}\n", parent.to_hex()));
1871 }
1872 body.push_str(&format!(
1873 "author A <a@example.com> {time} +0000\ncommitter A <a@example.com> {time} +0000\n\nc{time}\n"
1874 ));
1875 repo.odb()
1876 .write(ObjectKind::Commit, body.as_bytes())
1877 .expect("commit")
1878 }
1879
1880 #[test]
1881 fn tips_only_boundary_matches_closure_on_linear_incremental_push() {
1882 let dir = TempDir::new().unwrap();
1883 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1884 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1885 let mut parent = write_commit(
1886 &repo,
1887 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1888 &[],
1889 0,
1890 );
1891 for i in 1..800 {
1892 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1893 parent = write_commit(&repo, tree, &[parent], i);
1894 }
1895 let remote_tip = parent;
1896 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1897 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1898 let empty = HashSet::new();
1899 let closure_opts = PackEnumerateOptions {
1900 filter: None,
1901 shallow_grafts: &empty,
1902 have_shallow_grafts: Some(&empty),
1903 use_bitmaps: false,
1904 exclude_objects: None,
1905 peer_odb: None,
1906 have_boundary: HaveBoundary::CommitClosure,
1907 };
1908 let tips_opts = PackEnumerateOptions {
1909 have_boundary: HaveBoundary::TipsOnly,
1910 ..closure_opts
1911 };
1912 let closure = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &closure_opts)
1913 .expect("closure");
1914 let tips = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &tips_opts)
1915 .expect("tips");
1916 let closure_set: HashSet<ObjectId> = closure.send.iter().copied().collect();
1917 let tips_set: HashSet<ObjectId> = tips.send.iter().copied().collect();
1918 assert_eq!(closure_set, tips_set);
1919 assert!(
1920 tips.send.len() <= 8,
1921 "one new commit should enumerate a handful of objects, got {}",
1922 tips.send.len()
1923 );
1924 }
1925
1926 #[test]
1927 fn tips_only_send_pack_does_not_prewalk_deep_history() {
1928 use crate::merge_base::intersect_walk_test_hooks::{
1929 commit_parent_lookups, reset_commit_parent_lookups,
1930 };
1931
1932 let dir = TempDir::new().unwrap();
1933 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1934 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1935 let mut parent = write_commit(
1936 &repo,
1937 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1938 &[],
1939 0,
1940 );
1941 for i in 1..800 {
1942 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1943 parent = write_commit(&repo, tree, &[parent], i);
1944 }
1945 let remote_tip = parent;
1946 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1947 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1948 let haves = vec![remote_tip];
1949 let empty = HashSet::new();
1950 reset_commit_parent_lookups();
1951 let _selection = enumerate_pack_objects(
1952 repo.odb(),
1953 &[new_tip],
1954 &haves,
1955 &PackEnumerateOptions {
1956 filter: None,
1957 shallow_grafts: &empty,
1958 have_shallow_grafts: Some(&empty),
1959 use_bitmaps: false,
1960 exclude_objects: None,
1961 peer_odb: None,
1962 have_boundary: HaveBoundary::TipsOnly,
1963 },
1964 )
1965 .expect("enumerate");
1966 assert!(
1967 commit_parent_lookups() <= 128,
1968 "send-pack hide must stay coupled to the want walk ({} parent lookups)",
1969 commit_parent_lookups()
1970 );
1971 }
1972
1973 #[test]
1974 fn tree_read_ahead_leaves_the_walk_output_unchanged() {
1975 let dir = TempDir::new().unwrap();
1976 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1977 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1978 let mut tip: Option<ObjectId> = None;
1979 for i in 0..400i64 {
1981 let sub = write_tree(&repo, &[(0o100644, "f", blob(&format!("sub {i}")))]);
1982 let tree = write_tree(
1983 &repo,
1984 &[
1985 (0o040000, "dir", sub),
1986 (0o100644, "top", blob(&format!("top {}", i / 3))),
1987 ],
1988 );
1989 tip = Some(write_commit(&repo, tree, tip.as_slice(), 1_000 + i));
1990 }
1991 let tip = tip.unwrap();
1992 let config_path = repo.git_dir().join("config");
1993 let config = std::fs::read_to_string(&config_path).unwrap();
1994 let walk_with_threads = |threads: u32| {
1995 std::fs::write(
1996 &config_path,
1997 format!("{config}[pack]\n\tthreads = {threads}\n"),
1998 )
1999 .unwrap();
2000 let repo = Repository::open(repo.git_dir(), &RepositoryOptions::empty()).unwrap();
2001 walk(&repo, &[tip], NameTrees::Yes)
2002 };
2003 let serial = walk_with_threads(1);
2004 let parallel = walk_with_threads(4);
2005 assert_eq!(serial.send.len(), 400 * 3 + 400 + 134);
2006 assert_eq!(parallel.send, serial.send);
2007 assert_eq!(parallel.name_hashes, serial.name_hashes);
2008 }
2009
2010 #[test]
2011 fn send_pack_walk_without_haves_reads_no_commits() {
2012 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2013 let dir = TempDir::new().unwrap();
2014 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2015 let tree = write_tree(&repo, &[]);
2016 let mut tip = write_commit(&repo, tree, &[], 1_300_000_000);
2017 for i in 1..50 {
2018 tip = write_commit(&repo, tree, &[tip], 1_300_000_000 + i);
2019 }
2020
2021 reset_commit_reads();
2022 let hidden = send_pack_uninteresting_commits(
2023 repo.odb(),
2024 &[tip],
2025 &[],
2026 &HashSet::new(),
2027 &CancelSignal::never(),
2028 )
2029 .unwrap();
2030 assert!(hidden.is_empty());
2031 assert_eq!(
2032 commit_reads(),
2033 0,
2034 "nothing can be uninteresting without a have"
2035 );
2036
2037 let selection = walk(&repo, &[tip], NameTrees::No);
2038 assert_eq!(
2039 selection.send.len(),
2040 51,
2041 "every commit and the tree are still sent"
2042 );
2043 }
2044
2045 #[test]
2046 fn push_dated_before_graphed_history_stops_at_the_frontier() {
2047 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2048 let dir = TempDir::new().unwrap();
2049 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2050 let tree = write_tree(&repo, &[]);
2051 let mut tip = write_commit(&repo, tree, &[], 1_300_000_000);
2053 let mut tagged = tip;
2054 for i in 1..300 {
2055 tip = write_commit(&repo, tree, &[tip], 1_300_000_000 + i);
2056 if i == 150 {
2057 tagged = tip;
2058 }
2059 }
2060 std::fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n")).unwrap();
2061 repo.odb().write_commit_graph().unwrap();
2062 let mut new_tip = tip;
2065 for i in 0..3 {
2066 new_tip = write_commit(&repo, tree, &[new_tip], 1_000_000_000 + i);
2067 }
2068
2069 reset_commit_reads();
2070 let hidden = send_pack_uninteresting_commits(
2071 repo.odb(),
2072 &[new_tip],
2073 &[tip, tagged],
2074 &HashSet::new(),
2075 &CancelSignal::never(),
2076 )
2077 .unwrap();
2078 assert!(hidden.contains(&tip));
2079 assert!(hidden.contains(&tagged));
2080 assert!(!hidden.contains(&new_tip));
2081 assert_eq!(
2082 commit_reads(),
2083 3,
2084 "only the commits outside the graph are read from the object store"
2085 );
2086 assert!(
2087 hidden.len() <= 4,
2088 "the walk must stop at the frontier, not drain the remote's history ({} marked)",
2089 hidden.len()
2090 );
2091 }
2092
2093 #[test]
2094 fn push_dated_before_packed_history_reads_commit_headers() {
2095 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2096 let dir = TempDir::new().unwrap();
2097 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2098 let tree = write_tree(&repo, &[]);
2099 let body = "a long message line\n".repeat(100);
2100 let write = |parents: &[ObjectId], time: i64| {
2101 let mut text = format!("tree {tree}\n");
2102 for parent in parents {
2103 text.push_str(&format!("parent {parent}\n"));
2104 }
2105 text.push_str(&format!(
2106 "author A <a@example.com> {time} +0000\n\
2107 committer A <a@example.com> {time} +0000\n\nc{time}\n\n{body}"
2108 ));
2109 repo.odb()
2110 .write(ObjectKind::Commit, text.as_bytes())
2111 .unwrap()
2112 };
2113 let root = write(&[], 1_300_000_000);
2115 let mut history = vec![root];
2116 let mut tip = root;
2117 for i in 1..120 {
2118 tip = write(&[tip], 1_300_000_000 + i);
2119 history.push(tip);
2120 }
2121 let side = write(&[root], 1_300_000_500);
2122 tip = write(&[tip, side], 1_300_000_600);
2123 history.extend([side, tip]);
2124 let mut new_tip = tip;
2126 let mut new_commits = Vec::new();
2127 for i in 0..3 {
2128 new_tip = write(&[new_tip], 1_000_000_000 + i);
2129 new_commits.push(new_tip);
2130 }
2131 std::fs::write(
2132 repo.git_dir().join("refs/heads/main"),
2133 format!("{new_tip}\n"),
2134 )
2135 .unwrap();
2136 crate::maintenance::repack_all(&repo, &crate::maintenance::RepackOptions::default())
2137 .unwrap();
2138 assert!(repo.odb().commit_graph_chain().is_none());
2139 let mut header = Vec::new();
2140 assert!(repo
2141 .odb()
2142 .read_packed_commit_header_block_into(&tip, &mut header)
2143 .unwrap());
2144
2145 reset_commit_reads();
2146 let hidden = repo
2147 .odb()
2148 .with_pack_read_context(|| {
2149 send_pack_uninteresting_commits(
2150 repo.odb(),
2151 &[new_tip],
2152 &[tip],
2153 &HashSet::new(),
2154 &CancelSignal::never(),
2155 )
2156 })
2157 .unwrap();
2158 let expected: HashSet<ObjectId> = history.iter().copied().collect();
2159 assert_eq!(hidden, expected, "the remote's whole history, nothing new");
2160 assert!(new_commits.iter().all(|c| !hidden.contains(c)));
2161 assert_eq!(commit_reads(), history.len() + new_commits.len());
2162 }
2163
2164 #[test]
2165 fn duplicated_wants_already_on_peer_balance_the_frontier() {
2166 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2167 let dir = TempDir::new().unwrap();
2168 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2169 let tree = write_tree(&repo, &[]);
2170 let mut tip = write_commit(&repo, tree, &[], 0);
2171 for time in 1..100 {
2172 tip = write_commit(&repo, tree, &[tip], time);
2173 }
2174 reset_commit_reads();
2175 let hidden = send_pack_uninteresting_commits(
2176 repo.odb(),
2177 &[tip, tip],
2178 &[tip, tip],
2179 &HashSet::new(),
2180 &CancelSignal::never(),
2181 )
2182 .unwrap();
2183 assert!(hidden.contains(&tip));
2184 assert!(
2185 commit_reads() <= 2,
2186 "shared want/have must stop at the frontier"
2187 );
2188 let empty = HashSet::new();
2189 let selection = enumerate_pack_objects(
2190 repo.odb(),
2191 &[tip, tip],
2192 &[tip, tip],
2193 &PackEnumerateOptions {
2194 filter: None,
2195 shallow_grafts: &empty,
2196 have_shallow_grafts: None,
2197 use_bitmaps: false,
2198 exclude_objects: None,
2199 peer_odb: None,
2200 have_boundary: HaveBoundary::TipsOnly,
2201 },
2202 )
2203 .unwrap();
2204 assert!(selection.send.is_empty());
2205 }
2206
2207 #[test]
2208 fn tips_only_many_advertised_haves_does_not_prewalk_deep_history() {
2209 use crate::merge_base::intersect_walk_test_hooks::{
2210 commit_parent_lookups, reset_commit_parent_lookups,
2211 };
2212
2213 let dir = TempDir::new().unwrap();
2214 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2215 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2216 let mut chain = Vec::new();
2217 let mut parent = write_commit(
2218 &repo,
2219 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
2220 &[],
2221 0,
2222 );
2223 chain.push(parent);
2224 for i in 1..800 {
2225 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
2226 parent = write_commit(&repo, tree, &[parent], i);
2227 chain.push(parent);
2228 }
2229 let remote_tip = parent;
2230 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
2231 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
2232 let mut haves = vec![remote_tip];
2233 for n in (2..400).step_by(2) {
2234 let idx = chain.len().checked_sub(1 + n);
2235 if let Some(idx) = idx {
2236 haves.push(chain[idx]);
2237 }
2238 }
2239 let empty = HashSet::new();
2240 reset_commit_parent_lookups();
2241 let selection = enumerate_pack_objects(
2242 repo.odb(),
2243 &[new_tip],
2244 &haves,
2245 &PackEnumerateOptions {
2246 filter: None,
2247 shallow_grafts: &empty,
2248 have_shallow_grafts: Some(&empty),
2249 use_bitmaps: false,
2250 exclude_objects: None,
2251 peer_odb: None,
2252 have_boundary: HaveBoundary::TipsOnly,
2253 },
2254 )
2255 .expect("enumerate");
2256 assert!(
2257 selection.send.len() <= 8,
2258 "one new commit should stay small with many haves, got {}",
2259 selection.send.len()
2260 );
2261 assert!(
2262 commit_parent_lookups() <= 512,
2263 "many advertised haves must not pre-walk deep history ({} parent lookups)",
2264 commit_parent_lookups()
2265 );
2266 }
2267
2268 #[test]
2269 fn send_pack_have_walk_stops_without_scanning_deep_haves() {
2270 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2271
2272 let dir = TempDir::new().unwrap();
2273 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2274 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2275 let mut parent = write_commit(
2276 &repo,
2277 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
2278 &[],
2279 0,
2280 );
2281 let near_root = parent;
2282 for i in 1..800 {
2283 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
2284 parent = write_commit(&repo, tree, &[parent], i);
2285 }
2286 let direct_parent = parent;
2287 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
2288 let new_tip = write_commit(&repo, new_tree, &[direct_parent], 900);
2289 let empty = HashSet::new();
2290 reset_commit_reads();
2291 let have_commits = send_pack_uninteresting_commits(
2292 repo.odb(),
2293 &[new_tip],
2294 &[direct_parent, near_root],
2295 &empty,
2296 &CancelSignal::never(),
2297 )
2298 .expect("send-pack have walk");
2299 assert!(have_commits.contains(&direct_parent));
2300 assert!(have_commits.contains(&near_root));
2301 let reads = commit_reads();
2302 assert!(
2303 reads < 20,
2304 "send-pack have walk should not scan deep history, read {reads} commits"
2305 );
2306 }
2307
2308 #[test]
2309 fn walk_lists_trees_of_newer_commits_first() {
2310 let dir = TempDir::new().unwrap();
2311 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2312 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2313 let tree_a = write_tree(&repo, &[(0o100644, "f", blob("a"))]);
2314 let tree_b = write_tree(&repo, &[(0o100644, "f", blob("b"))]);
2315 let tree_c = write_tree(&repo, &[(0o100644, "f", blob("c"))]);
2316 let base = write_commit(&repo, tree_a, &[], 100);
2317 let old_tip = write_commit(&repo, tree_b, &[base], 200);
2319 let new_tip = write_commit(&repo, tree_c, &[base], 300);
2320
2321 let selection = walk(&repo, &[old_tip, new_tip], NameTrees::No);
2322 let pos = |oid: ObjectId| selection.send.iter().position(|o| *o == oid).unwrap();
2323 assert!(pos(old_tip) < pos(base) && pos(new_tip) < pos(base));
2324 assert!(pos(tree_c) < pos(tree_b) && pos(tree_b) < pos(tree_a));
2325 }
2326
2327 #[test]
2328 fn walk_names_trees_and_keeps_history_paths_for_path_less_roots() {
2329 let dir = TempDir::new().unwrap();
2330 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2331 let file = repo.odb().write(ObjectKind::Blob, b"x\n").unwrap();
2332 let link = repo.odb().write(ObjectKind::Blob, b"target").unwrap();
2333 let sub = write_tree(
2334 &repo,
2335 &[(0o100644, "file.rs", file), (0o120000, "link", link)],
2336 );
2337 let root = write_tree(&repo, &[(0o040000, "src", sub)]);
2338 let commit = write_commit(&repo, root, &[], 100);
2339
2340 let selection = walk(&repo, &[sub, file, commit], NameTrees::Yes);
2342 let hash = |oid: ObjectId| selection.name_hashes.get(&oid).copied();
2343 assert_eq!(hash(sub), Some(pack_name_hash("src")));
2344 assert_eq!(hash(file), Some(pack_name_hash("src/file.rs")));
2345 assert_eq!(hash(root), None, "root trees have no path");
2346 assert_eq!(selection.send.len(), 5);
2347
2348 let blobs_only = walk(&repo, &[sub, file, commit], NameTrees::No);
2349 assert_eq!(blobs_only.name_hashes.get(&sub), None);
2350 assert_eq!(
2351 blobs_only.name_hashes.get(&file),
2352 Some(&pack_name_hash("src/file.rs"))
2353 );
2354 }
2355
2356 fn tree_entry(mode: &str, name: &str, oid: ObjectId) -> Vec<u8> {
2357 let mut bytes = format!("{mode} {name}\0").into_bytes();
2358 bytes.extend_from_slice(oid.as_bytes());
2359 bytes
2360 }
2361
2362 #[cfg_attr(
2366 not(feature = "full-tests"),
2367 ignore = "starts system git or is heavy; run with --features full-tests"
2368 )]
2369 #[test]
2370 fn walk_descends_into_trees_with_non_canonical_modes() {
2371 let tmp = tempfile::tempdir().unwrap();
2372 Repository::init(tmp.path(), &InitOptions::new().with_bare(true)).unwrap();
2373 let repo = Repository::open(tmp.path(), &RepositoryOptions::empty()).unwrap();
2374 let odb = repo.odb();
2375 let deep = odb
2376 .write_loose_materialize(ObjectKind::Blob, b"deep\n")
2377 .unwrap();
2378 let odd = odb
2379 .write_loose_materialize(ObjectKind::Blob, b"odd\n")
2380 .unwrap();
2381 let inner = odb
2382 .write_loose_materialize(ObjectKind::Tree, &tree_entry("100644", "deep.txt", deep))
2383 .unwrap();
2384 let mut root = tree_entry("100664", "odd.txt", odd);
2385 root.extend(tree_entry("40755", "sub", inner));
2386 let root = odb
2387 .write_loose_materialize(ObjectKind::Tree, &root)
2388 .unwrap();
2389 let commit = format!(
2390 "tree {root}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nodd modes\n"
2391 );
2392 let commit = odb
2393 .write_loose_materialize(ObjectKind::Commit, commit.as_bytes())
2394 .unwrap();
2395
2396 let empty = HashSet::new();
2397 let selection = enumerate_pack_objects(
2398 odb,
2399 &[commit],
2400 &[],
2401 &PackEnumerateOptions {
2402 filter: None,
2403 shallow_grafts: &empty,
2404 have_shallow_grafts: Some(&empty),
2405 use_bitmaps: false,
2406 exclude_objects: None,
2407 peer_odb: None,
2408 have_boundary: HaveBoundary::CommitClosure,
2409 },
2410 )
2411 .unwrap();
2412
2413 let ours: HashSet<ObjectId> = selection.send.iter().copied().collect();
2414 let git: HashSet<ObjectId> =
2415 grit_test_support::git(tmp.path(), &["rev-list", "--objects", &commit.to_hex()])
2416 .lines()
2417 .map(|line| ObjectId::from_hex(&line[..40]).unwrap())
2418 .collect();
2419 assert_eq!(ours, git, "same objects as git rev-list --objects");
2420 assert!(ours.contains(&inner) && ours.contains(&deep));
2421 assert_eq!(selection.send.len(), 5);
2422 assert_eq!(
2423 selection.name_hashes.get(&deep),
2424 Some(&pack_name_hash("sub/deep.txt"))
2425 );
2426 assert_eq!(
2427 selection.name_hashes.get(&odd),
2428 Some(&pack_name_hash("odd.txt"))
2429 );
2430 }
2431
2432 fn tips_only_options(empty: &HashSet<ObjectId>) -> PackEnumerateOptions<'_> {
2433 PackEnumerateOptions {
2434 filter: None,
2435 shallow_grafts: empty,
2436 have_shallow_grafts: Some(empty),
2437 use_bitmaps: false,
2438 exclude_objects: None,
2439 peer_odb: None,
2440 have_boundary: HaveBoundary::TipsOnly,
2441 }
2442 }
2443
2444 fn write_tag(repo: &Repository, target: ObjectId, kind: &str, name: &str) -> ObjectId {
2445 let body = format!(
2446 "object {}\ntype {kind}\ntag {name}\ntagger A <a@example.com> 0 +0000\n\n{name}\n",
2447 target.to_hex()
2448 );
2449 repo.odb().write(ObjectKind::Tag, body.as_bytes()).unwrap()
2450 }
2451
2452 #[test]
2453 fn tips_only_thin_bases_come_from_edge_commits_not_every_advertised_tip() {
2454 let dir = TempDir::new().unwrap();
2455 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2456 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2457 let base_tree = write_tree(&repo, &[(0o100644, "f", blob("base"))]);
2458 let root = write_commit(&repo, base_tree, &[], 0);
2459 let main = write_commit(
2460 &repo,
2461 write_tree(&repo, &[(0o100644, "f", blob("main"))]),
2462 &[root],
2463 10,
2464 );
2465 let mut haves = vec![main];
2466 for i in 0..40 {
2467 let tree = write_tree(&repo, &[(0o100644, "f", blob(&format!("side {i}")))]);
2468 let side = write_commit(&repo, tree, &[root], 20 + i);
2469 haves.push(write_tag(&repo, side, "commit", &format!("t{i}")));
2470 haves.push(side);
2471 }
2472 let new_tip = write_commit(
2473 &repo,
2474 write_tree(&repo, &[(0o100644, "f", blob("main\nmore"))]),
2475 &[main],
2476 100,
2477 );
2478 let empty = HashSet::new();
2479 let selection =
2480 enumerate_pack_objects(repo.odb(), &[new_tip], &haves, &tips_only_options(&empty))
2481 .unwrap();
2482 assert_eq!(selection.send.len(), 3, "commit, tree and the changed blob");
2483 assert_eq!(
2484 selection.have_closure,
2485 HashSet::from([main]),
2486 "thin bases come from the commit the walk stopped at"
2487 );
2488
2489 let closure = enumerate_pack_objects(
2490 repo.odb(),
2491 &[new_tip],
2492 &haves,
2493 &PackEnumerateOptions {
2494 have_boundary: HaveBoundary::CommitClosure,
2495 ..tips_only_options(&empty)
2496 },
2497 )
2498 .unwrap();
2499 assert_eq!(closure.send, selection.send);
2500 assert_eq!(
2501 closure.have_closure,
2502 HashSet::from([main]),
2503 "the full have pre-walk seeds thin bases from the same edge commit"
2504 );
2505 }
2506
2507 #[test]
2508 fn send_pack_walk_peels_tag_haves_and_ignores_non_commit_targets() {
2509 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2510 let dir = TempDir::new().unwrap();
2511 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2512 let blob = repo.odb().write(ObjectKind::Blob, b"x").unwrap();
2513 let tree = write_tree(&repo, &[(0o100644, "f", blob)]);
2514 let old = write_commit(&repo, tree, &[], 0);
2515 let tip = write_commit(&repo, tree, &[old], 1);
2516 let tag = write_tag(&repo, tip, "commit", "v1");
2517 let nested = write_tag(&repo, tag, "tag", "v1-nested");
2518 let tree_tag = write_tag(&repo, tree, "tree", "tree-tag");
2519 let blob_tag = write_tag(&repo, blob, "blob", "blob-tag");
2520 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
2521 let new_tip = write_commit(&repo, tree, &[tip], 2);
2522
2523 reset_commit_reads();
2524 let hidden = send_pack_uninteresting_commits(
2525 repo.odb(),
2526 &[new_tip],
2527 &[nested, tag, tree_tag, blob_tag, missing],
2528 &HashSet::new(),
2529 &CancelSignal::never(),
2530 )
2531 .unwrap();
2532 assert!(hidden.contains(&tip), "tags peel to the commit they name");
2533 for oid in [
2534 tag, nested, tree_tag, blob_tag, tree, blob, missing, new_tip,
2535 ] {
2536 assert!(!hidden.contains(&oid), "{oid} is not a have-side commit");
2537 }
2538 assert_eq!(
2539 commit_reads(),
2540 2,
2541 "the want and the tag-named commit, each queued once"
2542 );
2543 }
2544
2545 #[test]
2546 fn common_prefix_and_suffix_lengths_cover_chunked_and_tail_bytes() {
2547 let a: Vec<u8> = (0..100u8).collect();
2548 assert_eq!(common_prefix_len(&a, &a), 100);
2549 assert_eq!(common_suffix_len(&a, &a), 100);
2550 assert_eq!(common_prefix_len(&a, &[]), 0);
2551 assert_eq!(common_suffix_len(&[], &a), 0);
2552 for at in [0, 1, 15, 16, 17, 31, 32, 63, 64, 99] {
2553 let mut b = a.clone();
2554 b[at] ^= 0xff;
2555 assert_eq!(common_prefix_len(&a, &b), at, "prefix, mismatch at {at}");
2556 assert_eq!(
2557 common_suffix_len(&a, &b),
2558 99 - at,
2559 "suffix, mismatch at {at}"
2560 );
2561 }
2562 assert_eq!(common_prefix_len(&a[..40], &a), 40);
2564 assert_eq!(common_suffix_len(&a[37..], &a), 63);
2565 assert_eq!(common_suffix_len(&a[..50], &a), 0);
2566 }
2567
2568 fn reachable_with_blob_paths(
2571 repo: &Repository,
2572 tip: ObjectId,
2573 ) -> (HashSet<ObjectId>, HashMap<ObjectId, HashSet<String>>) {
2574 let mut objects = HashSet::new();
2575 let mut blob_paths: HashMap<ObjectId, HashSet<String>> = HashMap::new();
2576 let mut commits = vec![tip];
2577 let mut trees = Vec::new();
2578 while let Some(commit) = commits.pop() {
2579 if !objects.insert(commit) {
2580 continue;
2581 }
2582 let obj = repo.odb().read(&commit).unwrap();
2583 let parsed = CommitRef::parse(&obj.data).unwrap();
2584 commits.extend(parsed.parents().iter().copied());
2585 trees.push((parsed.tree(), String::new()));
2586 }
2587 while let Some((tree, path)) = trees.pop() {
2588 objects.insert(tree);
2589 let obj = repo.odb().read(&tree).unwrap();
2590 for entry in TreeRefIter::with_oid_len(&obj.data, 20) {
2591 let entry = entry.unwrap();
2592 let child = if path.is_empty() {
2593 String::from_utf8(entry.name.to_vec()).unwrap()
2594 } else {
2595 format!("{path}/{}", String::from_utf8_lossy(entry.name))
2596 };
2597 match entry.mode {
2598 0o160000 => {}
2599 0o040000 => trees.push((entry.oid, child)),
2600 0o120000 => {
2601 objects.insert(entry.oid);
2602 }
2603 _ => {
2604 objects.insert(entry.oid);
2605 blob_paths.entry(entry.oid).or_default().insert(child);
2606 }
2607 }
2608 }
2609 }
2610 (objects, blob_paths)
2611 }
2612
2613 fn assert_walk_lists_reachable(repo: &Repository, tip: ObjectId) {
2614 let walked = walk(repo, &[tip], NameTrees::Yes);
2615 let listed: HashSet<ObjectId> = walked.send.iter().copied().collect();
2616 assert_eq!(listed.len(), walked.send.len(), "nothing is listed twice");
2617 let (expected, blob_paths) = reachable_with_blob_paths(repo, tip);
2618 assert_eq!(listed, expected);
2619 for (blob, paths) in &blob_paths {
2620 let hash = walked.name_hashes.get(blob).copied();
2621 assert!(
2622 paths.iter().any(|p| Some(pack_name_hash(p)) == hash),
2623 "blob {blob} is named after one of its paths {paths:?}"
2624 );
2625 }
2626 }
2627
2628 struct Xorshift(u64);
2630
2631 impl Xorshift {
2632 fn below(&mut self, n: usize) -> usize {
2633 self.0 ^= self.0 << 13;
2634 self.0 ^= self.0 >> 7;
2635 self.0 ^= self.0 << 17;
2636 (self.0 % n as u64) as usize
2637 }
2638 }
2639
2640 #[test]
2641 fn walk_lists_every_entry_of_successive_tree_versions() {
2642 let dir = TempDir::new().unwrap();
2643 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2644 let mut rng = Xorshift(0x2545_f491_4f6c_dd1d);
2645 let mut next_blob = 0usize;
2646 let mut new_blob = |repo: &Repository| {
2647 next_blob += 1;
2648 repo.odb()
2649 .write(ObjectKind::Blob, format!("blob {next_blob}\n").as_bytes())
2650 .unwrap()
2651 };
2652 let mut root: std::collections::BTreeMap<String, ObjectId> = (0..40)
2654 .map(|i| {
2655 (
2656 format!("f{}", "x".repeat(i % 7) + &i.to_string()),
2657 new_blob(&repo),
2658 )
2659 })
2660 .collect();
2661 let mut sub: std::collections::BTreeMap<String, ObjectId> = (0..12)
2662 .map(|i| (format!("s{i}"), new_blob(&repo)))
2663 .collect();
2664 let gitlink = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
2665 let link_target = repo.odb().write(ObjectKind::Blob, b"target").unwrap();
2666 let mut parent: Option<ObjectId> = None;
2667 for version in 0..300i64 {
2668 let files = if rng.below(3) == 0 {
2669 &mut sub
2670 } else {
2671 &mut root
2672 };
2673 let names: Vec<String> = files.keys().cloned().collect();
2674 match rng.below(4) {
2675 0 if names.len() > 2 => {
2676 files.remove(&names[rng.below(names.len())]);
2677 }
2678 1 => {
2679 let name = format!("f{}", "y".repeat(rng.below(5)) + &version.to_string());
2680 files.insert(name, new_blob(&repo));
2681 }
2682 _ => {
2683 let name = names[rng.below(names.len())].clone();
2684 let blob = if rng.below(4) == 0 {
2686 *files.values().next().unwrap()
2687 } else {
2688 new_blob(&repo)
2689 };
2690 files.insert(name, blob);
2691 }
2692 }
2693 let sub_entries: Vec<(u32, &str, ObjectId)> = sub
2694 .iter()
2695 .map(|(name, &oid)| (0o100644, name.as_str(), oid))
2696 .collect();
2697 let sub_tree = write_tree(&repo, &sub_entries);
2698 let mut entries: Vec<(u32, &str, ObjectId)> = vec![
2699 (0o160000, "a-module", gitlink),
2700 (0o120000, "a-link", link_target),
2701 (0o040000, "d", sub_tree),
2702 ];
2703 entries.extend(
2704 root.iter()
2705 .map(|(name, &oid)| (0o100644, name.as_str(), oid)),
2706 );
2707 entries.sort_by(|a, b| a.1.cmp(b.1));
2708 let tree = write_tree(&repo, &entries);
2709 let parents: Vec<ObjectId> = parent.into_iter().collect();
2710 parent = Some(write_commit(&repo, tree, &parents, version));
2711 }
2712 assert_walk_lists_reachable(&repo, parent.unwrap());
2713 }
2714
2715 #[test]
2716 fn walk_reparses_a_tree_whose_shared_tail_starts_inside_an_entry() {
2717 let dir = TempDir::new().unwrap();
2718 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
2719 let kept = repo.odb().write(ObjectKind::Blob, b"kept").unwrap();
2720 let older = repo.odb().write(ObjectKind::Blob, b"older").unwrap();
2721 let newer = repo.odb().write(ObjectKind::Blob, b"newer").unwrap();
2722 let old_tree = write_tree(&repo, &[(0o100644, "a", older), (0o100644, "g", kept)]);
2727 let new_tree = write_tree(
2728 &repo,
2729 &[(0o100644, "a", newer), (0o100644, "w100644 g", kept)],
2730 );
2731 let old = write_commit(&repo, old_tree, &[], 1);
2732 let new = write_commit(&repo, new_tree, &[old], 2);
2733 assert_walk_lists_reachable(&repo, new);
2734 }
2735
2736 #[cfg_attr(
2737 not(feature = "full-tests"),
2738 ignore = "starts system git; run with --features full-tests"
2739 )]
2740 #[test]
2741 fn walk_matches_git_rev_list_objects_on_edited_history() {
2742 let dir = TempDir::new().unwrap();
2743 let git = |args: &[&str]| {
2744 let out = std::process::Command::new("git")
2745 .current_dir(dir.path())
2746 .args(args)
2747 .env("GIT_CONFIG_GLOBAL", "/dev/null")
2748 .env("GIT_CONFIG_SYSTEM", "/dev/null")
2749 .env("GIT_AUTHOR_NAME", "T")
2750 .env("GIT_AUTHOR_EMAIL", "t@example.com")
2751 .env("GIT_COMMITTER_NAME", "T")
2752 .env("GIT_COMMITTER_EMAIL", "t@example.com")
2753 .output()
2754 .expect("run git");
2755 assert!(out.status.success(), "git {args:?}: {out:?}");
2756 String::from_utf8(out.stdout).unwrap()
2757 };
2758 git(&["init", "-q", "-b", "main"]);
2759 let mut rng = Xorshift(0x9e37_79b9_7f4a_7c15);
2760 for i in 0..60 {
2761 let dir_name = ["src", "src/deep", "docs", "."][rng.below(4)];
2762 std::fs::create_dir_all(dir.path().join(dir_name)).unwrap();
2763 let file = dir
2764 .path()
2765 .join(dir_name)
2766 .join(format!("f{}", rng.below(15)));
2767 if rng.below(5) == 0 && file.exists() {
2768 std::fs::remove_file(&file).unwrap();
2769 } else {
2770 std::fs::write(&file, format!("{i}\n")).unwrap();
2771 }
2772 git(&["add", "-A"]);
2773 git(&["commit", "-q", "--allow-empty", "-m", &format!("c{i}")]);
2774 }
2775 git(&["repack", "-adq"]);
2776 let expected: HashSet<ObjectId> = git(&["rev-list", "--objects", "--all"])
2777 .lines()
2778 .map(|line| ObjectId::from_hex(&line[..40]).unwrap())
2779 .collect();
2780 let repo =
2781 Repository::open(&dir.path().join(".git"), &RepositoryOptions::empty()).expect("open");
2782 let tip = ObjectId::from_hex(git(&["rev-parse", "HEAD"]).trim()).unwrap();
2783 let walked = walk(&repo, &[tip], NameTrees::Yes);
2784 let listed: HashSet<ObjectId> = walked.send.iter().copied().collect();
2785 assert_eq!(listed.len(), walked.send.len());
2786 assert_eq!(listed, expected);
2787 assert_walk_lists_reachable(&repo, tip);
2788 }
2789}