1use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
4use std::fmt;
5use std::sync::Arc;
6
7use crate::bitmap_walk::{BitmapWalkError, ReachabilityQuery, ReachableSet};
8use crate::commit_graph_file::{CommitGraphChain, UNRANKED_GENERATION};
9use crate::environment::RepositoryOptions;
10use crate::error::Result;
11use crate::objects::{
12 oid_hash_map, oid_hash_set, parse_tag, tag_object_line_oid, CommitRef, Object, ObjectId,
13 ObjectKind, OidHashMap, OidHashSet, TreeRefIter,
14};
15use crate::odb::Odb;
16use crate::pack_bitmap::BitmapIndex;
17use crate::pack_name_hash::pack_name_hash;
18use crate::repo::Repository;
19use crate::rev_list::{MissingAction, ObjectFilter};
20
21#[derive(Clone)]
23#[allow(clippy::type_complexity)]
24pub struct PackObjectSelection {
25 pub send: Vec<ObjectId>,
27 pub have_closure: HashSet<ObjectId>,
32 pub peer_held: Option<Arc<ReachableSet>>,
34 pub name_hashes: HashMap<ObjectId, u32>,
38}
39
40impl fmt::Debug for PackObjectSelection {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 f.debug_struct("PackObjectSelection")
43 .field("send", &self.send)
44 .field("have_closure", &self.have_closure)
45 .field(
46 "peer_held",
47 &self
48 .peer_held
49 .as_ref()
50 .map(|held| held.count())
51 .unwrap_or(0),
52 )
53 .field("name_hashes", &self.name_hashes)
54 .finish()
55 }
56}
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60pub enum HaveBoundary {
61 #[default]
63 CommitClosure,
64 TipsOnly,
68}
69
70#[derive(Clone, Copy, Debug)]
72pub struct PackEnumerateOptions<'a> {
73 pub filter: Option<&'a ObjectFilter>,
75 pub shallow_grafts: &'a HashSet<ObjectId>,
79 pub have_shallow_grafts: Option<&'a HashSet<ObjectId>>,
82 pub use_bitmaps: bool,
84 pub exclude_objects: Option<&'a HashSet<ObjectId>>,
86 pub peer_odb: Option<&'a Odb>,
88 pub have_boundary: HaveBoundary,
90}
91
92pub fn enumerate_pack_objects(
98 odb: &Odb,
99 wants: &[ObjectId],
100 haves: &[ObjectId],
101 opts: &PackEnumerateOptions<'_>,
102) -> Result<PackObjectSelection> {
103 enumerate_pack_objects_naming(odb, wants, haves, opts, NameTrees::No)
104}
105
106#[derive(Clone, Copy, PartialEq, Eq)]
108pub(crate) enum NameTrees {
109 No,
111 Yes,
113}
114
115impl NameTrees {
116 pub(crate) fn for_delta_search(opts: &crate::pack_objects::PackBuildOptions) -> Self {
119 if opts.delta && (opts.thin || (opts.window > 0 && opts.max_depth > 0)) {
120 Self::Yes
121 } else {
122 Self::No
123 }
124 }
125}
126
127pub(crate) fn enumerate_pack_objects_naming(
129 odb: &Odb,
130 wants: &[ObjectId],
131 haves: &[ObjectId],
132 opts: &PackEnumerateOptions<'_>,
133 name_trees: NameTrees,
134) -> Result<PackObjectSelection> {
135 if opts.use_bitmaps {
136 if let Some(sel) = try_bitmap_enumeration(odb, wants, haves, opts)? {
137 return Ok(sel);
138 }
139 }
140 walk_enumeration(odb, wants, haves, opts, name_trees)
141}
142
143pub fn try_bitmap_reachable_for_upload(
147 odb: &Odb,
148 wants: &[ObjectId],
149 haves: &[ObjectId],
150 filter: Option<&ObjectFilter>,
151 shallow_grafts: &HashSet<ObjectId>,
152) -> Result<Option<(ReachableSet, ReachableSet)>> {
153 if !shallow_grafts.is_empty() || filter.is_some_and(selects_object_type) {
154 return Ok(None);
155 }
156 let Some(git_dir) = odb.config_git_dir() else {
157 return Ok(None);
158 };
159 let repo = match Repository::open(git_dir, &RepositoryOptions::empty()) {
160 Ok(r) => r,
161 Err(_) => return Ok(None),
162 };
163 let Some(index) = BitmapIndex::open(&repo).ok().flatten() else {
164 return Ok(None);
165 };
166 let index = Arc::new(index);
167 match index.reachability_and_haves(
168 &repo,
169 ReachabilityQuery {
170 wants,
171 haves,
172 filter,
173 },
174 MissingAction::Error,
175 MissingAction::Allow,
176 ) {
177 Ok(sets) => Ok(Some(sets)),
178 Err(BitmapWalkError::Unsupported(_)) => Ok(None),
179 Err(BitmapWalkError::Bitmap(_)) => Ok(None),
180 Err(BitmapWalkError::Repository(e)) => Err(e),
181 }
182}
183
184fn selects_object_type(filter: &ObjectFilter) -> bool {
187 match filter {
188 ObjectFilter::ObjectType(_) => true,
189 ObjectFilter::Combine(parts) => parts.iter().any(selects_object_type),
190 _ => false,
191 }
192}
193
194fn try_bitmap_enumeration(
195 odb: &Odb,
196 wants: &[ObjectId],
197 haves: &[ObjectId],
198 opts: &PackEnumerateOptions<'_>,
199) -> Result<Option<PackObjectSelection>> {
200 let Some((send_set, peer_held)) =
201 try_bitmap_reachable_for_upload(odb, wants, haves, opts.filter, opts.shallow_grafts)?
202 else {
203 return Ok(None);
204 };
205 let index = send_set.bitmap_index();
206 let send: Vec<ObjectId> = send_set.iter_grouped_by_kind().collect();
207 let have_closure: HashSet<ObjectId> = haves.iter().copied().collect();
208 let name_hashes = name_hashes_from_bitmap(index, &send, &HashSet::new());
209 Ok(Some(PackObjectSelection {
210 send,
211 have_closure,
212 peer_held: Some(Arc::new(peer_held)),
213 name_hashes,
214 }))
215}
216
217pub(crate) fn name_hashes_for_send(
218 index: &BitmapIndex,
219 send: &[ObjectId],
220 have_closure: &HashSet<ObjectId>,
221) -> HashMap<ObjectId, u32> {
222 name_hashes_from_bitmap(index, send, have_closure)
223}
224
225fn name_hashes_from_bitmap(
226 index: &BitmapIndex,
227 send: &[ObjectId],
228 have_closure: &HashSet<ObjectId>,
229) -> HashMap<ObjectId, u32> {
230 let mut out = HashMap::new();
231 for oid in send.iter().chain(have_closure.iter()) {
232 let Some(pos) = index.position_of(oid) else {
233 continue;
234 };
235 if let Some(h) = index.name_hash(pos) {
236 out.insert(*oid, h);
237 }
238 }
239 out
240}
241
242fn walk_enumeration(
243 odb: &Odb,
244 wants: &[ObjectId],
245 haves: &[ObjectId],
246 opts: &PackEnumerateOptions<'_>,
247 name_trees: NameTrees,
248) -> Result<PackObjectSelection> {
249 let have_shallow = opts.have_shallow_grafts.unwrap_or(opts.shallow_grafts);
250 let have_commits = match opts.have_boundary {
251 HaveBoundary::CommitClosure => {
252 commits_reachable_from_haves(odb, haves, have_shallow, true)?
253 }
254 HaveBoundary::TipsOnly => send_pack_uninteresting_commits(odb, wants, haves, have_shallow)?,
255 };
256 let mut boundary: HashSet<ObjectId> = HashSet::new();
257 if let Some(extra) = opts.exclude_objects {
258 boundary.extend(extra.iter().copied());
259 }
260 let peer_held = try_peer_held_bitmap(odb, haves, opts)?;
261 let (send, name_hashes, edges) = match opts.filter {
262 Some(filter) if filter.uses_paths() => {
263 let (send, name_hashes) =
264 rev_list_filtered_objects(odb, wants, haves, filter, &have_commits)?;
265 (send, name_hashes, None)
266 }
267 filter => {
268 let (send, name_hashes, edges) = collect_reachable_excluding_with_name_hashes(
269 odb,
270 wants,
271 &HashSet::new(),
272 &mut boundary,
273 &have_commits,
274 opts.peer_odb,
275 false,
276 opts.shallow_grafts,
277 filter,
278 name_trees,
279 )?;
280 (send, name_hashes, Some(edges))
281 }
282 };
283 let have_closure = match edges {
286 Some(edges) => edges,
287 None => haves.iter().copied().collect(),
288 };
289 Ok(PackObjectSelection {
290 send,
291 have_closure,
292 peer_held,
293 name_hashes,
294 })
295}
296
297const SEND_PACK_REACH_WANT: u8 = 1;
298const SEND_PACK_REACH_HAVE: u8 = 2;
299
300#[cfg(test)]
301pub(crate) mod send_pack_walk_test_hooks {
302 use std::cell::Cell;
303
304 thread_local! {
305 static COMMIT_READS: Cell<usize> = const { Cell::new(0) };
306 }
307
308 pub fn reset_commit_reads() {
309 COMMIT_READS.with(|c| c.set(0));
310 }
311
312 pub fn commit_reads() -> usize {
313 COMMIT_READS.with(|c| c.get())
314 }
315
316 pub(crate) fn record_commit_read() {
317 COMMIT_READS.with(|c| c.set(c.get().saturating_add(1)));
318 }
319}
320
321fn send_pack_reach_uninteresting(reach: u8) -> bool {
322 reach & SEND_PACK_REACH_HAVE != 0
323}
324
325fn send_pack_reach_interesting(reach: u8) -> bool {
326 reach & SEND_PACK_REACH_WANT != 0 && !send_pack_reach_uninteresting(reach)
327}
328
329fn send_pack_uninteresting_commits(
335 odb: &Odb,
336 wants: &[ObjectId],
337 haves: &[ObjectId],
338 shallow_grafts: &HashSet<ObjectId>,
339) -> Result<HashSet<ObjectId>> {
340 struct SendPackRevWalk<'a> {
341 odb: &'a Odb,
342 graph: Option<Arc<CommitGraphChain>>,
343 shallow_grafts: &'a HashSet<ObjectId>,
344 reach: HashMap<ObjectId, u8>,
345 heap: BinaryHeap<QueuedCommit>,
346 on_heap: HashSet<ObjectId>,
347 processed: HashSet<ObjectId>,
348 uninteresting: HashSet<ObjectId>,
349 interesting_on_heap: usize,
350 next_seq: u64,
351 }
352
353 impl<'a> SendPackRevWalk<'a> {
354 fn note_reach(&mut self, oid: ObjectId, flag: u8) {
355 let entry = self.reach.entry(oid).or_insert(0);
356 let was_interesting = send_pack_reach_interesting(*entry);
357 *entry |= flag;
358 let is_interesting = send_pack_reach_interesting(*entry);
359 if self.on_heap.contains(&oid) {
360 match (was_interesting, is_interesting) {
361 (false, true) => self.interesting_on_heap += 1,
362 (true, false) => self.interesting_on_heap -= 1,
363 _ => {}
364 }
365 }
366 if send_pack_reach_uninteresting(*entry) {
367 self.uninteresting.insert(oid);
368 }
369 }
370
371 fn can_stop_after(&self) -> bool {
376 !self.heap.is_empty() && self.interesting_on_heap == 0
379 }
380
381 fn reach_unqueued(&mut self, oid: ObjectId, flag: u8) -> bool {
383 self.note_reach(oid, flag);
384 !(self.processed.contains(&oid)
385 || self.on_heap.contains(&oid)
386 || self.shallow_grafts.contains(&oid))
387 }
388
389 fn try_enqueue(&mut self, oid: ObjectId, flag: u8) -> Result<()> {
390 if !self.reach_unqueued(oid, flag) {
391 return Ok(());
392 }
393 if self.queue_from_graph(oid) {
394 return Ok(());
395 }
396 match self.odb.read(&oid) {
397 Ok(obj) => self.queue_commit(oid, &obj),
398 Err(_) => Ok(()),
399 }
400 }
401
402 fn enqueue_tip(&mut self, tip: ObjectId, flag: u8) -> Result<()> {
407 let mut oid = tip;
408 let mut tags_seen: Option<HashSet<ObjectId>> = None;
409 loop {
410 if self.graph_lists(&oid) {
411 if self.reach_unqueued(oid, flag) {
412 self.queue_from_graph(oid);
413 }
414 return Ok(());
415 }
416 let Ok(obj) = self.odb.read(&oid) else {
417 return Ok(());
418 };
419 if obj.kind != ObjectKind::Tag {
420 if obj.kind != ObjectKind::Commit || !self.reach_unqueued(oid, flag) {
421 return Ok(());
422 }
423 return self.queue_commit(oid, &obj);
424 }
425 if !tags_seen.get_or_insert_with(HashSet::new).insert(oid) {
426 return Ok(());
427 }
428 let Some(target) = tag_object_line_oid(&obj.data) else {
429 return Ok(());
430 };
431 if self.processed.contains(&target) || self.on_heap.contains(&target) {
432 self.note_reach(target, flag);
433 return Ok(());
434 }
435 oid = target;
436 }
437 }
438
439 fn graph_lists(&self, oid: &ObjectId) -> bool {
440 self.graph
441 .as_deref()
442 .is_some_and(|g| g.find_commit(oid).is_some())
443 }
444
445 fn queue_from_graph(&mut self, oid: ObjectId) -> bool {
448 let Some(entry) = self.graph.as_deref().and_then(|g| g.graph_walk_entry(&oid)) else {
449 return false;
450 };
451 self.push_queued(QueuedCommit {
452 generation: entry.generation,
453 time: entry.time,
454 seq: std::cmp::Reverse(self.next_seq),
455 oid,
456 tree: entry.tree,
457 parents: entry.parents,
458 });
459 true
460 }
461
462 fn queue_commit(&mut self, oid: ObjectId, obj: &Object) -> Result<()> {
463 if obj.kind != ObjectKind::Commit {
464 return Ok(());
465 }
466 let parsed = CommitRef::parse(&obj.data)?;
467 #[cfg(test)]
468 send_pack_walk_test_hooks::record_commit_read();
469 self.push_queued(QueuedCommit {
470 generation: UNRANKED_GENERATION,
471 time: parsed.committer_unix_seconds_for_ordering(),
472 seq: std::cmp::Reverse(self.next_seq),
473 oid,
474 tree: parsed.tree(),
475 parents: parsed.parents().to_vec(),
476 });
477 Ok(())
478 }
479
480 fn push_queued(&mut self, queued: QueuedCommit) {
481 self.on_heap.insert(queued.oid);
482 if send_pack_reach_interesting(*self.reach.get(&queued.oid).unwrap_or(&0)) {
483 self.interesting_on_heap += 1;
484 }
485 self.heap.push(queued);
486 self.next_seq += 1;
487 }
488 }
489
490 let graph = if shallow_grafts.is_empty() {
496 odb.commit_graph_chain()
497 } else {
498 None
499 };
500 let mut walk = SendPackRevWalk {
501 odb,
502 graph,
503 shallow_grafts,
504 reach: HashMap::new(),
505 heap: BinaryHeap::new(),
506 on_heap: HashSet::new(),
507 processed: HashSet::new(),
508 uninteresting: HashSet::new(),
509 interesting_on_heap: 0,
510 next_seq: 0,
511 };
512
513 for &want in wants {
514 walk.enqueue_tip(want, SEND_PACK_REACH_WANT)?;
515 }
516 for &have in haves {
517 walk.enqueue_tip(have, SEND_PACK_REACH_HAVE)?;
518 }
519
520 while let Some(commit) = walk.heap.pop() {
521 walk.on_heap.remove(&commit.oid);
522 if send_pack_reach_interesting(*walk.reach.get(&commit.oid).unwrap_or(&0)) {
523 walk.interesting_on_heap -= 1;
524 }
525 if !walk.processed.insert(commit.oid) {
526 continue;
527 }
528 let flags = *walk.reach.get(&commit.oid).unwrap_or(&0);
529 if send_pack_reach_uninteresting(flags) {
530 walk.uninteresting.insert(commit.oid);
531 }
532 let parent_flag = if send_pack_reach_uninteresting(flags) {
533 SEND_PACK_REACH_HAVE
534 } else {
535 SEND_PACK_REACH_WANT
536 };
537 if !walk.shallow_grafts.contains(&commit.oid) {
538 for parent in commit.parents {
539 walk.try_enqueue(parent, parent_flag)?;
540 }
541 }
542 if walk.can_stop_after() {
543 break;
544 }
545 }
546 Ok(walk.uninteresting)
547}
548
549fn commits_reachable_from_haves(
551 odb: &Odb,
552 haves: &[ObjectId],
553 shallow_grafts: &HashSet<ObjectId>,
554 skip_missing: bool,
555) -> Result<HashSet<ObjectId>> {
556 let mut seen = HashSet::new();
557 let mut queue: VecDeque<ObjectId> = haves.iter().copied().collect();
558 while let Some(oid) = queue.pop_front() {
559 if !seen.insert(oid) {
560 continue;
561 }
562 if shallow_grafts.contains(&oid) {
563 continue;
564 }
565 let obj = match odb.read(&oid) {
566 Ok(o) => o,
567 Err(_) if skip_missing => continue,
568 Err(e) => return Err(e),
569 };
570 if obj.kind != ObjectKind::Commit {
571 continue;
572 }
573 let parsed = CommitRef::parse(&obj.data)?;
574 for parent in parsed.parents() {
575 if !seen.contains(parent) {
576 queue.push_back(*parent);
577 }
578 }
579 }
580 Ok(seen)
581}
582
583fn objects_in_commit_snapshot(odb: &Odb, commit_oid: ObjectId) -> Result<HashSet<ObjectId>> {
585 let mut out = HashSet::new();
586 out.insert(commit_oid);
587 let commit_obj = match odb.read(&commit_oid) {
588 Ok(o) => o,
589 Err(_) => return Ok(out),
590 };
591 if commit_obj.kind != ObjectKind::Commit {
592 return Ok(out);
593 }
594 let commit = CommitRef::parse(&commit_obj.data)?;
595 let oid_len = odb.hash_algo().len();
596 let mut queue = VecDeque::from([commit.tree()]);
597 while let Some(tree_oid) = queue.pop_front() {
598 if !out.insert(tree_oid) {
599 continue;
600 }
601 let tree_obj = match odb.read(&tree_oid) {
602 Ok(o) => o,
603 Err(_) => continue,
604 };
605 for entry in TreeRefIter::with_oid_len(&tree_obj.data, oid_len) {
606 let entry = entry?;
607 if entry.mode == 0o160000 {
608 continue;
609 }
610 if entry.mode & 0o170000 == 0o040000 {
611 queue.push_back(entry.oid);
612 } else {
613 out.insert(entry.oid);
614 }
615 }
616 }
617 Ok(out)
618}
619
620fn try_peer_held_bitmap(
621 odb: &Odb,
622 haves: &[ObjectId],
623 opts: &PackEnumerateOptions<'_>,
624) -> Result<Option<Arc<ReachableSet>>> {
625 if haves.is_empty() || !opts.use_bitmaps {
626 return Ok(None);
627 }
628 let Some(peer) = try_bitmap_peer_held(odb, haves, opts.shallow_grafts)? else {
629 return Ok(None);
630 };
631 Ok(Some(Arc::new(peer)))
632}
633
634fn try_bitmap_peer_held(
635 odb: &Odb,
636 haves: &[ObjectId],
637 shallow_grafts: &HashSet<ObjectId>,
638) -> Result<Option<ReachableSet>> {
639 if !shallow_grafts.is_empty() {
640 return Ok(None);
641 }
642 let Some(git_dir) = odb.config_git_dir() else {
643 return Ok(None);
644 };
645 let repo = match Repository::open(git_dir, &RepositoryOptions::empty()) {
646 Ok(r) => r,
647 Err(_) => return Ok(None),
648 };
649 let Some(index) = BitmapIndex::open(&repo).ok().flatten() else {
650 return Ok(None);
651 };
652 let index = Arc::new(index);
653 match index.reachability(
654 &repo,
655 ReachabilityQuery {
656 wants: haves,
657 haves: &[],
658 filter: None,
659 },
660 MissingAction::Allow,
661 ) {
662 Ok(set) => Ok(Some(set)),
663 Err(BitmapWalkError::Unsupported(_)) => Ok(None),
664 Err(BitmapWalkError::Bitmap(_)) => Ok(None),
665 Err(BitmapWalkError::Repository(e)) => Err(e),
666 }
667}
668
669fn rev_list_filtered_objects(
671 odb: &Odb,
672 wants: &[ObjectId],
673 haves: &[ObjectId],
674 filter: &ObjectFilter,
675 have_commits: &HashSet<ObjectId>,
676) -> Result<(Vec<ObjectId>, HashMap<ObjectId, u32>)> {
677 let _ = have_commits;
678 let Some(git_dir) = odb.config_git_dir() else {
679 return Ok((Vec::new(), HashMap::new()));
680 };
681 let repo = Repository::open(git_dir, &RepositoryOptions::empty())?;
682 let wants: Vec<String> = wants.iter().map(ObjectId::to_hex).collect();
683 let haves: Vec<String> = haves.iter().map(ObjectId::to_hex).collect();
684 let opts = crate::rev_list::RevListOptions {
685 objects: true,
686 no_object_names: true,
687 quiet: true,
688 filter: Some(filter.clone()),
689 ..Default::default()
690 };
691 let result = crate::rev_list::rev_list(&repo, &wants, &haves, &opts)?;
692 let mut ordered = Vec::new();
693 let mut seen = HashSet::new();
694 for oid in result
695 .commits
696 .iter()
697 .chain(result.objects.iter().map(|(o, _)| o))
698 {
699 if seen.insert(*oid) {
700 ordered.push(*oid);
701 }
702 }
703 Ok((ordered, HashMap::new()))
704}
705
706#[allow(clippy::type_complexity, clippy::too_many_arguments)]
725fn collect_reachable_excluding_with_name_hashes(
726 odb: &Odb,
727 roots: &[ObjectId],
728 exclude: &HashSet<ObjectId>,
729 boundary: &mut HashSet<ObjectId>,
730 have_commits: &HashSet<ObjectId>,
731 peer_odb: Option<&Odb>,
732 skip_missing: bool,
733 shallow_grafts: &HashSet<ObjectId>,
734 filter: Option<&ObjectFilter>,
735 name_trees: NameTrees,
736) -> Result<(Vec<ObjectId>, HashMap<ObjectId, u32>, HashSet<ObjectId>)> {
737 let oid_len = odb.hash_algo().len();
738 let mut walk = EnumerationWalk {
739 odb,
740 exclude,
741 boundary,
742 have_commits,
743 peer_odb,
744 skip_missing,
745 filter,
746 depth_sensitive: filter.is_some_and(ObjectFilter::is_depth_sensitive),
747 name_trees,
748 visited: oid_hash_set(),
749 tree_depths: oid_hash_map(),
750 unlisted: oid_hash_set(),
751 ordered: Vec::new(),
752 commits: BinaryHeap::new(),
753 next_seq: 0,
754 queue: VecDeque::new(),
755 name_hashes: HashMap::new(),
756 edges: HashSet::new(),
757 };
758 let mut dropped_root_trees: Option<OidHashSet> = filter
760 .filter(|f| !f.includes_tree(0))
761 .map(|_| oid_hash_set());
762
763 let mut path_less_roots = Vec::new();
764 for &root in roots {
765 match odb.read_info(&root) {
766 Ok(info) if matches!(info.kind, ObjectKind::Tree | ObjectKind::Blob) => {
767 path_less_roots.push(root);
768 }
769 _ => walk.push_root(root)?,
770 }
771 }
772 let mut path_less_roots = path_less_roots.into_iter();
773
774 loop {
775 if let Some(commit) = walk.commits.pop() {
776 if !shallow_grafts.contains(&commit.oid) {
777 for parent in commit.parents {
778 walk.push_parent(parent)?;
779 }
780 }
781 if let Some(dropped) = dropped_root_trees.as_mut() {
782 dropped.insert(commit.tree);
783 }
784 walk.push_tree(commit.tree, String::new(), 0);
785 continue;
786 }
787 let Some((oid, path, depth)) = walk.queue.pop_front() else {
788 match path_less_roots.next() {
789 Some(root) => {
790 walk.push_object(root, String::new());
791 continue;
792 }
793 None => break,
794 }
795 };
796 let obj = match odb.read(&oid) {
797 Ok(o) => o,
798 Err(_) if skip_missing => continue,
799 Err(e) => return Err(e),
800 };
801 match obj.kind {
802 ObjectKind::Commit => {
803 walk.queue_commit(oid, obj)?;
804 continue;
805 }
806 ObjectKind::Tree => {
807 for entry in TreeRefIter::with_oid_len(&obj.data, oid_len) {
808 let entry = entry?;
809 if entry.mode == 0o160000 {
810 continue;
811 }
812 match entry.mode & 0o170000 {
819 0o120000 => walk.push_blob(entry.oid, None, depth)?,
820 0o100000 => {
821 walk.push_blob(entry.oid, Some((&path, entry.name)), depth)?;
822 }
823 file_type => {
824 let child_path = child_path(&path, entry.name);
825 if file_type == 0o040000 {
826 walk.push_tree(entry.oid, child_path, depth + 1);
827 } else {
828 walk.push_object(entry.oid, child_path);
829 }
830 }
831 }
832 }
833 }
834 ObjectKind::Tag => {
835 let tag = parse_tag(&obj.data)?;
836 walk.push_object(tag.object, path);
837 }
838 ObjectKind::Blob => {}
839 }
840 }
841
842 if let Some(dropped) = dropped_root_trees.filter(|d| !d.is_empty()) {
843 walk.ordered.retain(|oid| !dropped.contains(oid));
844 }
845 Ok((walk.ordered, walk.name_hashes, walk.edges))
846}
847
848fn child_path(parent: &str, name: &[u8]) -> String {
849 let name = String::from_utf8_lossy(name);
850 if parent.is_empty() {
851 name.into_owned()
852 } else {
853 format!("{parent}/{name}")
854 }
855}
856
857struct EnumerationWalk<'a> {
859 odb: &'a Odb,
860 exclude: &'a HashSet<ObjectId>,
861 boundary: &'a mut HashSet<ObjectId>,
863 have_commits: &'a HashSet<ObjectId>,
865 peer_odb: Option<&'a Odb>,
867 skip_missing: bool,
868 filter: Option<&'a ObjectFilter>,
869 depth_sensitive: bool,
871 name_trees: NameTrees,
872 visited: OidHashSet,
876 tree_depths: OidHashMap<u64>,
878 unlisted: OidHashSet,
881 ordered: Vec<ObjectId>,
882 commits: BinaryHeap<QueuedCommit>,
884 next_seq: u64,
885 queue: VecDeque<(ObjectId, String, u64)>,
888 name_hashes: HashMap<ObjectId, u32>,
889 edges: HashSet<ObjectId>,
891}
892
893struct QueuedCommit {
896 generation: u64,
898 time: i64,
899 seq: std::cmp::Reverse<u64>,
900 oid: ObjectId,
901 tree: ObjectId,
902 parents: Vec<ObjectId>,
903}
904
905impl PartialEq for QueuedCommit {
906 fn eq(&self, other: &Self) -> bool {
907 (self.generation, self.time, self.seq) == (other.generation, other.time, other.seq)
908 }
909}
910
911impl Eq for QueuedCommit {}
912
913impl PartialOrd for QueuedCommit {
914 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
915 Some(self.cmp(other))
916 }
917}
918
919impl Ord for QueuedCommit {
920 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
921 (self.generation, self.time, self.seq).cmp(&(other.generation, other.time, other.seq))
922 }
923}
924
925impl<'a> EnumerationWalk<'a> {
926 fn list(&mut self, oid: ObjectId) {
927 if !self.exclude.contains(&oid) {
928 self.ordered.push(oid);
929 }
930 }
931
932 fn note_boundary(&mut self, oid: ObjectId) {
933 self.visited.insert(oid);
934 }
935
936 fn at_object_boundary(&self, oid: ObjectId) -> bool {
937 self.boundary.contains(&oid)
938 || self
939 .peer_odb
940 .is_some_and(|peer| peer.exists_materialized_local_without_rescan(&oid))
941 }
942
943 fn seal_have_reachable_commit(&mut self, oid: ObjectId) -> Result<()> {
944 if !self.visited.insert(oid) {
945 return Ok(());
946 }
947 self.edges.insert(oid);
948 self.boundary
949 .extend(objects_in_commit_snapshot(self.odb, oid)?);
950 Ok(())
951 }
952
953 fn commit_on_have_side(&mut self, oid: ObjectId) -> Result<bool> {
954 Ok(self.have_commits.contains(&oid))
955 }
956
957 fn push_object(&mut self, oid: ObjectId, path: String) {
959 if let Ok(true) = self.commit_on_have_side(oid) {
960 let _ = self.seal_have_reachable_commit(oid);
961 return;
962 }
963 if self.at_object_boundary(oid) {
964 self.note_boundary(oid);
965 return;
966 }
967 if self.visited.insert(oid) {
968 self.list(oid);
969 self.queue.push_back((oid, path, 0));
970 } else if self.unlisted.remove(&oid) {
971 self.list(oid);
972 }
973 }
974
975 fn push_root(&mut self, oid: ObjectId) -> Result<()> {
978 if self.commit_on_have_side(oid)? {
979 return self.seal_have_reachable_commit(oid);
980 }
981 if self.at_object_boundary(oid) {
982 self.note_boundary(oid);
983 return Ok(());
984 }
985 if !self.visited.insert(oid) {
986 if self.unlisted.remove(&oid) {
987 self.list(oid);
988 }
989 return Ok(());
990 }
991 self.list(oid);
992 match self.read(&oid)? {
993 Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
994 Some(_) => {
995 self.queue.push_back((oid, String::new(), 0));
996 Ok(())
997 }
998 None => Ok(()),
999 }
1000 }
1001
1002 fn push_parent(&mut self, oid: ObjectId) -> Result<()> {
1005 if self.commit_on_have_side(oid)? {
1006 return self.seal_have_reachable_commit(oid);
1007 }
1008 if self.at_object_boundary(oid) {
1009 self.note_boundary(oid);
1010 return Ok(());
1011 }
1012 let listed = self
1013 .filter
1014 .is_none_or(|f| f.includes_commit_or_tag_object(ObjectKind::Commit));
1015 if !self.visited.insert(oid) {
1016 if listed && self.unlisted.remove(&oid) {
1017 self.list(oid);
1018 }
1019 return Ok(());
1020 }
1021 if listed {
1022 self.list(oid);
1023 } else {
1024 self.unlisted.insert(oid);
1025 }
1026 match self.read(&oid)? {
1027 Some(obj) if obj.kind == ObjectKind::Commit => self.queue_commit(oid, obj),
1028 Some(_) => {
1029 self.queue.push_back((oid, String::new(), 0));
1030 Ok(())
1031 }
1032 None => Ok(()),
1033 }
1034 }
1035
1036 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
1037 match self.odb.read(oid) {
1038 Ok(obj) => Ok(Some(obj)),
1039 Err(_) if self.skip_missing => Ok(None),
1040 Err(e) => Err(e),
1041 }
1042 }
1043
1044 fn queue_commit(&mut self, oid: ObjectId, obj: Object) -> Result<()> {
1046 let parsed = CommitRef::parse(&obj.data)?;
1047 let queued = QueuedCommit {
1048 generation: UNRANKED_GENERATION,
1049 time: parsed.committer_unix_seconds_for_ordering(),
1050 seq: std::cmp::Reverse(self.next_seq),
1051 oid,
1052 tree: parsed.tree(),
1053 parents: parsed.parents().to_vec(),
1054 };
1055 self.next_seq += 1;
1056 self.commits.push(queued);
1057 Ok(())
1058 }
1059
1060 fn push_tree(&mut self, oid: ObjectId, path: String, depth: u64) {
1062 if self.name_trees == NameTrees::Yes && !path.is_empty() && !self.visited.contains(&oid) {
1063 self.name_hashes
1064 .entry(oid)
1065 .or_insert_with(|| pack_name_hash(&path));
1066 }
1067 let Some(filter) = self.filter else {
1068 self.push_object(oid, path);
1069 return;
1070 };
1071 if self.at_object_boundary(oid) {
1073 self.note_boundary(oid);
1074 return;
1075 }
1076 let first_walk = if self.depth_sensitive {
1077 match self.tree_depths.get(&oid) {
1078 Some(&walked_at) if walked_at <= depth => false,
1079 _ => {
1080 self.tree_depths.insert(oid, depth);
1081 true
1082 }
1083 }
1084 } else {
1085 self.visited.insert(oid)
1086 };
1087 if !first_walk {
1088 return;
1089 }
1090 if filter.includes_tree(depth) {
1091 if !self.depth_sensitive || self.visited.insert(oid) {
1092 self.list(oid);
1093 }
1094 } else if !self.depth_sensitive {
1095 self.unlisted.insert(oid);
1096 }
1097 if !filter.prunes_tree_entries(depth) {
1098 self.queue.push_back((oid, path, depth));
1099 }
1100 }
1101
1102 fn push_blob(
1105 &mut self,
1106 oid: ObjectId,
1107 name: Option<(&str, &[u8])>,
1108 tree_depth: u64,
1109 ) -> Result<()> {
1110 if self.at_object_boundary(oid) {
1111 self.note_boundary(oid);
1112 return Ok(());
1113 }
1114 if self.visited.contains(&oid) {
1115 return Ok(());
1116 }
1117 let (passes, size_checked) = match self.filter {
1118 None => (true, false),
1119 Some(filter) => self.blob_passes(filter, &oid, tree_depth)?,
1120 };
1121 if passes {
1122 self.visited.insert(oid);
1123 self.list(oid);
1124 if let Some((parent, entry_name)) = name {
1125 self.name_hashes
1126 .insert(oid, pack_name_hash(&child_path(parent, entry_name)));
1127 }
1128 } else if size_checked && !self.depth_sensitive {
1129 self.visited.insert(oid);
1130 self.unlisted.insert(oid);
1131 }
1132 Ok(())
1133 }
1134
1135 fn blob_passes(
1137 &self,
1138 filter: &ObjectFilter,
1139 oid: &ObjectId,
1140 tree_depth: u64,
1141 ) -> Result<(bool, bool)> {
1142 if !filter.includes_blob_under_tree(0, tree_depth) {
1144 return Ok((false, false));
1145 }
1146 if filter.includes_blob_under_tree(u64::MAX, tree_depth) {
1147 return Ok((true, false));
1148 }
1149 match self.odb.read_info(oid) {
1150 Ok(info) => Ok((filter.includes_blob_under_tree(info.size, tree_depth), true)),
1151 Err(_) if self.skip_missing => Ok((false, true)),
1152 Err(e) => Err(e),
1153 }
1154 }
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159 #![allow(clippy::unwrap_used, clippy::expect_used)]
1160
1161 use super::*;
1162
1163 use crate::repo::{InitOptions, Repository};
1164 use tempfile::TempDir;
1165
1166 fn walk(repo: &Repository, roots: &[ObjectId], name_trees: NameTrees) -> PackObjectSelection {
1167 let empty = HashSet::new();
1168 enumerate_pack_objects_naming(
1169 repo.odb(),
1170 roots,
1171 &[],
1172 &PackEnumerateOptions {
1173 filter: None,
1174 shallow_grafts: &empty,
1175 have_shallow_grafts: Some(&empty),
1176 use_bitmaps: false,
1177 exclude_objects: None,
1178 peer_odb: None,
1179 have_boundary: HaveBoundary::CommitClosure,
1180 },
1181 name_trees,
1182 )
1183 .expect("walk")
1184 }
1185
1186 fn write_tree(repo: &Repository, entries: &[(u32, &str, ObjectId)]) -> ObjectId {
1187 let entries: Vec<crate::objects::TreeEntry> = entries
1188 .iter()
1189 .map(|&(mode, name, oid)| crate::objects::TreeEntry {
1190 mode,
1191 name: name.as_bytes().to_vec(),
1192 oid,
1193 })
1194 .collect();
1195 repo.odb()
1196 .write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
1197 .expect("tree")
1198 }
1199
1200 fn write_commit(
1201 repo: &Repository,
1202 tree: ObjectId,
1203 parents: &[ObjectId],
1204 time: i64,
1205 ) -> ObjectId {
1206 let mut body = format!("tree {}\n", tree.to_hex());
1207 for parent in parents {
1208 body.push_str(&format!("parent {}\n", parent.to_hex()));
1209 }
1210 body.push_str(&format!(
1211 "author A <a@example.com> {time} +0000\ncommitter A <a@example.com> {time} +0000\n\nc{time}\n"
1212 ));
1213 repo.odb()
1214 .write(ObjectKind::Commit, body.as_bytes())
1215 .expect("commit")
1216 }
1217
1218 #[test]
1219 fn tips_only_boundary_matches_closure_on_linear_incremental_push() {
1220 let dir = TempDir::new().unwrap();
1221 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1222 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1223 let mut parent = write_commit(
1224 &repo,
1225 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1226 &[],
1227 0,
1228 );
1229 for i in 1..800 {
1230 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1231 parent = write_commit(&repo, tree, &[parent], i);
1232 }
1233 let remote_tip = parent;
1234 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1235 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1236 let empty = HashSet::new();
1237 let closure_opts = PackEnumerateOptions {
1238 filter: None,
1239 shallow_grafts: &empty,
1240 have_shallow_grafts: Some(&empty),
1241 use_bitmaps: false,
1242 exclude_objects: None,
1243 peer_odb: None,
1244 have_boundary: HaveBoundary::CommitClosure,
1245 };
1246 let tips_opts = PackEnumerateOptions {
1247 have_boundary: HaveBoundary::TipsOnly,
1248 ..closure_opts
1249 };
1250 let closure = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &closure_opts)
1251 .expect("closure");
1252 let tips = enumerate_pack_objects(repo.odb(), &[new_tip], &[remote_tip], &tips_opts)
1253 .expect("tips");
1254 let closure_set: HashSet<ObjectId> = closure.send.iter().copied().collect();
1255 let tips_set: HashSet<ObjectId> = tips.send.iter().copied().collect();
1256 assert_eq!(closure_set, tips_set);
1257 assert!(
1258 tips.send.len() <= 8,
1259 "one new commit should enumerate a handful of objects, got {}",
1260 tips.send.len()
1261 );
1262 }
1263
1264 #[test]
1265 fn tips_only_send_pack_does_not_prewalk_deep_history() {
1266 use crate::merge_base::intersect_walk_test_hooks::{
1267 commit_parent_lookups, reset_commit_parent_lookups,
1268 };
1269
1270 let dir = TempDir::new().unwrap();
1271 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1272 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1273 let mut parent = write_commit(
1274 &repo,
1275 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1276 &[],
1277 0,
1278 );
1279 for i in 1..800 {
1280 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1281 parent = write_commit(&repo, tree, &[parent], i);
1282 }
1283 let remote_tip = parent;
1284 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1285 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1286 let haves = vec![remote_tip];
1287 let empty = HashSet::new();
1288 reset_commit_parent_lookups();
1289 let _selection = enumerate_pack_objects(
1290 repo.odb(),
1291 &[new_tip],
1292 &haves,
1293 &PackEnumerateOptions {
1294 filter: None,
1295 shallow_grafts: &empty,
1296 have_shallow_grafts: Some(&empty),
1297 use_bitmaps: false,
1298 exclude_objects: None,
1299 peer_odb: None,
1300 have_boundary: HaveBoundary::TipsOnly,
1301 },
1302 )
1303 .expect("enumerate");
1304 assert!(
1305 commit_parent_lookups() <= 128,
1306 "send-pack hide must stay coupled to the want walk ({} parent lookups)",
1307 commit_parent_lookups()
1308 );
1309 }
1310
1311 #[test]
1312 fn push_dated_before_graphed_history_stops_at_the_frontier() {
1313 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1314 let dir = TempDir::new().unwrap();
1315 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1316 let tree = write_tree(&repo, &[]);
1317 let mut tip = write_commit(&repo, tree, &[], 1_300_000_000);
1319 let mut tagged = tip;
1320 for i in 1..300 {
1321 tip = write_commit(&repo, tree, &[tip], 1_300_000_000 + i);
1322 if i == 150 {
1323 tagged = tip;
1324 }
1325 }
1326 std::fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n")).unwrap();
1327 repo.odb().write_commit_graph().unwrap();
1328 let mut new_tip = tip;
1331 for i in 0..3 {
1332 new_tip = write_commit(&repo, tree, &[new_tip], 1_000_000_000 + i);
1333 }
1334
1335 reset_commit_reads();
1336 let hidden = send_pack_uninteresting_commits(
1337 repo.odb(),
1338 &[new_tip],
1339 &[tip, tagged],
1340 &HashSet::new(),
1341 )
1342 .unwrap();
1343 assert!(hidden.contains(&tip));
1344 assert!(hidden.contains(&tagged));
1345 assert!(!hidden.contains(&new_tip));
1346 assert_eq!(
1347 commit_reads(),
1348 3,
1349 "only the commits outside the graph are read from the object store"
1350 );
1351 assert!(
1352 hidden.len() <= 4,
1353 "the walk must stop at the frontier, not drain the remote's history ({} marked)",
1354 hidden.len()
1355 );
1356 }
1357
1358 #[test]
1359 fn duplicated_wants_already_on_peer_balance_the_frontier() {
1360 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1361 let dir = TempDir::new().unwrap();
1362 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1363 let tree = write_tree(&repo, &[]);
1364 let mut tip = write_commit(&repo, tree, &[], 0);
1365 for time in 1..100 {
1366 tip = write_commit(&repo, tree, &[tip], time);
1367 }
1368 reset_commit_reads();
1369 let hidden =
1370 send_pack_uninteresting_commits(repo.odb(), &[tip, tip], &[tip, tip], &HashSet::new())
1371 .unwrap();
1372 assert!(hidden.contains(&tip));
1373 assert!(
1374 commit_reads() <= 2,
1375 "shared want/have must stop at the frontier"
1376 );
1377 let empty = HashSet::new();
1378 let selection = enumerate_pack_objects(
1379 repo.odb(),
1380 &[tip, tip],
1381 &[tip, tip],
1382 &PackEnumerateOptions {
1383 filter: None,
1384 shallow_grafts: &empty,
1385 have_shallow_grafts: None,
1386 use_bitmaps: false,
1387 exclude_objects: None,
1388 peer_odb: None,
1389 have_boundary: HaveBoundary::TipsOnly,
1390 },
1391 )
1392 .unwrap();
1393 assert!(selection.send.is_empty());
1394 }
1395
1396 #[test]
1397 fn tips_only_many_advertised_haves_does_not_prewalk_deep_history() {
1398 use crate::merge_base::intersect_walk_test_hooks::{
1399 commit_parent_lookups, reset_commit_parent_lookups,
1400 };
1401
1402 let dir = TempDir::new().unwrap();
1403 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1404 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1405 let mut chain = Vec::new();
1406 let mut parent = write_commit(
1407 &repo,
1408 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1409 &[],
1410 0,
1411 );
1412 chain.push(parent);
1413 for i in 1..800 {
1414 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1415 parent = write_commit(&repo, tree, &[parent], i);
1416 chain.push(parent);
1417 }
1418 let remote_tip = parent;
1419 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1420 let new_tip = write_commit(&repo, new_tree, &[remote_tip], 900);
1421 let mut haves = vec![remote_tip];
1422 for n in (2..400).step_by(2) {
1423 let idx = chain.len().checked_sub(1 + n);
1424 if let Some(idx) = idx {
1425 haves.push(chain[idx]);
1426 }
1427 }
1428 let empty = HashSet::new();
1429 reset_commit_parent_lookups();
1430 let selection = enumerate_pack_objects(
1431 repo.odb(),
1432 &[new_tip],
1433 &haves,
1434 &PackEnumerateOptions {
1435 filter: None,
1436 shallow_grafts: &empty,
1437 have_shallow_grafts: Some(&empty),
1438 use_bitmaps: false,
1439 exclude_objects: None,
1440 peer_odb: None,
1441 have_boundary: HaveBoundary::TipsOnly,
1442 },
1443 )
1444 .expect("enumerate");
1445 assert!(
1446 selection.send.len() <= 8,
1447 "one new commit should stay small with many haves, got {}",
1448 selection.send.len()
1449 );
1450 assert!(
1451 commit_parent_lookups() <= 512,
1452 "many advertised haves must not pre-walk deep history ({} parent lookups)",
1453 commit_parent_lookups()
1454 );
1455 }
1456
1457 #[test]
1458 fn send_pack_have_walk_stops_without_scanning_deep_haves() {
1459 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1460
1461 let dir = TempDir::new().unwrap();
1462 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1463 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1464 let mut parent = write_commit(
1465 &repo,
1466 write_tree(&repo, &[(0o100644, "f", blob("0"))]),
1467 &[],
1468 0,
1469 );
1470 let near_root = parent;
1471 for i in 1..800 {
1472 let tree = write_tree(&repo, &[(0o100644, "f", blob(&i.to_string()))]);
1473 parent = write_commit(&repo, tree, &[parent], i);
1474 }
1475 let direct_parent = parent;
1476 let new_tree = write_tree(&repo, &[(0o100644, "f", blob("new"))]);
1477 let new_tip = write_commit(&repo, new_tree, &[direct_parent], 900);
1478 let empty = HashSet::new();
1479 reset_commit_reads();
1480 let have_commits = send_pack_uninteresting_commits(
1481 repo.odb(),
1482 &[new_tip],
1483 &[direct_parent, near_root],
1484 &empty,
1485 )
1486 .expect("send-pack have walk");
1487 assert!(have_commits.contains(&direct_parent));
1488 assert!(have_commits.contains(&near_root));
1489 let reads = commit_reads();
1490 assert!(
1491 reads < 20,
1492 "send-pack have walk should not scan deep history, read {reads} commits"
1493 );
1494 }
1495
1496 #[test]
1497 fn walk_lists_trees_of_newer_commits_first() {
1498 let dir = TempDir::new().unwrap();
1499 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1500 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1501 let tree_a = write_tree(&repo, &[(0o100644, "f", blob("a"))]);
1502 let tree_b = write_tree(&repo, &[(0o100644, "f", blob("b"))]);
1503 let tree_c = write_tree(&repo, &[(0o100644, "f", blob("c"))]);
1504 let base = write_commit(&repo, tree_a, &[], 100);
1505 let old_tip = write_commit(&repo, tree_b, &[base], 200);
1507 let new_tip = write_commit(&repo, tree_c, &[base], 300);
1508
1509 let selection = walk(&repo, &[old_tip, new_tip], NameTrees::No);
1510 let pos = |oid: ObjectId| selection.send.iter().position(|o| *o == oid).unwrap();
1511 assert!(pos(old_tip) < pos(base) && pos(new_tip) < pos(base));
1512 assert!(pos(tree_c) < pos(tree_b) && pos(tree_b) < pos(tree_a));
1513 }
1514
1515 #[test]
1516 fn walk_names_trees_and_keeps_history_paths_for_path_less_roots() {
1517 let dir = TempDir::new().unwrap();
1518 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
1519 let file = repo.odb().write(ObjectKind::Blob, b"x\n").unwrap();
1520 let link = repo.odb().write(ObjectKind::Blob, b"target").unwrap();
1521 let sub = write_tree(
1522 &repo,
1523 &[(0o100644, "file.rs", file), (0o120000, "link", link)],
1524 );
1525 let root = write_tree(&repo, &[(0o040000, "src", sub)]);
1526 let commit = write_commit(&repo, root, &[], 100);
1527
1528 let selection = walk(&repo, &[sub, file, commit], NameTrees::Yes);
1530 let hash = |oid: ObjectId| selection.name_hashes.get(&oid).copied();
1531 assert_eq!(hash(sub), Some(pack_name_hash("src")));
1532 assert_eq!(hash(file), Some(pack_name_hash("src/file.rs")));
1533 assert_eq!(hash(root), None, "root trees have no path");
1534 assert_eq!(selection.send.len(), 5);
1535
1536 let blobs_only = walk(&repo, &[sub, file, commit], NameTrees::No);
1537 assert_eq!(blobs_only.name_hashes.get(&sub), None);
1538 assert_eq!(
1539 blobs_only.name_hashes.get(&file),
1540 Some(&pack_name_hash("src/file.rs"))
1541 );
1542 }
1543
1544 fn tree_entry(mode: &str, name: &str, oid: ObjectId) -> Vec<u8> {
1545 let mut bytes = format!("{mode} {name}\0").into_bytes();
1546 bytes.extend_from_slice(oid.as_bytes());
1547 bytes
1548 }
1549
1550 #[cfg_attr(
1554 not(feature = "full-tests"),
1555 ignore = "starts system git or is heavy; run with --features full-tests"
1556 )]
1557 #[test]
1558 fn walk_descends_into_trees_with_non_canonical_modes() {
1559 let tmp = tempfile::tempdir().unwrap();
1560 Repository::init(tmp.path(), &InitOptions::new().with_bare(true)).unwrap();
1561 let repo = Repository::open(tmp.path(), &RepositoryOptions::empty()).unwrap();
1562 let odb = repo.odb();
1563 let deep = odb
1564 .write_loose_materialize(ObjectKind::Blob, b"deep\n")
1565 .unwrap();
1566 let odd = odb
1567 .write_loose_materialize(ObjectKind::Blob, b"odd\n")
1568 .unwrap();
1569 let inner = odb
1570 .write_loose_materialize(ObjectKind::Tree, &tree_entry("100644", "deep.txt", deep))
1571 .unwrap();
1572 let mut root = tree_entry("100664", "odd.txt", odd);
1573 root.extend(tree_entry("40755", "sub", inner));
1574 let root = odb
1575 .write_loose_materialize(ObjectKind::Tree, &root)
1576 .unwrap();
1577 let commit = format!(
1578 "tree {root}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nodd modes\n"
1579 );
1580 let commit = odb
1581 .write_loose_materialize(ObjectKind::Commit, commit.as_bytes())
1582 .unwrap();
1583
1584 let empty = HashSet::new();
1585 let selection = enumerate_pack_objects(
1586 odb,
1587 &[commit],
1588 &[],
1589 &PackEnumerateOptions {
1590 filter: None,
1591 shallow_grafts: &empty,
1592 have_shallow_grafts: Some(&empty),
1593 use_bitmaps: false,
1594 exclude_objects: None,
1595 peer_odb: None,
1596 have_boundary: HaveBoundary::CommitClosure,
1597 },
1598 )
1599 .unwrap();
1600
1601 let ours: HashSet<ObjectId> = selection.send.iter().copied().collect();
1602 let git: HashSet<ObjectId> =
1603 grit_test_support::git(tmp.path(), &["rev-list", "--objects", &commit.to_hex()])
1604 .lines()
1605 .map(|line| ObjectId::from_hex(&line[..40]).unwrap())
1606 .collect();
1607 assert_eq!(ours, git, "same objects as git rev-list --objects");
1608 assert!(ours.contains(&inner) && ours.contains(&deep));
1609 assert_eq!(selection.send.len(), 5);
1610 assert_eq!(
1611 selection.name_hashes.get(&deep),
1612 Some(&pack_name_hash("sub/deep.txt"))
1613 );
1614 assert_eq!(
1615 selection.name_hashes.get(&odd),
1616 Some(&pack_name_hash("odd.txt"))
1617 );
1618 }
1619
1620 fn tips_only_options(empty: &HashSet<ObjectId>) -> PackEnumerateOptions<'_> {
1621 PackEnumerateOptions {
1622 filter: None,
1623 shallow_grafts: empty,
1624 have_shallow_grafts: Some(empty),
1625 use_bitmaps: false,
1626 exclude_objects: None,
1627 peer_odb: None,
1628 have_boundary: HaveBoundary::TipsOnly,
1629 }
1630 }
1631
1632 fn write_tag(repo: &Repository, target: ObjectId, kind: &str, name: &str) -> ObjectId {
1633 let body = format!(
1634 "object {}\ntype {kind}\ntag {name}\ntagger A <a@example.com> 0 +0000\n\n{name}\n",
1635 target.to_hex()
1636 );
1637 repo.odb().write(ObjectKind::Tag, body.as_bytes()).unwrap()
1638 }
1639
1640 #[test]
1641 fn tips_only_thin_bases_come_from_edge_commits_not_every_advertised_tip() {
1642 let dir = TempDir::new().unwrap();
1643 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1644 let blob = |s: &str| repo.odb().write(ObjectKind::Blob, s.as_bytes()).unwrap();
1645 let base_tree = write_tree(&repo, &[(0o100644, "f", blob("base"))]);
1646 let root = write_commit(&repo, base_tree, &[], 0);
1647 let main = write_commit(
1648 &repo,
1649 write_tree(&repo, &[(0o100644, "f", blob("main"))]),
1650 &[root],
1651 10,
1652 );
1653 let mut haves = vec![main];
1654 for i in 0..40 {
1655 let tree = write_tree(&repo, &[(0o100644, "f", blob(&format!("side {i}")))]);
1656 let side = write_commit(&repo, tree, &[root], 20 + i);
1657 haves.push(write_tag(&repo, side, "commit", &format!("t{i}")));
1658 haves.push(side);
1659 }
1660 let new_tip = write_commit(
1661 &repo,
1662 write_tree(&repo, &[(0o100644, "f", blob("main\nmore"))]),
1663 &[main],
1664 100,
1665 );
1666 let empty = HashSet::new();
1667 let selection =
1668 enumerate_pack_objects(repo.odb(), &[new_tip], &haves, &tips_only_options(&empty))
1669 .unwrap();
1670 assert_eq!(selection.send.len(), 3, "commit, tree and the changed blob");
1671 assert_eq!(
1672 selection.have_closure,
1673 HashSet::from([main]),
1674 "thin bases come from the commit the walk stopped at"
1675 );
1676
1677 let closure = enumerate_pack_objects(
1678 repo.odb(),
1679 &[new_tip],
1680 &haves,
1681 &PackEnumerateOptions {
1682 have_boundary: HaveBoundary::CommitClosure,
1683 ..tips_only_options(&empty)
1684 },
1685 )
1686 .unwrap();
1687 assert_eq!(closure.send, selection.send);
1688 assert_eq!(
1689 closure.have_closure,
1690 HashSet::from([main]),
1691 "the full have pre-walk seeds thin bases from the same edge commit"
1692 );
1693 }
1694
1695 #[test]
1696 fn send_pack_walk_peels_tag_haves_and_ignores_non_commit_targets() {
1697 use super::send_pack_walk_test_hooks::{commit_reads, reset_commit_reads};
1698 let dir = TempDir::new().unwrap();
1699 let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
1700 let blob = repo.odb().write(ObjectKind::Blob, b"x").unwrap();
1701 let tree = write_tree(&repo, &[(0o100644, "f", blob)]);
1702 let old = write_commit(&repo, tree, &[], 0);
1703 let tip = write_commit(&repo, tree, &[old], 1);
1704 let tag = write_tag(&repo, tip, "commit", "v1");
1705 let nested = write_tag(&repo, tag, "tag", "v1-nested");
1706 let tree_tag = write_tag(&repo, tree, "tree", "tree-tag");
1707 let blob_tag = write_tag(&repo, blob, "blob", "blob-tag");
1708 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
1709 let new_tip = write_commit(&repo, tree, &[tip], 2);
1710
1711 reset_commit_reads();
1712 let hidden = send_pack_uninteresting_commits(
1713 repo.odb(),
1714 &[new_tip],
1715 &[nested, tag, tree_tag, blob_tag, missing],
1716 &HashSet::new(),
1717 )
1718 .unwrap();
1719 assert!(hidden.contains(&tip), "tags peel to the commit they name");
1720 for oid in [
1721 tag, nested, tree_tag, blob_tag, tree, blob, missing, new_tip,
1722 ] {
1723 assert!(!hidden.contains(&oid), "{oid} is not a have-side commit");
1724 }
1725 assert_eq!(
1726 commit_reads(),
1727 2,
1728 "the want and the tag-named commit, each queued once"
1729 );
1730 }
1731}