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 CommitRef, Object, ObjectId, ObjectKind, OidHashMap, OidHashSet, TreeRefIter,
15};
16use crate::odb::store::{CompositeStore, ObjectStore};
17use crate::odb::Odb;
18use crate::pack_bitmap::BitmapIndex;
19use crate::pack_name_hash::pack_name_hash;
20use crate::progress::CancelSignal;
21use crate::repo::Repository;
22use crate::rev_list::{MissingAction, ObjectFilter};
23
24#[derive(Clone)]
26#[allow(clippy::type_complexity)]
27pub struct PackObjectSelection {
28 pub send: Vec<ObjectId>,
30 pub have_closure: HashSet<ObjectId>,
35 pub peer_held: Option<Arc<ReachableSet>>,
37 pub name_hashes: HashMap<ObjectId, u32>,
41}
42
43impl fmt::Debug for PackObjectSelection {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.debug_struct("PackObjectSelection")
46 .field("send", &self.send)
47 .field("have_closure", &self.have_closure)
48 .field(
49 "peer_held",
50 &self
51 .peer_held
52 .as_ref()
53 .map(|held| held.count())
54 .unwrap_or(0),
55 )
56 .field("name_hashes", &self.name_hashes)
57 .finish()
58 }
59}
60
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
63pub enum HaveBoundary {
64 #[default]
66 CommitClosure,
67 TipsOnly,
71}
72
73#[derive(Clone, Copy, Debug)]
75pub struct PackEnumerateOptions<'a> {
76 pub filter: Option<&'a ObjectFilter>,
78 pub shallow_grafts: &'a HashSet<ObjectId>,
82 pub have_shallow_grafts: Option<&'a HashSet<ObjectId>>,
87 pub use_bitmaps: bool,
89 pub exclude_objects: Option<&'a HashSet<ObjectId>>,
91 pub peer_odb: Option<&'a Odb>,
93 pub have_boundary: HaveBoundary,
95}
96
97impl PackEnumerateOptions<'_> {
98 fn has_shallow_grafts(&self) -> bool {
101 !self.shallow_grafts.is_empty() || self.have_shallow_grafts.is_some_and(|g| !g.is_empty())
102 }
103}
104
105pub fn enumerate_pack_objects(
111 odb: &Odb,
112 wants: &[ObjectId],
113 haves: &[ObjectId],
114 opts: &PackEnumerateOptions<'_>,
115) -> Result<PackObjectSelection> {
116 enumerate_pack_objects_naming(
117 odb,
118 wants,
119 haves,
120 opts,
121 NameTrees::No,
122 false,
123 &CancelSignal::never(),
124 )
125}
126
127#[derive(Clone, Copy, PartialEq, Eq)]
129pub(crate) enum NameTrees {
130 No,
132 Yes,
134}
135
136impl NameTrees {
137 pub(crate) fn for_delta_search(opts: &crate::pack_objects::PackBuildOptions) -> Self {
140 if opts.delta && (opts.thin || (opts.window > 0 && opts.max_depth > 0)) {
141 Self::Yes
142 } else {
143 Self::No
144 }
145 }
146}
147
148pub(crate) fn enumerate_pack_objects_naming(
152 odb: &Odb,
153 wants: &[ObjectId],
154 haves: &[ObjectId],
155 opts: &PackEnumerateOptions<'_>,
156 name_trees: NameTrees,
157 local_only: bool,
158 stop: &CancelSignal<'_>,
159) -> Result<PackObjectSelection> {
160 stop.poll()?;
161 if !local_only && opts.use_bitmaps {
162 if let Some(sel) = try_bitmap_enumeration(odb, wants, haves, opts)? {
163 stop.poll()?;
164 return Ok(sel);
165 }
166 }
167 let alternate_sources = if local_only {
168 AlternateSources::load(odb)?.composite()
169 } else {
170 None
171 };
172 walk_enumeration(
173 odb,
174 wants,
175 haves,
176 opts,
177 name_trees,
178 alternate_sources,
179 MissingBlobMode::Fail,
180 stop,
181 )
182 .map(|(selection, _)| selection)
183}
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub(crate) enum MissingBlobMode {
188 Fail,
190 Probe,
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub(crate) struct MissingBlob {
198 pub(crate) oid: ObjectId,
200 pub(crate) referrer: Option<ObjectId>,
202}
203
204pub(crate) fn enumerate_pack_objects_probing(
210 odb: &Odb,
211 wants: &[ObjectId],
212 haves: &[ObjectId],
213 opts: &PackEnumerateOptions<'_>,
214 name_trees: NameTrees,
215 stop: &CancelSignal<'_>,
216) -> Result<(PackObjectSelection, Vec<MissingBlob>)> {
217 stop.poll()?;
218 let opts = PackEnumerateOptions {
219 use_bitmaps: false,
220 ..*opts
221 };
222 walk_enumeration(
223 odb,
224 wants,
225 haves,
226 &opts,
227 name_trees,
228 None,
229 MissingBlobMode::Probe,
230 stop,
231 )
232}
233
234pub fn try_bitmap_reachable_for_upload(
238 odb: &Odb,
239 wants: &[ObjectId],
240 haves: &[ObjectId],
241 filter: Option<&ObjectFilter>,
242 shallow_grafts: &HashSet<ObjectId>,
243) -> Result<Option<(ReachableSet, ReachableSet)>> {
244 if !shallow_grafts.is_empty() || filter.is_some_and(selects_object_type) {
245 return Ok(None);
246 }
247 let Some(git_dir) = odb.config_git_dir() else {
248 return Ok(None);
249 };
250 let repo = match Repository::open(git_dir, &RepositoryOptions::empty()) {
251 Ok(r) => r,
252 Err(_) => return Ok(None),
253 };
254 let Some(index) = BitmapIndex::open(&repo).ok().flatten() else {
255 return Ok(None);
256 };
257 let index = Arc::new(index);
258 match index.reachability_and_haves(
259 &repo,
260 ReachabilityQuery {
261 wants,
262 haves,
263 filter,
264 },
265 MissingAction::Error,
266 MissingAction::Allow,
267 ) {
268 Ok(sets) => Ok(Some(sets)),
269 Err(BitmapWalkError::Unsupported(_)) => Ok(None),
270 Err(BitmapWalkError::Bitmap(_)) => Ok(None),
271 Err(BitmapWalkError::Repository(e)) => Err(e),
272 }
273}
274
275fn selects_object_type(filter: &ObjectFilter) -> bool {
278 match filter {
279 ObjectFilter::ObjectType(_) => true,
280 ObjectFilter::Combine(parts) => parts.iter().any(selects_object_type),
281 _ => false,
282 }
283}
284
285fn try_bitmap_enumeration(
286 odb: &Odb,
287 wants: &[ObjectId],
288 haves: &[ObjectId],
289 opts: &PackEnumerateOptions<'_>,
290) -> Result<Option<PackObjectSelection>> {
291 if opts.has_shallow_grafts() {
292 return Ok(None);
293 }
294 let Some((send_set, peer_held)) =
295 try_bitmap_reachable_for_upload(odb, wants, haves, opts.filter, opts.shallow_grafts)?
296 else {
297 return Ok(None);
298 };
299 let index = send_set.bitmap_index();
300 let send: Vec<ObjectId> = send_set.iter_grouped_by_kind().collect();
301 let have_closure: HashSet<ObjectId> = haves.iter().copied().collect();
302 let name_hashes = name_hashes_from_bitmap(index, &send, &HashSet::new());
303 Ok(Some(PackObjectSelection {
304 send,
305 have_closure,
306 peer_held: Some(Arc::new(peer_held)),
307 name_hashes,
308 }))
309}
310
311pub(crate) fn name_hashes_for_send(
312 index: &BitmapIndex,
313 send: &[ObjectId],
314 have_closure: &HashSet<ObjectId>,
315) -> HashMap<ObjectId, u32> {
316 name_hashes_from_bitmap(index, send, have_closure)
317}
318
319fn name_hashes_from_bitmap(
320 index: &BitmapIndex,
321 send: &[ObjectId],
322 have_closure: &HashSet<ObjectId>,
323) -> HashMap<ObjectId, u32> {
324 let mut out = HashMap::new();
325 for oid in send.iter().chain(have_closure.iter()) {
326 let Some(pos) = index.position_of(oid) else {
327 continue;
328 };
329 if let Some(h) = index.name_hash(pos) {
330 out.insert(*oid, h);
331 }
332 }
333 out
334}
335
336#[allow(clippy::too_many_arguments)]
337fn walk_enumeration(
338 odb: &Odb,
339 wants: &[ObjectId],
340 haves: &[ObjectId],
341 opts: &PackEnumerateOptions<'_>,
342 name_trees: NameTrees,
343 alternate_sources: Option<Arc<CompositeStore>>,
344 missing_mode: MissingBlobMode,
345 stop: &CancelSignal<'_>,
346) -> Result<(PackObjectSelection, Vec<MissingBlob>)> {
347 let have_shallow = opts.have_shallow_grafts.unwrap_or(opts.shallow_grafts);
348 let have_commits = match opts.have_boundary {
349 HaveBoundary::CommitClosure => {
350 commits_reachable_from_haves(odb, haves, have_shallow, true, stop)?
351 }
352 HaveBoundary::TipsOnly => {
353 send_pack_uninteresting_commits(odb, wants, haves, have_shallow, stop)?
354 }
355 };
356 let mut boundary: HashSet<ObjectId> = HashSet::new();
357 if let Some(extra) = opts.exclude_objects {
358 boundary.extend(extra.iter().copied());
359 }
360 let peer_held = try_peer_held_bitmap(odb, haves, opts)?;
361 stop.poll()?;
362 let mut missing = Vec::new();
363 let (send, name_hashes, edges) = match opts.filter {
364 Some(filter) if filter.uses_paths() => {
365 let (mut send, name_hashes) = rev_list_filtered_objects(
366 odb,
367 wants,
368 haves,
369 filter,
370 &have_commits,
371 missing_mode,
372 stop,
373 )?;
374 if missing_mode == MissingBlobMode::Probe {
375 missing = take_unstored_blobs(odb, &mut send)?;
376 }
377 (send, name_hashes, None)
378 }
379 filter => {
380 let walked = collect_reachable_excluding_with_name_hashes(
381 odb,
382 wants,
383 &HashSet::new(),
384 &mut boundary,
385 &have_commits,
386 opts.peer_odb,
387 alternate_sources,
388 false,
389 opts.shallow_grafts,
390 filter,
391 name_trees,
392 missing_mode,
393 stop,
394 )?;
395 missing = walked.missing;
396 (walked.send, walked.name_hashes, Some(walked.edges))
397 }
398 };
399 let have_closure = match edges {
402 Some(edges) => edges,
403 None => haves.iter().copied().collect(),
404 };
405 Ok((
406 PackObjectSelection {
407 send,
408 have_closure,
409 peer_held,
410 name_hashes,
411 },
412 missing,
413 ))
414}
415
416fn take_unstored_blobs(odb: &Odb, send: &mut Vec<ObjectId>) -> Result<Vec<MissingBlob>> {
420 let present = odb.contains_many(send)?;
421 let mut missing = Vec::new();
422 let mut present = present.into_iter();
423 send.retain(|oid| {
424 let keep = present.next().unwrap_or(true);
425 if !keep {
426 missing.push(MissingBlob {
427 oid: *oid,
428 referrer: None,
429 });
430 }
431 keep
432 });
433 Ok(missing)
434}
435
436#[derive(Clone, Copy, Debug, PartialEq, Eq)]
438pub(crate) struct TagCandidate {
439 pub(crate) tag: ObjectId,
441 pub(crate) peeled: ObjectId,
443}
444
445pub(crate) fn append_included_tags(
463 odb: &Odb,
464 send: &mut Vec<ObjectId>,
465 candidates: &[TagCandidate],
466) -> Result<()> {
467 if candidates.is_empty() {
468 return Ok(());
469 }
470 let mut named = oid_hash_set_with_capacity(candidates.len() * 2);
471 for c in candidates {
472 named.insert(c.tag);
473 named.insert(c.peeled);
474 }
475 let present: OidHashSet = send
476 .iter()
477 .filter(|o| named.contains(*o))
478 .copied()
479 .collect();
480 let mut added = included_tag_chains(odb, candidates, |oid| present.contains(oid))?;
481 let unnamed: OidHashSet = added
482 .iter()
483 .filter(|o| !named.contains(*o))
484 .copied()
485 .collect();
486 if !unnamed.is_empty() {
487 let already_sent: OidHashSet = send
488 .iter()
489 .filter(|o| unnamed.contains(*o))
490 .copied()
491 .collect();
492 added.retain(|oid| !already_sent.contains(oid));
493 }
494 send.extend(added);
495 Ok(())
496}
497
498pub(crate) fn included_tag_chains(
511 odb: &Odb,
512 candidates: &[TagCandidate],
513 in_pack: impl Fn(&ObjectId) -> bool,
514) -> Result<Vec<ObjectId>> {
515 let mut seen = oid_hash_set();
516 let mut out = Vec::new();
517 for c in candidates {
518 if !in_pack(&c.peeled) || in_pack(&c.tag) {
519 continue;
520 }
521 let mut current = c.tag;
522 while current != c.peeled && seen.insert(current) {
523 if !in_pack(¤t) {
524 out.push(current);
525 }
526 let tag = odb.read(¤t)?;
527 current = tag_object_line_oid(&tag.data).ok_or_else(|| {
528 Error::CorruptObject(format!("tag {current} has no valid object header"))
529 })?;
530 }
531 }
532 Ok(out)
533}
534
535const SEND_PACK_REACH_WANT: u8 = 1;
536const SEND_PACK_REACH_HAVE: u8 = 2;
537
538#[cfg(test)]
539pub(crate) mod send_pack_walk_test_hooks {
540 use std::cell::Cell;
541
542 thread_local! {
543 static COMMIT_READS: Cell<usize> = const { Cell::new(0) };
544 }
545
546 pub fn reset_commit_reads() {
547 COMMIT_READS.with(|c| c.set(0));
548 }
549
550 pub fn commit_reads() -> usize {
551 COMMIT_READS.with(|c| c.get())
552 }
553
554 pub(crate) fn record_commit_read() {
555 COMMIT_READS.with(|c| c.set(c.get().saturating_add(1)));
556 }
557}
558
559fn send_pack_reach_uninteresting(reach: u8) -> bool {
560 reach & SEND_PACK_REACH_HAVE != 0
561}
562
563fn send_pack_reach_interesting(reach: u8) -> bool {
564 reach & SEND_PACK_REACH_WANT != 0 && !send_pack_reach_uninteresting(reach)
565}
566
567fn send_pack_uninteresting_commits(
573 odb: &Odb,
574 wants: &[ObjectId],
575 haves: &[ObjectId],
576 shallow_grafts: &HashSet<ObjectId>,
577 stop: &CancelSignal<'_>,
578) -> Result<HashSet<ObjectId>> {
579 struct SendPackRevWalk<'a> {
580 odb: &'a Odb,
581 graph: Option<Arc<CommitGraphChain>>,
582 shallow_grafts: &'a HashSet<ObjectId>,
583 reach: HashMap<ObjectId, u8>,
584 heap: BinaryHeap<QueuedCommit>,
585 on_heap: HashSet<ObjectId>,
586 processed: HashSet<ObjectId>,
587 uninteresting: HashSet<ObjectId>,
588 interesting_on_heap: usize,
589 next_seq: u64,
590 }
591
592 impl<'a> SendPackRevWalk<'a> {
593 fn note_reach(&mut self, oid: ObjectId, flag: u8) {
594 let entry = self.reach.entry(oid).or_insert(0);
595 let was_interesting = send_pack_reach_interesting(*entry);
596 *entry |= flag;
597 let is_interesting = send_pack_reach_interesting(*entry);
598 if self.on_heap.contains(&oid) {
599 match (was_interesting, is_interesting) {
600 (false, true) => self.interesting_on_heap += 1,
601 (true, false) => self.interesting_on_heap -= 1,
602 _ => {}
603 }
604 }
605 if send_pack_reach_uninteresting(*entry) {
606 self.uninteresting.insert(oid);
607 }
608 }
609
610 fn can_stop_after(&self) -> bool {
615 !self.heap.is_empty() && self.interesting_on_heap == 0
618 }
619
620 fn reach_unqueued(&mut self, oid: ObjectId, flag: u8) -> bool {
622 self.note_reach(oid, flag);
623 !(self.processed.contains(&oid)
624 || self.on_heap.contains(&oid)
625 || self.shallow_grafts.contains(&oid))
626 }
627
628 fn try_enqueue(&mut self, oid: ObjectId, flag: u8) -> Result<()> {
629 if !self.reach_unqueued(oid, flag) {
630 return Ok(());
631 }
632 if self.queue_from_graph(oid) {
633 return Ok(());
634 }
635 match self.odb.read(&oid) {
636 Ok(obj) => self.queue_commit(oid, &obj),
637 Err(_) => Ok(()),
638 }
639 }
640
641 fn enqueue_tip(&mut self, tip: ObjectId, flag: u8) -> Result<()> {
646 let mut oid = tip;
647 let mut tags_seen: Option<HashSet<ObjectId>> = None;
648 loop {
649 if self.graph_lists(&oid) {
650 if self.reach_unqueued(oid, flag) {
651 self.queue_from_graph(oid);
652 }
653 return Ok(());
654 }
655 match self.odb.read_info(&oid) {
658 Ok(info) if matches!(info.kind, ObjectKind::Commit | ObjectKind::Tag) => {}
659 _ => return Ok(()),
660 }
661 let Ok(obj) = self.odb.read(&oid) else {
662 return Ok(());
663 };
664 if obj.kind != ObjectKind::Tag {
665 if obj.kind != ObjectKind::Commit || !self.reach_unqueued(oid, flag) {
666 return Ok(());
667 }
668 return self.queue_commit(oid, &obj);
669 }
670 if !tags_seen.get_or_insert_with(HashSet::new).insert(oid) {
671 return Ok(());
672 }
673 let Some(target) = tag_object_line_oid(&obj.data) else {
674 return Ok(());
675 };
676 if self.processed.contains(&target) || self.on_heap.contains(&target) {
677 self.note_reach(target, flag);
678 return Ok(());
679 }
680 oid = target;
681 }
682 }
683
684 fn graph_lists(&self, oid: &ObjectId) -> bool {
685 self.graph
686 .as_deref()
687 .is_some_and(|g| g.find_commit(oid).is_some())
688 }
689
690 fn queue_from_graph(&mut self, oid: ObjectId) -> bool {
693 let Some(entry) = self.graph.as_deref().and_then(|g| g.graph_walk_entry(&oid)) else {
694 return false;
695 };
696 self.push_queued(QueuedCommit {
697 generation: entry.generation,
698 time: entry.time,
699 seq: std::cmp::Reverse(self.next_seq),
700 oid,
701 tree: entry.tree,
702 parents: entry.parents,
703 });
704 true
705 }
706
707 fn queue_commit(&mut self, oid: ObjectId, obj: &Object) -> Result<()> {
708 if obj.kind != ObjectKind::Commit {
709 return Ok(());
710 }
711 let parsed = CommitRef::parse(&obj.data)?;
712 #[cfg(test)]
713 send_pack_walk_test_hooks::record_commit_read();
714 self.push_queued(QueuedCommit {
715 generation: UNRANKED_GENERATION,
716 time: parsed.committer_unix_seconds_for_ordering(),
717 seq: std::cmp::Reverse(self.next_seq),
718 oid,
719 tree: parsed.tree(),
720 parents: parsed.parents().to_vec(),
721 });
722 Ok(())
723 }
724
725 fn push_queued(&mut self, queued: QueuedCommit) {
726 self.on_heap.insert(queued.oid);
727 if send_pack_reach_interesting(*self.reach.get(&queued.oid).unwrap_or(&0)) {
728 self.interesting_on_heap += 1;
729 }
730 self.heap.push(queued);
731 self.next_seq += 1;
732 }
733 }
734
735 let graph = if shallow_grafts.is_empty() {
741 odb.commit_graph_chain()
742 } else {
743 None
744 };
745 let mut walk = SendPackRevWalk {
746 odb,
747 graph,
748 shallow_grafts,
749 reach: HashMap::new(),
750 heap: BinaryHeap::new(),
751 on_heap: HashSet::new(),
752 processed: HashSet::new(),
753 uninteresting: HashSet::new(),
754 interesting_on_heap: 0,
755 next_seq: 0,
756 };
757
758 for &want in wants {
759 walk.enqueue_tip(want, SEND_PACK_REACH_WANT)?;
760 }
761 for &have in haves {
762 walk.enqueue_tip(have, SEND_PACK_REACH_HAVE)?;
763 }
764
765 while let Some(commit) = walk.heap.pop() {
766 stop.poll()?;
767 walk.on_heap.remove(&commit.oid);
768 if send_pack_reach_interesting(*walk.reach.get(&commit.oid).unwrap_or(&0)) {
769 walk.interesting_on_heap -= 1;
770 }
771 if !walk.processed.insert(commit.oid) {
772 continue;
773 }
774 let flags = *walk.reach.get(&commit.oid).unwrap_or(&0);
775 if send_pack_reach_uninteresting(flags) {
776 walk.uninteresting.insert(commit.oid);
777 }
778 let parent_flag = if send_pack_reach_uninteresting(flags) {
779 SEND_PACK_REACH_HAVE
780 } else {
781 SEND_PACK_REACH_WANT
782 };
783 if !walk.shallow_grafts.contains(&commit.oid) {
784 for parent in commit.parents {
785 walk.try_enqueue(parent, parent_flag)?;
786 }
787 }
788 if walk.can_stop_after() {
789 break;
790 }
791 }
792 Ok(walk.uninteresting)
793}
794
795fn commits_reachable_from_haves(
797 odb: &Odb,
798 haves: &[ObjectId],
799 shallow_grafts: &HashSet<ObjectId>,
800 skip_missing: bool,
801 stop: &CancelSignal<'_>,
802) -> Result<HashSet<ObjectId>> {
803 let mut seen = HashSet::new();
804 let mut queue: VecDeque<ObjectId> = haves.iter().copied().collect();
805 while let Some(oid) = queue.pop_front() {
806 stop.poll()?;
807 if !seen.insert(oid) {
808 continue;
809 }
810 if shallow_grafts.contains(&oid) {
811 continue;
812 }
813 let obj = match odb.read(&oid) {
814 Ok(o) => o,
815 Err(_) if skip_missing => continue,
816 Err(e) => return Err(e),
817 };
818 if obj.kind != ObjectKind::Commit {
819 continue;
820 }
821 let parsed = CommitRef::parse(&obj.data)?;
822 for parent in parsed.parents() {
823 if !seen.contains(parent) {
824 queue.push_back(*parent);
825 }
826 }
827 }
828 Ok(seen)
829}
830
831fn objects_in_commit_snapshot(odb: &Odb, commit_oid: ObjectId) -> Result<HashSet<ObjectId>> {
833 let mut out = HashSet::new();
834 out.insert(commit_oid);
835 let commit_obj = match odb.read(&commit_oid) {
836 Ok(o) => o,
837 Err(_) => return Ok(out),
838 };
839 if commit_obj.kind != ObjectKind::Commit {
840 return Ok(out);
841 }
842 let commit = CommitRef::parse(&commit_obj.data)?;
843 let oid_len = odb.hash_algo().len();
844 let mut queue = VecDeque::from([commit.tree()]);
845 while let Some(tree_oid) = queue.pop_front() {
846 if !out.insert(tree_oid) {
847 continue;
848 }
849 let tree_obj = match odb.read(&tree_oid) {
850 Ok(o) => o,
851 Err(_) => continue,
852 };
853 for entry in TreeRefIter::with_oid_len(&tree_obj.data, oid_len) {
854 let entry = entry?;
855 if entry.mode == 0o160000 {
856 continue;
857 }
858 if entry.mode & 0o170000 == 0o040000 {
859 queue.push_back(entry.oid);
860 } else {
861 out.insert(entry.oid);
862 }
863 }
864 }
865 Ok(out)
866}
867
868fn try_peer_held_bitmap(
869 odb: &Odb,
870 haves: &[ObjectId],
871 opts: &PackEnumerateOptions<'_>,
872) -> Result<Option<Arc<ReachableSet>>> {
873 if haves.is_empty() || !opts.use_bitmaps || opts.has_shallow_grafts() {
874 return Ok(None);
875 }
876 let Some(peer) = try_bitmap_peer_held(odb, haves, opts.shallow_grafts)? else {
877 return Ok(None);
878 };
879 Ok(Some(Arc::new(peer)))
880}
881
882fn try_bitmap_peer_held(
883 odb: &Odb,
884 haves: &[ObjectId],
885 shallow_grafts: &HashSet<ObjectId>,
886) -> Result<Option<ReachableSet>> {
887 if !shallow_grafts.is_empty() {
888 return Ok(None);
889 }
890 let Some(git_dir) = odb.config_git_dir() else {
891 return Ok(None);
892 };
893 let repo = match Repository::open(git_dir, &RepositoryOptions::empty()) {
894 Ok(r) => r,
895 Err(_) => return Ok(None),
896 };
897 let Some(index) = BitmapIndex::open(&repo).ok().flatten() else {
898 return Ok(None);
899 };
900 let index = Arc::new(index);
901 match index.reachability(
902 &repo,
903 ReachabilityQuery {
904 wants: haves,
905 haves: &[],
906 filter: None,
907 },
908 MissingAction::Allow,
909 ) {
910 Ok(set) => Ok(Some(set)),
911 Err(BitmapWalkError::Unsupported(_)) => Ok(None),
912 Err(BitmapWalkError::Bitmap(_)) => Ok(None),
913 Err(BitmapWalkError::Repository(e)) => Err(e),
914 }
915}
916
917fn rev_list_filtered_objects(
919 odb: &Odb,
920 wants: &[ObjectId],
921 haves: &[ObjectId],
922 filter: &ObjectFilter,
923 have_commits: &HashSet<ObjectId>,
924 missing_mode: MissingBlobMode,
925 stop: &CancelSignal<'_>,
926) -> Result<(Vec<ObjectId>, HashMap<ObjectId, u32>)> {
927 let _ = have_commits;
928 stop.poll()?;
929 let Some(git_dir) = odb.config_git_dir() else {
930 return Ok((Vec::new(), HashMap::new()));
931 };
932 let repo = Repository::open(git_dir, &RepositoryOptions::empty())?;
933 let wants: Vec<String> = wants.iter().map(ObjectId::to_hex).collect();
934 let haves: Vec<String> = haves.iter().map(ObjectId::to_hex).collect();
935 let opts = crate::rev_list::RevListOptions {
936 objects: true,
937 no_object_names: false,
938 quiet: true,
939 filter: Some(filter.clone()),
940 cancel_watch: Some(stop.watch_arc()),
941 missing_action: match missing_mode {
942 MissingBlobMode::Fail => MissingAction::Error,
943 MissingBlobMode::Probe => MissingAction::Allow,
944 },
945 ..Default::default()
946 };
947 let result = crate::rev_list::rev_list(&repo, &wants, &haves, &opts)?;
948 stop.poll()?;
949 let mut ordered = Vec::new();
950 let mut seen = HashSet::new();
951 for oid in result
952 .commits
953 .iter()
954 .chain(result.objects.iter().map(|(o, _)| o))
955 {
956 if seen.insert(*oid) {
957 ordered.push(*oid);
958 }
959 }
960 let mut name_hashes = HashMap::new();
961 for (oid, path) in &result.objects {
962 if !path.is_empty() {
963 name_hashes
964 .entry(*oid)
965 .or_insert_with(|| pack_name_hash(path));
966 }
967 }
968 Ok((ordered, name_hashes))
969}
970
971#[allow(clippy::too_many_arguments)]
992fn collect_reachable_excluding_with_name_hashes(
993 odb: &Odb,
994 roots: &[ObjectId],
995 exclude: &HashSet<ObjectId>,
996 boundary: &mut HashSet<ObjectId>,
997 have_commits: &HashSet<ObjectId>,
998 peer_odb: Option<&Odb>,
999 alternate_sources: Option<Arc<CompositeStore>>,
1000 skip_missing: bool,
1001 shallow_grafts: &HashSet<ObjectId>,
1002 filter: Option<&ObjectFilter>,
1003 name_trees: NameTrees,
1004 missing_mode: MissingBlobMode,
1005 stop: &CancelSignal<'_>,
1006) -> Result<WalkedObjects> {
1007 let oid_len = odb.hash_algo().len();
1008 let mut walk = EnumerationWalk {
1009 odb,
1010 exclude,
1011 boundary,
1012 have_commits,
1013 peer_odb,
1014 alternate_sources,
1015 skip_missing,
1016 filter,
1017 depth_sensitive: filter.is_some_and(ObjectFilter::is_depth_sensitive),
1018 name_trees,
1019 visited: oid_hash_set(),
1020 tree_depths: oid_hash_map(),
1021 unlisted: oid_hash_set(),
1022 ordered: Vec::new(),
1023 commits: BinaryHeap::new(),
1024 next_seq: 0,
1025 queue: VecDeque::new(),
1026 name_hashes: HashMap::new(),
1027 edges: HashSet::new(),
1028 missing: (missing_mode == MissingBlobMode::Probe).then(Vec::new),
1029 };
1030 let mut dropped_root_trees: Option<OidHashSet> = filter
1032 .filter(|f| !f.includes_tree(0))
1033 .map(|_| oid_hash_set());
1034
1035 let mut path_less_roots = Vec::new();
1036 for &root in roots {
1037 match odb.read_info(&root) {
1038 Ok(info) if matches!(info.kind, ObjectKind::Tree | ObjectKind::Blob) => {
1039 path_less_roots.push(root);
1040 }
1041 _ => walk.push_root(root)?,
1042 }
1043 }
1044 let mut path_less_roots = path_less_roots.into_iter();
1045
1046 loop {
1047 stop.poll()?;
1048 if let Some(commit) = walk.commits.pop() {
1049 if !shallow_grafts.contains(&commit.oid) {
1050 for parent in commit.parents {
1051 walk.push_parent(parent)?;
1052 }
1053 }
1054 if let Some(dropped) = dropped_root_trees.as_mut() {
1055 dropped.insert(commit.tree);
1056 }
1057 walk.push_tree(commit.tree, String::new(), 0);
1058 continue;
1059 }
1060 let Some((oid, path, depth)) = walk.queue.pop_front() else {
1061 match path_less_roots.next() {
1062 Some(root) => {
1063 walk.push_object(root, String::new());
1064 continue;
1065 }
1066 None => break,
1067 }
1068 };
1069 match odb.read_info(&oid) {
1072 Ok(info) if info.kind == ObjectKind::Blob => continue,
1073 Ok(_) => {}
1074 Err(_) if skip_missing => continue,
1075 Err(e) => return Err(e),
1076 }
1077 let obj = match odb.read(&oid) {
1078 Ok(o) => o,
1079 Err(_) if skip_missing => continue,
1080 Err(e) => return Err(e),
1081 };
1082 match obj.kind {
1083 ObjectKind::Commit => {
1084 walk.queue_commit(oid, obj)?;
1085 continue;
1086 }
1087 ObjectKind::Tree => {
1088 for entry in TreeRefIter::with_oid_len(&obj.data, oid_len) {
1089 let entry = entry?;
1090 if entry.mode == 0o160000 {
1091 continue;
1092 }
1093 match entry.mode & 0o170000 {
1100 0o120000 => walk.push_blob(entry.oid, None, oid, depth)?,
1101 0o100000 => {
1102 walk.push_blob(entry.oid, Some((&path, entry.name)), oid, depth)?;
1103 }
1104 file_type => {
1105 let child_path = child_path(&path, entry.name);
1106 if file_type == 0o040000 {
1107 walk.push_tree(entry.oid, child_path, depth + 1);
1108 } else {
1109 walk.push_object(entry.oid, child_path);
1110 }
1111 }
1112 }
1113 }
1114 }
1115 ObjectKind::Tag => {
1116 let tag = parse_tag(&obj.data)?;
1117 walk.push_object(tag.object, path);
1118 }
1119 ObjectKind::Blob => {}
1120 }
1121 }
1122
1123 if let Some(dropped) = dropped_root_trees.filter(|d| !d.is_empty()) {
1124 walk.ordered.retain(|oid| !dropped.contains(oid));
1125 }
1126 Ok(WalkedObjects {
1127 send: walk.ordered,
1128 name_hashes: walk.name_hashes,
1129 edges: walk.edges,
1130 missing: walk.missing.unwrap_or_default(),
1131 })
1132}
1133
1134struct WalkedObjects {
1136 send: Vec<ObjectId>,
1138 name_hashes: HashMap<ObjectId, u32>,
1140 edges: HashSet<ObjectId>,
1142 missing: Vec<MissingBlob>,
1144}
1145
1146fn child_path(parent: &str, name: &[u8]) -> String {
1147 let name = String::from_utf8_lossy(name);
1148 if parent.is_empty() {
1149 name.into_owned()
1150 } else {
1151 format!("{parent}/{name}")
1152 }
1153}
1154
1155struct EnumerationWalk<'a> {
1157 odb: &'a Odb,
1158 exclude: &'a HashSet<ObjectId>,
1159 boundary: &'a mut HashSet<ObjectId>,
1161 have_commits: &'a HashSet<ObjectId>,
1163 peer_odb: Option<&'a Odb>,
1165 alternate_sources: Option<Arc<CompositeStore>>,
1167 skip_missing: bool,
1168 filter: Option<&'a ObjectFilter>,
1169 depth_sensitive: bool,
1171 name_trees: NameTrees,
1172 visited: OidHashSet,
1176 tree_depths: OidHashMap<u64>,
1178 unlisted: OidHashSet,
1181 ordered: Vec<ObjectId>,
1182 commits: BinaryHeap<QueuedCommit>,
1184 next_seq: u64,
1185 queue: VecDeque<(ObjectId, String, u64)>,
1188 name_hashes: HashMap<ObjectId, u32>,
1189 edges: HashSet<ObjectId>,
1191 missing: Option<Vec<MissingBlob>>,
1193}
1194
1195struct QueuedCommit {
1198 generation: u64,
1200 time: i64,
1201 seq: std::cmp::Reverse<u64>,
1202 oid: ObjectId,
1203 tree: ObjectId,
1204 parents: Vec<ObjectId>,
1205}
1206
1207impl PartialEq for QueuedCommit {
1208 fn eq(&self, other: &Self) -> bool {
1209 (self.generation, self.time, self.seq) == (other.generation, other.time, other.seq)
1210 }
1211}
1212
1213impl Eq for QueuedCommit {}
1214
1215impl PartialOrd for QueuedCommit {
1216 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1217 Some(self.cmp(other))
1218 }
1219}
1220
1221impl Ord for QueuedCommit {
1222 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1223 (self.generation, self.time, self.seq).cmp(&(other.generation, other.time, other.seq))
1224 }
1225}
1226
1227impl<'a> EnumerationWalk<'a> {
1228 fn list(&mut self, oid: ObjectId) {
1229 if !self.exclude.contains(&oid) {
1230 self.ordered.push(oid);
1231 }
1232 }
1233
1234 fn note_boundary(&mut self, oid: ObjectId) {
1235 self.visited.insert(oid);
1236 }
1237
1238 fn at_object_boundary(&self, oid: ObjectId) -> bool {
1239 if self
1240 .alternate_sources
1241 .as_ref()
1242 .is_some_and(|s| s.contains(&oid).unwrap_or(false))
1243 {
1244 return true;
1245 }
1246 self.boundary.contains(&oid)
1247 || self
1248 .peer_odb
1249 .is_some_and(|peer| peer.exists_materialized_local_without_rescan(&oid))
1250 }
1251
1252 fn seal_have_reachable_commit(&mut self, oid: ObjectId) -> Result<()> {
1253 if !self.visited.insert(oid) {
1254 return Ok(());
1255 }
1256 self.edges.insert(oid);
1257 self.boundary
1258 .extend(objects_in_commit_snapshot(self.odb, oid)?);
1259 Ok(())
1260 }
1261
1262 fn commit_on_have_side(&mut self, oid: ObjectId) -> Result<bool> {
1263 Ok(self.have_commits.contains(&oid))
1264 }
1265
1266 fn push_object(&mut self, oid: ObjectId, path: String) {
1268 if let Ok(true) = self.commit_on_have_side(oid) {
1269 let _ = self.seal_have_reachable_commit(oid);
1270 return;
1271 }
1272 if self.at_object_boundary(oid) {
1273 self.note_boundary(oid);
1274 return;
1275 }
1276 if self.visited.insert(oid) {
1277 self.list(oid);
1278 self.queue.push_back((oid, path, 0));
1279 } else if self.unlisted.remove(&oid) {
1280 self.list(oid);
1281 }
1282 }
1283
1284 fn push_root(&mut self, oid: ObjectId) -> Result<()> {
1287 if self.commit_on_have_side(oid)? {
1288 return self.seal_have_reachable_commit(oid);
1289 }
1290 if self.at_object_boundary(oid) {
1291 self.note_boundary(oid);
1292 return Ok(());
1293 }
1294 if !self.visited.insert(oid) {
1295 if self.unlisted.remove(&oid) {
1296 self.list(oid);
1297 }
1298 return Ok(());
1299 }
1300 self.list(oid);
1301 match self.odb.read_info(&oid) {
1304 Ok(info) if info.kind != ObjectKind::Commit => {
1305 self.queue.push_back((oid, String::new(), 0));
1306 return Ok(());
1307 }
1308 Ok(_) => {}
1309 Err(_) if self.skip_missing => return Ok(()),
1310 Err(e) => return Err(e),
1311 }
1312 match self.read(&oid)? {
1313 Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1314 Some(_) => {
1315 self.queue.push_back((oid, String::new(), 0));
1316 Ok(())
1317 }
1318 None => Ok(()),
1319 }
1320 }
1321
1322 fn push_parent(&mut self, oid: ObjectId) -> Result<()> {
1325 if self.commit_on_have_side(oid)? {
1326 return self.seal_have_reachable_commit(oid);
1327 }
1328 if self.at_object_boundary(oid) {
1329 self.note_boundary(oid);
1330 return Ok(());
1331 }
1332 let listed = self
1333 .filter
1334 .is_none_or(|f| f.includes_commit_or_tag_object(ObjectKind::Commit));
1335 if !self.visited.insert(oid) {
1336 if listed && self.unlisted.remove(&oid) {
1337 self.list(oid);
1338 }
1339 return Ok(());
1340 }
1341 if listed {
1342 self.list(oid);
1343 } else {
1344 self.unlisted.insert(oid);
1345 }
1346 match self.read(&oid)? {
1347 Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1348 Some(_) => {
1349 self.queue.push_back((oid, String::new(), 0));
1350 Ok(())
1351 }
1352 None => Ok(()),
1353 }
1354 }
1355
1356 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
1357 match self.odb.read(oid) {
1358 Ok(obj) => Ok(Some(obj)),
1359 Err(_) if self.skip_missing => Ok(None),
1360 Err(e) => Err(e),
1361 }
1362 }
1363
1364 fn queue_commit(&mut self, oid: ObjectId, obj: Object) -> Result<()> {
1366 let parsed = CommitRef::parse(&obj.data)?;
1367 let queued = QueuedCommit {
1368 generation: UNRANKED_GENERATION,
1369 time: parsed.committer_unix_seconds_for_ordering(),
1370 seq: std::cmp::Reverse(self.next_seq),
1371 oid,
1372 tree: parsed.tree(),
1373 parents: parsed.parents().to_vec(),
1374 };
1375 self.next_seq += 1;
1376 self.commits.push(queued);
1377 Ok(())
1378 }
1379
1380 fn push_tree(&mut self, oid: ObjectId, path: String, depth: u64) {
1382 if self.name_trees == NameTrees::Yes && !path.is_empty() && !self.visited.contains(&oid) {
1383 self.name_hashes
1384 .entry(oid)
1385 .or_insert_with(|| pack_name_hash(&path));
1386 }
1387 let Some(filter) = self.filter else {
1388 self.push_object(oid, path);
1389 return;
1390 };
1391 if self.at_object_boundary(oid) {
1393 self.note_boundary(oid);
1394 return;
1395 }
1396 let first_walk = if self.depth_sensitive {
1397 match self.tree_depths.get(&oid) {
1398 Some(&walked_at) if walked_at <= depth => false,
1399 _ => {
1400 self.tree_depths.insert(oid, depth);
1401 true
1402 }
1403 }
1404 } else {
1405 self.visited.insert(oid)
1406 };
1407 if !first_walk {
1408 return;
1409 }
1410 if filter.includes_tree(depth) {
1411 if !self.depth_sensitive || self.visited.insert(oid) {
1412 self.list(oid);
1413 }
1414 } else if !self.depth_sensitive {
1415 self.unlisted.insert(oid);
1416 }
1417 if !filter.prunes_tree_entries(depth) {
1418 self.queue.push_back((oid, path, depth));
1419 }
1420 }
1421
1422 fn push_blob(
1425 &mut self,
1426 oid: ObjectId,
1427 name: Option<(&str, &[u8])>,
1428 referrer: ObjectId,
1429 tree_depth: u64,
1430 ) -> Result<()> {
1431 if self.at_object_boundary(oid) {
1432 self.note_boundary(oid);
1433 return Ok(());
1434 }
1435 if self.visited.contains(&oid) {
1436 return Ok(());
1437 }
1438 let (passes, size_checked) = match self.filter {
1439 None => (true, false),
1440 Some(filter) => self
1443 .blob_passes(filter, &oid, tree_depth)?
1444 .unwrap_or((true, true)),
1445 };
1446 if passes && self.missing.is_some() && !self.odb.contains(&oid)? {
1447 self.visited.insert(oid);
1448 if let Some(missing) = self.missing.as_mut() {
1449 missing.push(MissingBlob {
1450 oid,
1451 referrer: Some(referrer),
1452 });
1453 }
1454 return Ok(());
1455 }
1456 if passes {
1457 self.visited.insert(oid);
1458 self.list(oid);
1459 if let Some((parent, entry_name)) = name {
1460 self.name_hashes
1461 .insert(oid, pack_name_hash(&child_path(parent, entry_name)));
1462 }
1463 } else if size_checked && !self.depth_sensitive {
1464 self.visited.insert(oid);
1465 self.unlisted.insert(oid);
1466 }
1467 Ok(())
1468 }
1469
1470 fn blob_passes(
1477 &self,
1478 filter: &ObjectFilter,
1479 oid: &ObjectId,
1480 tree_depth: u64,
1481 ) -> Result<Option<(bool, bool)>> {
1482 if !filter.includes_blob_under_tree(0, tree_depth) {
1484 return Ok(Some((false, false)));
1485 }
1486 if filter.includes_blob_under_tree(u64::MAX, tree_depth) {
1487 return Ok(Some((true, false)));
1488 }
1489 let info = if self.missing.is_some() {
1490 match self.odb.read_info_without_fetch(oid) {
1491 Err(crate::error::Error::ObjectNotFound(_)) => return Ok(None),
1492 other => other,
1493 }
1494 } else {
1495 self.odb.read_info(oid)
1496 };
1497 match info {
1498 Ok(info) => Ok(Some((
1499 filter.includes_blob_under_tree(info.size, tree_depth),
1500 true,
1501 ))),
1502 Err(_) if self.skip_missing => Ok(Some((false, true))),
1503 Err(e) => Err(e),
1504 }
1505 }
1506}
1507
1508#[cfg(test)]
1509mod tests {
1510 #![allow(clippy::unwrap_used, clippy::expect_used)]
1511
1512 use super::*;
1513
1514 use crate::repo::{InitOptions, Repository};
1515 use tempfile::TempDir;
1516
1517 fn walk(repo: &Repository, roots: &[ObjectId], name_trees: NameTrees) -> PackObjectSelection {
1518 let empty = HashSet::new();
1519 enumerate_pack_objects_naming(
1520 repo.odb(),
1521 roots,
1522 &[],
1523 &PackEnumerateOptions {
1524 filter: None,
1525 shallow_grafts: &empty,
1526 have_shallow_grafts: Some(&empty),
1527 use_bitmaps: false,
1528 exclude_objects: None,
1529 peer_odb: None,
1530 have_boundary: HaveBoundary::CommitClosure,
1531 },
1532 name_trees,
1533 false,
1534 &CancelSignal::never(),
1535 )
1536 .expect("walk")
1537 }
1538
1539 fn write_tree(repo: &Repository, entries: &[(u32, &str, ObjectId)]) -> ObjectId {
1540 let entries: Vec<crate::objects::TreeEntry> = entries
1541 .iter()
1542 .map(|&(mode, name, oid)| crate::objects::TreeEntry {
1543 mode,
1544 name: name.as_bytes().to_vec(),
1545 oid,
1546 })
1547 .collect();
1548 repo.odb()
1549 .write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
1550 .expect("tree")
1551 }
1552
1553 fn write_commit(
1554 repo: &Repository,
1555 tree: ObjectId,
1556 parents: &[ObjectId],
1557 time: i64,
1558 ) -> ObjectId {
1559 let mut body = format!("tree {}\n", tree.to_hex());
1560 for parent in parents {
1561 body.push_str(&format!("parent {}\n", parent.to_hex()));
1562 }
1563 body.push_str(&format!(
1564 "author A <a@example.com> {time} +0000\ncommitter A <a@example.com> {time} +0000\n\nc{time}\n"
1565 ));
1566 repo.odb()
1567 .write(ObjectKind::Commit, body.as_bytes())
1568 .expect("commit")
1569 }
1570
1571 #[test]
1572 fn tips_only_boundary_matches_closure_on_linear_incremental_push() {
1573 let dir = TempDir::new().unwrap();
1574 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1575 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1576 let mut parent = write_commit(
1577 &repo,
1578 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1579 &[],
1580 0,
1581 );
1582 for i in 1..800 {
1583 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1584 parent = write_commit(&repo, tree, &[parent], i);
1585 }
1586 let remote_tip = parent;
1587 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1588 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1589 let empty = HashSet::new();
1590 let closure_opts = PackEnumerateOptions {
1591 filter: None,
1592 shallow_grafts: &empty,
1593 have_shallow_grafts: Some(&empty),
1594 use_bitmaps: false,
1595 exclude_objects: None,
1596 peer_odb: None,
1597 have_boundary: HaveBoundary::CommitClosure,
1598 };
1599 let tips_opts = PackEnumerateOptions {
1600 have_boundary: HaveBoundary::TipsOnly,
1601 ..closure_opts
1602 };
1603 let closure = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &closure_opts)
1604 .expect("closure");
1605 let tips = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &tips_opts)
1606 .expect("tips");
1607 let closure_set: HashSet<ObjectId> = closure.send.iter().copied().collect();
1608 let tips_set: HashSet<ObjectId> = tips.send.iter().copied().collect();
1609 assert_eq!(closure_set, tips_set);
1610 assert!(
1611 tips.send.len() <= 8,
1612 "one new commit should enumerate a handful of objects, got {}",
1613 tips.send.len()
1614 );
1615 }
1616
1617 #[test]
1618 fn tips_only_send_pack_does_not_prewalk_deep_history() {
1619 use crate::merge_base::intersect_walk_test_hooks::{
1620 commit_parent_lookups, reset_commit_parent_lookups,
1621 };
1622
1623 let dir = TempDir::new().unwrap();
1624 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1625 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1626 let mut parent = write_commit(
1627 &repo,
1628 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1629 &[],
1630 0,
1631 );
1632 for i in 1..800 {
1633 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1634 parent = write_commit(&repo, tree, &[parent], i);
1635 }
1636 let remote_tip = parent;
1637 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1638 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1639 let haves = vec![remote_tip];
1640 let empty = HashSet::new();
1641 reset_commit_parent_lookups();
1642 let _selection = enumerate_pack_objects(
1643 repo.odb(),
1644 &[new_tip],
1645 &haves,
1646 &PackEnumerateOptions {
1647 filter: None,
1648 shallow_grafts: &empty,
1649 have_shallow_grafts: Some(&empty),
1650 use_bitmaps: false,
1651 exclude_objects: None,
1652 peer_odb: None,
1653 have_boundary: HaveBoundary::TipsOnly,
1654 },
1655 )
1656 .expect("enumerate");
1657 assert!(
1658 commit_parent_lookups() <= 128,
1659 "send-pack hide must stay coupled to the want walk ({} parent lookups)",
1660 commit_parent_lookups()
1661 );
1662 }
1663
1664 #[test]
1665 fn push_dated_before_graphed_history_stops_at_the_frontier() {
1666 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1667 let dir = TempDir::new().unwrap();
1668 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1669 let tree = write_tree(&repo, &[]);
1670 let mut tip = write_commit(&repo, tree, &[], 1_300_000_000);
1672 let mut tagged = tip;
1673 for i in 1..300 {
1674 tip = write_commit(&repo, tree, &[tip], 1_300_000_000 + i);
1675 if i == 150 {
1676 tagged = tip;
1677 }
1678 }
1679 std::fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n")).unwrap();
1680 repo.odb().write_commit_graph().unwrap();
1681 let mut new_tip = tip;
1684 for i in 0..3 {
1685 new_tip = write_commit(&repo, tree, &[new_tip], 1_000_000_000 + i);
1686 }
1687
1688 reset_commit_reads();
1689 let hidden = send_pack_uninteresting_commits(
1690 repo.odb(),
1691 &[new_tip],
1692 &[tip, tagged],
1693 &HashSet::new(),
1694 &CancelSignal::never(),
1695 )
1696 .unwrap();
1697 assert!(hidden.contains(&tip));
1698 assert!(hidden.contains(&tagged));
1699 assert!(!hidden.contains(&new_tip));
1700 assert_eq!(
1701 commit_reads(),
1702 3,
1703 "only the commits outside the graph are read from the object store"
1704 );
1705 assert!(
1706 hidden.len() <= 4,
1707 "the walk must stop at the frontier, not drain the remote's history ({} marked)",
1708 hidden.len()
1709 );
1710 }
1711
1712 #[test]
1713 fn duplicated_wants_already_on_peer_balance_the_frontier() {
1714 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1715 let dir = TempDir::new().unwrap();
1716 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1717 let tree = write_tree(&repo, &[]);
1718 let mut tip = write_commit(&repo, tree, &[], 0);
1719 for time in 1..100 {
1720 tip = write_commit(&repo, tree, &[tip], time);
1721 }
1722 reset_commit_reads();
1723 let hidden = send_pack_uninteresting_commits(
1724 repo.odb(),
1725 &[tip, tip],
1726 &[tip, tip],
1727 &HashSet::new(),
1728 &CancelSignal::never(),
1729 )
1730 .unwrap();
1731 assert!(hidden.contains(&tip));
1732 assert!(
1733 commit_reads() <= 2,
1734 "shared want/have must stop at the frontier"
1735 );
1736 let empty = HashSet::new();
1737 let selection = enumerate_pack_objects(
1738 repo.odb(),
1739 &[tip, tip],
1740 &[tip, tip],
1741 &PackEnumerateOptions {
1742 filter: None,
1743 shallow_grafts: &empty,
1744 have_shallow_grafts: None,
1745 use_bitmaps: false,
1746 exclude_objects: None,
1747 peer_odb: None,
1748 have_boundary: HaveBoundary::TipsOnly,
1749 },
1750 )
1751 .unwrap();
1752 assert!(selection.send.is_empty());
1753 }
1754
1755 #[test]
1756 fn tips_only_many_advertised_haves_does_not_prewalk_deep_history() {
1757 use crate::merge_base::intersect_walk_test_hooks::{
1758 commit_parent_lookups, reset_commit_parent_lookups,
1759 };
1760
1761 let dir = TempDir::new().unwrap();
1762 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1763 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1764 let mut chain = Vec::new();
1765 let mut parent = write_commit(
1766 &repo,
1767 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1768 &[],
1769 0,
1770 );
1771 chain.push(parent);
1772 for i in 1..800 {
1773 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1774 parent = write_commit(&repo, tree, &[parent], i);
1775 chain.push(parent);
1776 }
1777 let remote_tip = parent;
1778 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1779 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1780 let mut haves = vec![remote_tip];
1781 for n in (2..400).step_by(2) {
1782 let idx = chain.len().checked_sub(1 + n);
1783 if let Some(idx) = idx {
1784 haves.push(chain[idx]);
1785 }
1786 }
1787 let empty = HashSet::new();
1788 reset_commit_parent_lookups();
1789 let selection = enumerate_pack_objects(
1790 repo.odb(),
1791 &[new_tip],
1792 &haves,
1793 &PackEnumerateOptions {
1794 filter: None,
1795 shallow_grafts: &empty,
1796 have_shallow_grafts: Some(&empty),
1797 use_bitmaps: false,
1798 exclude_objects: None,
1799 peer_odb: None,
1800 have_boundary: HaveBoundary::TipsOnly,
1801 },
1802 )
1803 .expect("enumerate");
1804 assert!(
1805 selection.send.len() <= 8,
1806 "one new commit should stay small with many haves, got {}",
1807 selection.send.len()
1808 );
1809 assert!(
1810 commit_parent_lookups() <= 512,
1811 "many advertised haves must not pre-walk deep history ({} parent lookups)",
1812 commit_parent_lookups()
1813 );
1814 }
1815
1816 #[test]
1817 fn send_pack_have_walk_stops_without_scanning_deep_haves() {
1818 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1819
1820 let dir = TempDir::new().unwrap();
1821 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1822 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1823 let mut parent = write_commit(
1824 &repo,
1825 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1826 &[],
1827 0,
1828 );
1829 let near_root = parent;
1830 for i in 1..800 {
1831 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1832 parent = write_commit(&repo, tree, &[parent], i);
1833 }
1834 let direct_parent = parent;
1835 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1836 let new_tip = write_commit(&repo, new_tree, &[direct_parent], 900);
1837 let empty = HashSet::new();
1838 reset_commit_reads();
1839 let have_commits = send_pack_uninteresting_commits(
1840 repo.odb(),
1841 &[new_tip],
1842 &[direct_parent, near_root],
1843 &empty,
1844 &CancelSignal::never(),
1845 )
1846 .expect("send-pack have walk");
1847 assert!(have_commits.contains(&direct_parent));
1848 assert!(have_commits.contains(&near_root));
1849 let reads = commit_reads();
1850 assert!(
1851 reads < 20,
1852 "send-pack have walk should not scan deep history, read {reads} commits"
1853 );
1854 }
1855
1856 #[test]
1857 fn walk_lists_trees_of_newer_commits_first() {
1858 let dir = TempDir::new().unwrap();
1859 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1860 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1861 let tree_a = write_tree(&repo, &[(0o100644, "f", blob("a"))]);
1862 let tree_b = write_tree(&repo, &[(0o100644, "f", blob("b"))]);
1863 let tree_c = write_tree(&repo, &[(0o100644, "f", blob("c"))]);
1864 let base = write_commit(&repo, tree_a, &[], 100);
1865 let old_tip = write_commit(&repo, tree_b, &[base], 200);
1867 let new_tip = write_commit(&repo, tree_c, &[base], 300);
1868
1869 let selection = walk(&repo, &[old_tip, new_tip], NameTrees::No);
1870 let pos = |oid: ObjectId| selection.send.iter().position(|o| *o == oid).unwrap();
1871 assert!(pos(old_tip) < pos(base) && pos(new_tip) < pos(base));
1872 assert!(pos(tree_c) < pos(tree_b) && pos(tree_b) < pos(tree_a));
1873 }
1874
1875 #[test]
1876 fn walk_names_trees_and_keeps_history_paths_for_path_less_roots() {
1877 let dir = TempDir::new().unwrap();
1878 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1879 let file = repo.odb().write(ObjectKind::Blob, b"x\n").unwrap();
1880 let link = repo.odb().write(ObjectKind::Blob, b"target").unwrap();
1881 let sub = write_tree(
1882 &repo,
1883 &[(0o100644, "file.rs", file), (0o120000, "link", link)],
1884 );
1885 let root = write_tree(&repo, &[(0o040000, "src", sub)]);
1886 let commit = write_commit(&repo, root, &[], 100);
1887
1888 let selection = walk(&repo, &[sub, file, commit], NameTrees::Yes);
1890 let hash = |oid: ObjectId| selection.name_hashes.get(&oid).copied();
1891 assert_eq!(hash(sub), Some(pack_name_hash("src")));
1892 assert_eq!(hash(file), Some(pack_name_hash("src/file.rs")));
1893 assert_eq!(hash(root), None, "root trees have no path");
1894 assert_eq!(selection.send.len(), 5);
1895
1896 let blobs_only = walk(&repo, &[sub, file, commit], NameTrees::No);
1897 assert_eq!(blobs_only.name_hashes.get(&sub), None);
1898 assert_eq!(
1899 blobs_only.name_hashes.get(&file),
1900 Some(&pack_name_hash("src/file.rs"))
1901 );
1902 }
1903
1904 fn tree_entry(mode: &str, name: &str, oid: ObjectId) -> Vec<u8> {
1905 let mut bytes = format!("{mode} {name}\0").into_bytes();
1906 bytes.extend_from_slice(oid.as_bytes());
1907 bytes
1908 }
1909
1910 #[cfg_attr(
1914 not(feature = "full-tests"),
1915 ignore = "starts system git or is heavy; run with --features full-tests"
1916 )]
1917 #[test]
1918 fn walk_descends_into_trees_with_non_canonical_modes() {
1919 let tmp = tempfile::tempdir().unwrap();
1920 Repository::init(tmp.path(), &InitOptions::new().with_bare(true)).unwrap();
1921 let repo = Repository::open(tmp.path(), &RepositoryOptions::empty()).unwrap();
1922 let odb = repo.odb();
1923 let deep = odb
1924 .write_loose_materialize(ObjectKind::Blob, b"deep\n")
1925 .unwrap();
1926 let odd = odb
1927 .write_loose_materialize(ObjectKind::Blob, b"odd\n")
1928 .unwrap();
1929 let inner = odb
1930 .write_loose_materialize(ObjectKind::Tree, &tree_entry("100644", "deep.txt", deep))
1931 .unwrap();
1932 let mut root = tree_entry("100664", "odd.txt", odd);
1933 root.extend(tree_entry("40755", "sub", inner));
1934 let root = odb
1935 .write_loose_materialize(ObjectKind::Tree, &root)
1936 .unwrap();
1937 let commit = format!(
1938 "tree {root}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nodd modes\n"
1939 );
1940 let commit = odb
1941 .write_loose_materialize(ObjectKind::Commit, commit.as_bytes())
1942 .unwrap();
1943
1944 let empty = HashSet::new();
1945 let selection = enumerate_pack_objects(
1946 odb,
1947 &[commit],
1948 &[],
1949 &PackEnumerateOptions {
1950 filter: None,
1951 shallow_grafts: &empty,
1952 have_shallow_grafts: Some(&empty),
1953 use_bitmaps: false,
1954 exclude_objects: None,
1955 peer_odb: None,
1956 have_boundary: HaveBoundary::CommitClosure,
1957 },
1958 )
1959 .unwrap();
1960
1961 let ours: HashSet<ObjectId> = selection.send.iter().copied().collect();
1962 let git: HashSet<ObjectId> =
1963 grit_test_support::git(tmp.path(), &["rev-list", "--objects", &commit.to_hex()])
1964 .lines()
1965 .map(|line| ObjectId::from_hex(&line[..40]).unwrap())
1966 .collect();
1967 assert_eq!(ours, git, "same objects as git rev-list --objects");
1968 assert!(ours.contains(&inner) && ours.contains(&deep));
1969 assert_eq!(selection.send.len(), 5);
1970 assert_eq!(
1971 selection.name_hashes.get(&deep),
1972 Some(&pack_name_hash("sub/deep.txt"))
1973 );
1974 assert_eq!(
1975 selection.name_hashes.get(&odd),
1976 Some(&pack_name_hash("odd.txt"))
1977 );
1978 }
1979
1980 fn tips_only_options(empty: &HashSet<ObjectId>) -> PackEnumerateOptions<'_> {
1981 PackEnumerateOptions {
1982 filter: None,
1983 shallow_grafts: empty,
1984 have_shallow_grafts: Some(empty),
1985 use_bitmaps: false,
1986 exclude_objects: None,
1987 peer_odb: None,
1988 have_boundary: HaveBoundary::TipsOnly,
1989 }
1990 }
1991
1992 fn write_tag(repo: &Repository, target: ObjectId, kind: &str, name: &str) -> ObjectId {
1993 let body = format!(
1994 "object {}\ntype {kind}\ntag {name}\ntagger A <a@example.com> 0 +0000\n\n{name}\n",
1995 target.to_hex()
1996 );
1997 repo.odb().write(ObjectKind::Tag, body.as_bytes()).unwrap()
1998 }
1999
2000 #[test]
2001 fn tips_only_thin_bases_come_from_edge_commits_not_every_advertised_tip() {
2002 let dir = TempDir::new().unwrap();
2003 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2004 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2005 let base_tree = write_tree(&repo, &[(0o100644, "f", blob("base"))]);
2006 let root = write_commit(&repo, base_tree, &[], 0);
2007 let main = write_commit(
2008 &repo,
2009 write_tree(&repo, &[(0o100644, "f", blob("main"))]),
2010 &[root],
2011 10,
2012 );
2013 let mut haves = vec![main];
2014 for i in 0..40 {
2015 let tree = write_tree(&repo, &[(0o100644, "f", blob(&format!("side {i}")))]);
2016 let side = write_commit(&repo, tree, &[root], 20 + i);
2017 haves.push(write_tag(&repo, side, "commit", &format!("t{i}")));
2018 haves.push(side);
2019 }
2020 let new_tip = write_commit(
2021 &repo,
2022 write_tree(&repo, &[(0o100644, "f", blob("main\nmore"))]),
2023 &[main],
2024 100,
2025 );
2026 let empty = HashSet::new();
2027 let selection =
2028 enumerate_pack_objects(repo.odb(), &[new_tip], &haves, &tips_only_options(&empty))
2029 .unwrap();
2030 assert_eq!(selection.send.len(), 3, "commit, tree and the changed blob");
2031 assert_eq!(
2032 selection.have_closure,
2033 HashSet::from([main]),
2034 "thin bases come from the commit the walk stopped at"
2035 );
2036
2037 let closure = enumerate_pack_objects(
2038 repo.odb(),
2039 &[new_tip],
2040 &haves,
2041 &PackEnumerateOptions {
2042 have_boundary: HaveBoundary::CommitClosure,
2043 ..tips_only_options(&empty)
2044 },
2045 )
2046 .unwrap();
2047 assert_eq!(closure.send, selection.send);
2048 assert_eq!(
2049 closure.have_closure,
2050 HashSet::from([main]),
2051 "the full have pre-walk seeds thin bases from the same edge commit"
2052 );
2053 }
2054
2055 #[test]
2056 fn send_pack_walk_peels_tag_haves_and_ignores_non_commit_targets() {
2057 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2058 let dir = TempDir::new().unwrap();
2059 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2060 let blob = repo.odb().write(ObjectKind::Blob, b"x").unwrap();
2061 let tree = write_tree(&repo, &[(0o100644, "f", blob)]);
2062 let old = write_commit(&repo, tree, &[], 0);
2063 let tip = write_commit(&repo, tree, &[old], 1);
2064 let tag = write_tag(&repo, tip, "commit", "v1");
2065 let nested = write_tag(&repo, tag, "tag", "v1-nested");
2066 let tree_tag = write_tag(&repo, tree, "tree", "tree-tag");
2067 let blob_tag = write_tag(&repo, blob, "blob", "blob-tag");
2068 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
2069 let new_tip = write_commit(&repo, tree, &[tip], 2);
2070
2071 reset_commit_reads();
2072 let hidden = send_pack_uninteresting_commits(
2073 repo.odb(),
2074 &[new_tip],
2075 &[nested, tag, tree_tag, blob_tag, missing],
2076 &HashSet::new(),
2077 &CancelSignal::never(),
2078 )
2079 .unwrap();
2080 assert!(hidden.contains(&tip), "tags peel to the commit they name");
2081 for oid in [
2082 tag, nested, tree_tag, blob_tag, tree, blob, missing, new_tip,
2083 ] {
2084 assert!(!hidden.contains(&oid), "{oid} is not a have-side commit");
2085 }
2086 assert_eq!(
2087 commit_reads(),
2088 2,
2089 "the want and the tag-named commit, each queued once"
2090 );
2091 }
2092}