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 let mut send = walk.ordered;
1127 if let Some(peer) = peer_odb {
1128 if !peer.uses_files_primary() && !peer.custom_primary_is_empty()? {
1129 send = peer.missing_materialized_many(&send)?;
1130 }
1131 }
1132 Ok(WalkedObjects {
1133 send,
1134 name_hashes: walk.name_hashes,
1135 edges: walk.edges,
1136 missing: walk.missing.unwrap_or_default(),
1137 })
1138}
1139
1140struct WalkedObjects {
1142 send: Vec<ObjectId>,
1144 name_hashes: HashMap<ObjectId, u32>,
1146 edges: HashSet<ObjectId>,
1148 missing: Vec<MissingBlob>,
1150}
1151
1152fn child_path(parent: &str, name: &[u8]) -> String {
1153 let name = String::from_utf8_lossy(name);
1154 if parent.is_empty() {
1155 name.into_owned()
1156 } else {
1157 format!("{parent}/{name}")
1158 }
1159}
1160
1161struct EnumerationWalk<'a> {
1163 odb: &'a Odb,
1164 exclude: &'a HashSet<ObjectId>,
1165 boundary: &'a mut HashSet<ObjectId>,
1167 have_commits: &'a HashSet<ObjectId>,
1169 peer_odb: Option<&'a Odb>,
1171 alternate_sources: Option<Arc<CompositeStore>>,
1173 skip_missing: bool,
1174 filter: Option<&'a ObjectFilter>,
1175 depth_sensitive: bool,
1177 name_trees: NameTrees,
1178 visited: OidHashSet,
1182 tree_depths: OidHashMap<u64>,
1184 unlisted: OidHashSet,
1187 ordered: Vec<ObjectId>,
1188 commits: BinaryHeap<QueuedCommit>,
1190 next_seq: u64,
1191 queue: VecDeque<(ObjectId, String, u64)>,
1194 name_hashes: HashMap<ObjectId, u32>,
1195 edges: HashSet<ObjectId>,
1197 missing: Option<Vec<MissingBlob>>,
1199}
1200
1201struct QueuedCommit {
1204 generation: u64,
1206 time: i64,
1207 seq: std::cmp::Reverse<u64>,
1208 oid: ObjectId,
1209 tree: ObjectId,
1210 parents: Vec<ObjectId>,
1211}
1212
1213impl PartialEq for QueuedCommit {
1214 fn eq(&self, other: &Self) -> bool {
1215 (self.generation, self.time, self.seq) == (other.generation, other.time, other.seq)
1216 }
1217}
1218
1219impl Eq for QueuedCommit {}
1220
1221impl PartialOrd for QueuedCommit {
1222 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1223 Some(self.cmp(other))
1224 }
1225}
1226
1227impl Ord for QueuedCommit {
1228 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1229 (self.generation, self.time, self.seq).cmp(&(other.generation, other.time, other.seq))
1230 }
1231}
1232
1233impl<'a> EnumerationWalk<'a> {
1234 fn list(&mut self, oid: ObjectId) {
1235 if !self.exclude.contains(&oid) {
1236 self.ordered.push(oid);
1237 }
1238 }
1239
1240 fn note_boundary(&mut self, oid: ObjectId) {
1241 self.visited.insert(oid);
1242 }
1243
1244 fn at_object_boundary(&self, oid: ObjectId) -> bool {
1245 if self
1246 .alternate_sources
1247 .as_ref()
1248 .is_some_and(|s| s.contains(&oid).unwrap_or(false))
1249 {
1250 return true;
1251 }
1252 if self.boundary.contains(&oid) {
1253 return true;
1254 }
1255 self.peer_odb.is_some_and(|peer| {
1256 peer.uses_files_primary() && peer.exists_materialized_local_without_rescan(&oid)
1257 })
1258 }
1259
1260 fn seal_have_reachable_commit(&mut self, oid: ObjectId) -> Result<()> {
1261 if !self.visited.insert(oid) {
1262 return Ok(());
1263 }
1264 self.edges.insert(oid);
1265 self.boundary
1266 .extend(objects_in_commit_snapshot(self.odb, oid)?);
1267 Ok(())
1268 }
1269
1270 fn commit_on_have_side(&mut self, oid: ObjectId) -> Result<bool> {
1271 Ok(self.have_commits.contains(&oid))
1272 }
1273
1274 fn push_object(&mut self, oid: ObjectId, path: String) {
1276 if let Ok(true) = self.commit_on_have_side(oid) {
1277 let _ = self.seal_have_reachable_commit(oid);
1278 return;
1279 }
1280 if self.at_object_boundary(oid) {
1281 self.note_boundary(oid);
1282 return;
1283 }
1284 if self.visited.insert(oid) {
1285 self.list(oid);
1286 self.queue.push_back((oid, path, 0));
1287 } else if self.unlisted.remove(&oid) {
1288 self.list(oid);
1289 }
1290 }
1291
1292 fn push_root(&mut self, oid: ObjectId) -> Result<()> {
1295 if self.commit_on_have_side(oid)? {
1296 return self.seal_have_reachable_commit(oid);
1297 }
1298 if self.at_object_boundary(oid) {
1299 self.note_boundary(oid);
1300 return Ok(());
1301 }
1302 if !self.visited.insert(oid) {
1303 if self.unlisted.remove(&oid) {
1304 self.list(oid);
1305 }
1306 return Ok(());
1307 }
1308 self.list(oid);
1309 match self.odb.read_info(&oid) {
1312 Ok(info) if info.kind != ObjectKind::Commit => {
1313 self.queue.push_back((oid, String::new(), 0));
1314 return Ok(());
1315 }
1316 Ok(_) => {}
1317 Err(_) if self.skip_missing => return Ok(()),
1318 Err(e) => return Err(e),
1319 }
1320 match self.read(&oid)? {
1321 Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1322 Some(_) => {
1323 self.queue.push_back((oid, String::new(), 0));
1324 Ok(())
1325 }
1326 None => Ok(()),
1327 }
1328 }
1329
1330 fn push_parent(&mut self, oid: ObjectId) -> Result<()> {
1333 if self.commit_on_have_side(oid)? {
1334 return self.seal_have_reachable_commit(oid);
1335 }
1336 if self.at_object_boundary(oid) {
1337 self.note_boundary(oid);
1338 return Ok(());
1339 }
1340 let listed = self
1341 .filter
1342 .is_none_or(|f| f.includes_commit_or_tag_object(ObjectKind::Commit));
1343 if !self.visited.insert(oid) {
1344 if listed && self.unlisted.remove(&oid) {
1345 self.list(oid);
1346 }
1347 return Ok(());
1348 }
1349 if listed {
1350 self.list(oid);
1351 } else {
1352 self.unlisted.insert(oid);
1353 }
1354 match self.read(&oid)? {
1355 Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1356 Some(_) => {
1357 self.queue.push_back((oid, String::new(), 0));
1358 Ok(())
1359 }
1360 None => Ok(()),
1361 }
1362 }
1363
1364 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
1365 match self.odb.read(oid) {
1366 Ok(obj) => Ok(Some(obj)),
1367 Err(_) if self.skip_missing => Ok(None),
1368 Err(e) => Err(e),
1369 }
1370 }
1371
1372 fn queue_commit(&mut self, oid: ObjectId, obj: Object) -> Result<()> {
1374 let parsed = CommitRef::parse(&obj.data)?;
1375 let queued = QueuedCommit {
1376 generation: UNRANKED_GENERATION,
1377 time: parsed.committer_unix_seconds_for_ordering(),
1378 seq: std::cmp::Reverse(self.next_seq),
1379 oid,
1380 tree: parsed.tree(),
1381 parents: parsed.parents().to_vec(),
1382 };
1383 self.next_seq += 1;
1384 self.commits.push(queued);
1385 Ok(())
1386 }
1387
1388 fn push_tree(&mut self, oid: ObjectId, path: String, depth: u64) {
1390 if self.name_trees == NameTrees::Yes && !path.is_empty() && !self.visited.contains(&oid) {
1391 self.name_hashes
1392 .entry(oid)
1393 .or_insert_with(|| pack_name_hash(&path));
1394 }
1395 let Some(filter) = self.filter else {
1396 self.push_object(oid, path);
1397 return;
1398 };
1399 if self.at_object_boundary(oid) {
1401 self.note_boundary(oid);
1402 return;
1403 }
1404 let first_walk = if self.depth_sensitive {
1405 match self.tree_depths.get(&oid) {
1406 Some(&walked_at) if walked_at <= depth => false,
1407 _ => {
1408 self.tree_depths.insert(oid, depth);
1409 true
1410 }
1411 }
1412 } else {
1413 self.visited.insert(oid)
1414 };
1415 if !first_walk {
1416 return;
1417 }
1418 if filter.includes_tree(depth) {
1419 if !self.depth_sensitive || self.visited.insert(oid) {
1420 self.list(oid);
1421 }
1422 } else if !self.depth_sensitive {
1423 self.unlisted.insert(oid);
1424 }
1425 if !filter.prunes_tree_entries(depth) {
1426 self.queue.push_back((oid, path, depth));
1427 }
1428 }
1429
1430 fn push_blob(
1433 &mut self,
1434 oid: ObjectId,
1435 name: Option<(&str, &[u8])>,
1436 referrer: ObjectId,
1437 tree_depth: u64,
1438 ) -> Result<()> {
1439 if self.at_object_boundary(oid) {
1440 self.note_boundary(oid);
1441 return Ok(());
1442 }
1443 if self.visited.contains(&oid) {
1444 return Ok(());
1445 }
1446 let (passes, size_checked) = match self.filter {
1447 None => (true, false),
1448 Some(filter) => self
1451 .blob_passes(filter, &oid, tree_depth)?
1452 .unwrap_or((true, true)),
1453 };
1454 if passes && self.missing.is_some() && !self.odb.contains(&oid)? {
1455 self.visited.insert(oid);
1456 if let Some(missing) = self.missing.as_mut() {
1457 missing.push(MissingBlob {
1458 oid,
1459 referrer: Some(referrer),
1460 });
1461 }
1462 return Ok(());
1463 }
1464 if passes {
1465 self.visited.insert(oid);
1466 self.list(oid);
1467 if let Some((parent, entry_name)) = name {
1468 self.name_hashes
1469 .insert(oid, pack_name_hash(&child_path(parent, entry_name)));
1470 }
1471 } else if size_checked && !self.depth_sensitive {
1472 self.visited.insert(oid);
1473 self.unlisted.insert(oid);
1474 }
1475 Ok(())
1476 }
1477
1478 fn blob_passes(
1485 &self,
1486 filter: &ObjectFilter,
1487 oid: &ObjectId,
1488 tree_depth: u64,
1489 ) -> Result<Option<(bool, bool)>> {
1490 if !filter.includes_blob_under_tree(0, tree_depth) {
1492 return Ok(Some((false, false)));
1493 }
1494 if filter.includes_blob_under_tree(u64::MAX, tree_depth) {
1495 return Ok(Some((true, false)));
1496 }
1497 let info = if self.missing.is_some() {
1498 match self.odb.read_info_without_fetch(oid) {
1499 Err(crate::error::Error::ObjectNotFound(_)) => return Ok(None),
1500 other => other,
1501 }
1502 } else {
1503 self.odb.read_info(oid)
1504 };
1505 match info {
1506 Ok(info) => Ok(Some((
1507 filter.includes_blob_under_tree(info.size, tree_depth),
1508 true,
1509 ))),
1510 Err(_) if self.skip_missing => Ok(Some((false, true))),
1511 Err(e) => Err(e),
1512 }
1513 }
1514}
1515
1516#[cfg(test)]
1517mod tests {
1518 #![allow(clippy::unwrap_used, clippy::expect_used)]
1519
1520 use super::*;
1521
1522 use crate::repo::{InitOptions, Repository};
1523 use tempfile::TempDir;
1524
1525 fn walk(repo: &Repository, roots: &[ObjectId], name_trees: NameTrees) -> PackObjectSelection {
1526 let empty = HashSet::new();
1527 enumerate_pack_objects_naming(
1528 repo.odb(),
1529 roots,
1530 &[],
1531 &PackEnumerateOptions {
1532 filter: None,
1533 shallow_grafts: &empty,
1534 have_shallow_grafts: Some(&empty),
1535 use_bitmaps: false,
1536 exclude_objects: None,
1537 peer_odb: None,
1538 have_boundary: HaveBoundary::CommitClosure,
1539 },
1540 name_trees,
1541 false,
1542 &CancelSignal::never(),
1543 )
1544 .expect("walk")
1545 }
1546
1547 fn write_tree(repo: &Repository, entries: &[(u32, &str, ObjectId)]) -> ObjectId {
1548 let entries: Vec<crate::objects::TreeEntry> = entries
1549 .iter()
1550 .map(|&(mode, name, oid)| crate::objects::TreeEntry {
1551 mode,
1552 name: name.as_bytes().to_vec(),
1553 oid,
1554 })
1555 .collect();
1556 repo.odb()
1557 .write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
1558 .expect("tree")
1559 }
1560
1561 fn write_commit(
1562 repo: &Repository,
1563 tree: ObjectId,
1564 parents: &[ObjectId],
1565 time: i64,
1566 ) -> ObjectId {
1567 let mut body = format!("tree {}\n", tree.to_hex());
1568 for parent in parents {
1569 body.push_str(&format!("parent {}\n", parent.to_hex()));
1570 }
1571 body.push_str(&format!(
1572 "author A <a@example.com> {time} +0000\ncommitter A <a@example.com> {time} +0000\n\nc{time}\n"
1573 ));
1574 repo.odb()
1575 .write(ObjectKind::Commit, body.as_bytes())
1576 .expect("commit")
1577 }
1578
1579 #[test]
1580 fn tips_only_boundary_matches_closure_on_linear_incremental_push() {
1581 let dir = TempDir::new().unwrap();
1582 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1583 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1584 let mut parent = write_commit(
1585 &repo,
1586 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1587 &[],
1588 0,
1589 );
1590 for i in 1..800 {
1591 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1592 parent = write_commit(&repo, tree, &[parent], i);
1593 }
1594 let remote_tip = parent;
1595 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1596 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1597 let empty = HashSet::new();
1598 let closure_opts = PackEnumerateOptions {
1599 filter: None,
1600 shallow_grafts: &empty,
1601 have_shallow_grafts: Some(&empty),
1602 use_bitmaps: false,
1603 exclude_objects: None,
1604 peer_odb: None,
1605 have_boundary: HaveBoundary::CommitClosure,
1606 };
1607 let tips_opts = PackEnumerateOptions {
1608 have_boundary: HaveBoundary::TipsOnly,
1609 ..closure_opts
1610 };
1611 let closure = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &closure_opts)
1612 .expect("closure");
1613 let tips = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &tips_opts)
1614 .expect("tips");
1615 let closure_set: HashSet<ObjectId> = closure.send.iter().copied().collect();
1616 let tips_set: HashSet<ObjectId> = tips.send.iter().copied().collect();
1617 assert_eq!(closure_set, tips_set);
1618 assert!(
1619 tips.send.len() <= 8,
1620 "one new commit should enumerate a handful of objects, got {}",
1621 tips.send.len()
1622 );
1623 }
1624
1625 #[test]
1626 fn tips_only_send_pack_does_not_prewalk_deep_history() {
1627 use crate::merge_base::intersect_walk_test_hooks::{
1628 commit_parent_lookups, reset_commit_parent_lookups,
1629 };
1630
1631 let dir = TempDir::new().unwrap();
1632 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1633 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1634 let mut parent = write_commit(
1635 &repo,
1636 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1637 &[],
1638 0,
1639 );
1640 for i in 1..800 {
1641 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1642 parent = write_commit(&repo, tree, &[parent], i);
1643 }
1644 let remote_tip = parent;
1645 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1646 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1647 let haves = vec![remote_tip];
1648 let empty = HashSet::new();
1649 reset_commit_parent_lookups();
1650 let _selection = enumerate_pack_objects(
1651 repo.odb(),
1652 &[new_tip],
1653 &haves,
1654 &PackEnumerateOptions {
1655 filter: None,
1656 shallow_grafts: &empty,
1657 have_shallow_grafts: Some(&empty),
1658 use_bitmaps: false,
1659 exclude_objects: None,
1660 peer_odb: None,
1661 have_boundary: HaveBoundary::TipsOnly,
1662 },
1663 )
1664 .expect("enumerate");
1665 assert!(
1666 commit_parent_lookups() <= 128,
1667 "send-pack hide must stay coupled to the want walk ({} parent lookups)",
1668 commit_parent_lookups()
1669 );
1670 }
1671
1672 #[test]
1673 fn push_dated_before_graphed_history_stops_at_the_frontier() {
1674 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1675 let dir = TempDir::new().unwrap();
1676 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1677 let tree = write_tree(&repo, &[]);
1678 let mut tip = write_commit(&repo, tree, &[], 1_300_000_000);
1680 let mut tagged = tip;
1681 for i in 1..300 {
1682 tip = write_commit(&repo, tree, &[tip], 1_300_000_000 + i);
1683 if i == 150 {
1684 tagged = tip;
1685 }
1686 }
1687 std::fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n")).unwrap();
1688 repo.odb().write_commit_graph().unwrap();
1689 let mut new_tip = tip;
1692 for i in 0..3 {
1693 new_tip = write_commit(&repo, tree, &[new_tip], 1_000_000_000 + i);
1694 }
1695
1696 reset_commit_reads();
1697 let hidden = send_pack_uninteresting_commits(
1698 repo.odb(),
1699 &[new_tip],
1700 &[tip, tagged],
1701 &HashSet::new(),
1702 &CancelSignal::never(),
1703 )
1704 .unwrap();
1705 assert!(hidden.contains(&tip));
1706 assert!(hidden.contains(&tagged));
1707 assert!(!hidden.contains(&new_tip));
1708 assert_eq!(
1709 commit_reads(),
1710 3,
1711 "only the commits outside the graph are read from the object store"
1712 );
1713 assert!(
1714 hidden.len() <= 4,
1715 "the walk must stop at the frontier, not drain the remote's history ({} marked)",
1716 hidden.len()
1717 );
1718 }
1719
1720 #[test]
1721 fn duplicated_wants_already_on_peer_balance_the_frontier() {
1722 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1723 let dir = TempDir::new().unwrap();
1724 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1725 let tree = write_tree(&repo, &[]);
1726 let mut tip = write_commit(&repo, tree, &[], 0);
1727 for time in 1..100 {
1728 tip = write_commit(&repo, tree, &[tip], time);
1729 }
1730 reset_commit_reads();
1731 let hidden = send_pack_uninteresting_commits(
1732 repo.odb(),
1733 &[tip, tip],
1734 &[tip, tip],
1735 &HashSet::new(),
1736 &CancelSignal::never(),
1737 )
1738 .unwrap();
1739 assert!(hidden.contains(&tip));
1740 assert!(
1741 commit_reads() <= 2,
1742 "shared want/have must stop at the frontier"
1743 );
1744 let empty = HashSet::new();
1745 let selection = enumerate_pack_objects(
1746 repo.odb(),
1747 &[tip, tip],
1748 &[tip, tip],
1749 &PackEnumerateOptions {
1750 filter: None,
1751 shallow_grafts: &empty,
1752 have_shallow_grafts: None,
1753 use_bitmaps: false,
1754 exclude_objects: None,
1755 peer_odb: None,
1756 have_boundary: HaveBoundary::TipsOnly,
1757 },
1758 )
1759 .unwrap();
1760 assert!(selection.send.is_empty());
1761 }
1762
1763 #[test]
1764 fn tips_only_many_advertised_haves_does_not_prewalk_deep_history() {
1765 use crate::merge_base::intersect_walk_test_hooks::{
1766 commit_parent_lookups, reset_commit_parent_lookups,
1767 };
1768
1769 let dir = TempDir::new().unwrap();
1770 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1771 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1772 let mut chain = Vec::new();
1773 let mut parent = write_commit(
1774 &repo,
1775 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1776 &[],
1777 0,
1778 );
1779 chain.push(parent);
1780 for i in 1..800 {
1781 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1782 parent = write_commit(&repo, tree, &[parent], i);
1783 chain.push(parent);
1784 }
1785 let remote_tip = parent;
1786 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1787 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1788 let mut haves = vec![remote_tip];
1789 for n in (2..400).step_by(2) {
1790 let idx = chain.len().checked_sub(1 + n);
1791 if let Some(idx) = idx {
1792 haves.push(chain[idx]);
1793 }
1794 }
1795 let empty = HashSet::new();
1796 reset_commit_parent_lookups();
1797 let selection = enumerate_pack_objects(
1798 repo.odb(),
1799 &[new_tip],
1800 &haves,
1801 &PackEnumerateOptions {
1802 filter: None,
1803 shallow_grafts: &empty,
1804 have_shallow_grafts: Some(&empty),
1805 use_bitmaps: false,
1806 exclude_objects: None,
1807 peer_odb: None,
1808 have_boundary: HaveBoundary::TipsOnly,
1809 },
1810 )
1811 .expect("enumerate");
1812 assert!(
1813 selection.send.len() <= 8,
1814 "one new commit should stay small with many haves, got {}",
1815 selection.send.len()
1816 );
1817 assert!(
1818 commit_parent_lookups() <= 512,
1819 "many advertised haves must not pre-walk deep history ({} parent lookups)",
1820 commit_parent_lookups()
1821 );
1822 }
1823
1824 #[test]
1825 fn send_pack_have_walk_stops_without_scanning_deep_haves() {
1826 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1827
1828 let dir = TempDir::new().unwrap();
1829 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1830 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1831 let mut parent = write_commit(
1832 &repo,
1833 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1834 &[],
1835 0,
1836 );
1837 let near_root = parent;
1838 for i in 1..800 {
1839 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1840 parent = write_commit(&repo, tree, &[parent], i);
1841 }
1842 let direct_parent = parent;
1843 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1844 let new_tip = write_commit(&repo, new_tree, &[direct_parent], 900);
1845 let empty = HashSet::new();
1846 reset_commit_reads();
1847 let have_commits = send_pack_uninteresting_commits(
1848 repo.odb(),
1849 &[new_tip],
1850 &[direct_parent, near_root],
1851 &empty,
1852 &CancelSignal::never(),
1853 )
1854 .expect("send-pack have walk");
1855 assert!(have_commits.contains(&direct_parent));
1856 assert!(have_commits.contains(&near_root));
1857 let reads = commit_reads();
1858 assert!(
1859 reads < 20,
1860 "send-pack have walk should not scan deep history, read {reads} commits"
1861 );
1862 }
1863
1864 #[test]
1865 fn walk_lists_trees_of_newer_commits_first() {
1866 let dir = TempDir::new().unwrap();
1867 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1868 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1869 let tree_a = write_tree(&repo, &[(0o100644, "f", blob("a"))]);
1870 let tree_b = write_tree(&repo, &[(0o100644, "f", blob("b"))]);
1871 let tree_c = write_tree(&repo, &[(0o100644, "f", blob("c"))]);
1872 let base = write_commit(&repo, tree_a, &[], 100);
1873 let old_tip = write_commit(&repo, tree_b, &[base], 200);
1875 let new_tip = write_commit(&repo, tree_c, &[base], 300);
1876
1877 let selection = walk(&repo, &[old_tip, new_tip], NameTrees::No);
1878 let pos = |oid: ObjectId| selection.send.iter().position(|o| *o == oid).unwrap();
1879 assert!(pos(old_tip) < pos(base) && pos(new_tip) < pos(base));
1880 assert!(pos(tree_c) < pos(tree_b) && pos(tree_b) < pos(tree_a));
1881 }
1882
1883 #[test]
1884 fn walk_names_trees_and_keeps_history_paths_for_path_less_roots() {
1885 let dir = TempDir::new().unwrap();
1886 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1887 let file = repo.odb().write(ObjectKind::Blob, b"x\n").unwrap();
1888 let link = repo.odb().write(ObjectKind::Blob, b"target").unwrap();
1889 let sub = write_tree(
1890 &repo,
1891 &[(0o100644, "file.rs", file), (0o120000, "link", link)],
1892 );
1893 let root = write_tree(&repo, &[(0o040000, "src", sub)]);
1894 let commit = write_commit(&repo, root, &[], 100);
1895
1896 let selection = walk(&repo, &[sub, file, commit], NameTrees::Yes);
1898 let hash = |oid: ObjectId| selection.name_hashes.get(&oid).copied();
1899 assert_eq!(hash(sub), Some(pack_name_hash("src")));
1900 assert_eq!(hash(file), Some(pack_name_hash("src/file.rs")));
1901 assert_eq!(hash(root), None, "root trees have no path");
1902 assert_eq!(selection.send.len(), 5);
1903
1904 let blobs_only = walk(&repo, &[sub, file, commit], NameTrees::No);
1905 assert_eq!(blobs_only.name_hashes.get(&sub), None);
1906 assert_eq!(
1907 blobs_only.name_hashes.get(&file),
1908 Some(&pack_name_hash("src/file.rs"))
1909 );
1910 }
1911
1912 fn tree_entry(mode: &str, name: &str, oid: ObjectId) -> Vec<u8> {
1913 let mut bytes = format!("{mode} {name}\0").into_bytes();
1914 bytes.extend_from_slice(oid.as_bytes());
1915 bytes
1916 }
1917
1918 #[cfg_attr(
1922 not(feature = "full-tests"),
1923 ignore = "starts system git or is heavy; run with --features full-tests"
1924 )]
1925 #[test]
1926 fn walk_descends_into_trees_with_non_canonical_modes() {
1927 let tmp = tempfile::tempdir().unwrap();
1928 Repository::init(tmp.path(), &InitOptions::new().with_bare(true)).unwrap();
1929 let repo = Repository::open(tmp.path(), &RepositoryOptions::empty()).unwrap();
1930 let odb = repo.odb();
1931 let deep = odb
1932 .write_loose_materialize(ObjectKind::Blob, b"deep\n")
1933 .unwrap();
1934 let odd = odb
1935 .write_loose_materialize(ObjectKind::Blob, b"odd\n")
1936 .unwrap();
1937 let inner = odb
1938 .write_loose_materialize(ObjectKind::Tree, &tree_entry("100644", "deep.txt", deep))
1939 .unwrap();
1940 let mut root = tree_entry("100664", "odd.txt", odd);
1941 root.extend(tree_entry("40755", "sub", inner));
1942 let root = odb
1943 .write_loose_materialize(ObjectKind::Tree, &root)
1944 .unwrap();
1945 let commit = format!(
1946 "tree {root}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nodd modes\n"
1947 );
1948 let commit = odb
1949 .write_loose_materialize(ObjectKind::Commit, commit.as_bytes())
1950 .unwrap();
1951
1952 let empty = HashSet::new();
1953 let selection = enumerate_pack_objects(
1954 odb,
1955 &[commit],
1956 &[],
1957 &PackEnumerateOptions {
1958 filter: None,
1959 shallow_grafts: &empty,
1960 have_shallow_grafts: Some(&empty),
1961 use_bitmaps: false,
1962 exclude_objects: None,
1963 peer_odb: None,
1964 have_boundary: HaveBoundary::CommitClosure,
1965 },
1966 )
1967 .unwrap();
1968
1969 let ours: HashSet<ObjectId> = selection.send.iter().copied().collect();
1970 let git: HashSet<ObjectId> =
1971 grit_test_support::git(tmp.path(), &["rev-list", "--objects", &commit.to_hex()])
1972 .lines()
1973 .map(|line| ObjectId::from_hex(&line[..40]).unwrap())
1974 .collect();
1975 assert_eq!(ours, git, "same objects as git rev-list --objects");
1976 assert!(ours.contains(&inner) && ours.contains(&deep));
1977 assert_eq!(selection.send.len(), 5);
1978 assert_eq!(
1979 selection.name_hashes.get(&deep),
1980 Some(&pack_name_hash("sub/deep.txt"))
1981 );
1982 assert_eq!(
1983 selection.name_hashes.get(&odd),
1984 Some(&pack_name_hash("odd.txt"))
1985 );
1986 }
1987
1988 fn tips_only_options(empty: &HashSet<ObjectId>) -> PackEnumerateOptions<'_> {
1989 PackEnumerateOptions {
1990 filter: None,
1991 shallow_grafts: empty,
1992 have_shallow_grafts: Some(empty),
1993 use_bitmaps: false,
1994 exclude_objects: None,
1995 peer_odb: None,
1996 have_boundary: HaveBoundary::TipsOnly,
1997 }
1998 }
1999
2000 fn write_tag(repo: &Repository, target: ObjectId, kind: &str, name: &str) -> ObjectId {
2001 let body = format!(
2002 "object {}\ntype {kind}\ntag {name}\ntagger A <a@example.com> 0 +0000\n\n{name}\n",
2003 target.to_hex()
2004 );
2005 repo.odb().write(ObjectKind::Tag, body.as_bytes()).unwrap()
2006 }
2007
2008 #[test]
2009 fn tips_only_thin_bases_come_from_edge_commits_not_every_advertised_tip() {
2010 let dir = TempDir::new().unwrap();
2011 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2012 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
2013 let base_tree = write_tree(&repo, &[(0o100644, "f", blob("base"))]);
2014 let root = write_commit(&repo, base_tree, &[], 0);
2015 let main = write_commit(
2016 &repo,
2017 write_tree(&repo, &[(0o100644, "f", blob("main"))]),
2018 &[root],
2019 10,
2020 );
2021 let mut haves = vec![main];
2022 for i in 0..40 {
2023 let tree = write_tree(&repo, &[(0o100644, "f", blob(&format!("side {i}")))]);
2024 let side = write_commit(&repo, tree, &[root], 20 + i);
2025 haves.push(write_tag(&repo, side, "commit", &format!("t{i}")));
2026 haves.push(side);
2027 }
2028 let new_tip = write_commit(
2029 &repo,
2030 write_tree(&repo, &[(0o100644, "f", blob("main\nmore"))]),
2031 &[main],
2032 100,
2033 );
2034 let empty = HashSet::new();
2035 let selection =
2036 enumerate_pack_objects(repo.odb(), &[new_tip], &haves, &tips_only_options(&empty))
2037 .unwrap();
2038 assert_eq!(selection.send.len(), 3, "commit, tree and the changed blob");
2039 assert_eq!(
2040 selection.have_closure,
2041 HashSet::from([main]),
2042 "thin bases come from the commit the walk stopped at"
2043 );
2044
2045 let closure = enumerate_pack_objects(
2046 repo.odb(),
2047 &[new_tip],
2048 &haves,
2049 &PackEnumerateOptions {
2050 have_boundary: HaveBoundary::CommitClosure,
2051 ..tips_only_options(&empty)
2052 },
2053 )
2054 .unwrap();
2055 assert_eq!(closure.send, selection.send);
2056 assert_eq!(
2057 closure.have_closure,
2058 HashSet::from([main]),
2059 "the full have pre-walk seeds thin bases from the same edge commit"
2060 );
2061 }
2062
2063 #[test]
2064 fn send_pack_walk_peels_tag_haves_and_ignores_non_commit_targets() {
2065 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
2066 let dir = TempDir::new().unwrap();
2067 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
2068 let blob = repo.odb().write(ObjectKind::Blob, b"x").unwrap();
2069 let tree = write_tree(&repo, &[(0o100644, "f", blob)]);
2070 let old = write_commit(&repo, tree, &[], 0);
2071 let tip = write_commit(&repo, tree, &[old], 1);
2072 let tag = write_tag(&repo, tip, "commit", "v1");
2073 let nested = write_tag(&repo, tag, "tag", "v1-nested");
2074 let tree_tag = write_tag(&repo, tree, "tree", "tree-tag");
2075 let blob_tag = write_tag(&repo, blob, "blob", "blob-tag");
2076 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
2077 let new_tip = write_commit(&repo, tree, &[tip], 2);
2078
2079 reset_commit_reads();
2080 let hidden = send_pack_uninteresting_commits(
2081 repo.odb(),
2082 &[new_tip],
2083 &[nested, tag, tree_tag, blob_tag, missing],
2084 &HashSet::new(),
2085 &CancelSignal::never(),
2086 )
2087 .unwrap();
2088 assert!(hidden.contains(&tip), "tags peel to the commit they name");
2089 for oid in [
2090 tag, nested, tree_tag, blob_tag, tree, blob, missing, new_tip,
2091 ] {
2092 assert!(!hidden.contains(&oid), "{oid} is not a have-side commit");
2093 }
2094 assert_eq!(
2095 commit_reads(),
2096 2,
2097 "the want and the tag-named commit, each queued once"
2098 );
2099 }
2100}