1use std::cmp::Ordering;
8use std::collections::{BTreeSet, BinaryHeap, HashSet, VecDeque};
9
10use crate::error::{Error, Result};
11use crate::ewah_bitmap::Bitmap;
12use crate::log_walk::WalkedCommits;
13use crate::objects::{
14 oid_hash_map, oid_hash_set, oid_hash_set_from, CommitRef, ObjectId, ObjectKind, OidHashMap,
15 OidHashSet,
16};
17use crate::pack_bitmap::BitmapIndex;
18use crate::promisor::{read_promisor_missing_oids, repo_treats_promisor_packs};
19use crate::reflog::read_reflog;
20use crate::repo::Repository;
21use crate::rev_parse::{
22 load_graft_parents, peel_to_commit_for_merge_base, resolve_revision,
23 resolve_upstream_symbolic_name, try_peel_to_commit_for_merge_base, upstream_suffix_info,
24};
25use crate::shallow::load_shallow_boundaries;
26
27pub fn resolve_commit_specs(repo: &Repository, specs: &[String]) -> Result<Vec<ObjectId>> {
39 let mut out = Vec::with_capacity(specs.len());
40 for spec in specs {
41 let oid = resolve_revision(repo, spec)?;
42 ensure_is_commit(repo, oid)?;
43 out.push(oid);
44 }
45 Ok(out)
46}
47
48pub fn merge_bases_first_vs_rest(
63 repo: &Repository,
64 first: ObjectId,
65 others: &[ObjectId],
66) -> Result<Vec<ObjectId>> {
67 if others.is_empty() {
68 return Ok(vec![first]);
69 }
70 for &other in others {
71 if first == other {
72 return Ok(vec![first]);
73 }
74 }
75 if others.len() == 1 {
76 let other = others[0];
77 let mut cache = CommitGraphCache::new(repo);
78 match direct_ancestor_relation(&mut cache, first, other)? {
79 FirstParentRelation::Same | FirstParentRelation::FirstIsAncestor => {
80 return Ok(vec![first]);
81 }
82 FirstParentRelation::SecondIsAncestor => return Ok(vec![other]),
83 FirstParentRelation::Unrelated => {}
84 }
85 let mut selected = walk_intersecting_histories(&mut cache, first, others, false)?;
86 if selected.is_empty() {
87 return Ok(Vec::new());
88 }
89 sort_merge_bases_for_git_output(&mut cache, &mut selected)?;
90 if selected.len() > 1 {
91 selected.truncate(1);
92 }
93 return Ok(selected);
94 }
95 let mut cache = CommitGraphCache::new(repo);
96 let first_anc = cache.ancestor_closure(first)?;
97 let mut others_union = oid_hash_set();
98 for &other in others {
99 others_union.extend(cache.ancestor_closure(other)?);
100 }
101 let candidates: OidHashSet = first_anc.intersection(&others_union).copied().collect();
102 let all_minimal = reduce_to_best(candidates, &mut cache)?;
103 if all_minimal.len() <= 1 {
104 return Ok(all_minimal);
105 }
106
107 let mut selected = walk_intersecting_histories(&mut cache, first, others, false)?;
108 if selected.is_empty() {
109 selected = all_minimal;
110 }
111 sort_merge_bases_for_git_output(&mut cache, &mut selected)?;
112 if selected.len() > 1 {
113 selected.truncate(1);
114 }
115 Ok(selected)
116}
117
118pub fn merge_base_for_diff_index(
122 repo: &Repository,
123 head: ObjectId,
124 other: ObjectId,
125) -> std::result::Result<ObjectId, MergeBaseForDiffError> {
126 let bases = merge_bases_first_vs_rest(repo, other, &[head])
127 .map_err(|e| MergeBaseForDiffError::Other(e.to_string()))?;
128 match bases.len() {
129 0 => Err(MergeBaseForDiffError::None),
130 1 => Ok(bases[0]),
131 _ => Err(MergeBaseForDiffError::Multiple),
132 }
133}
134
135pub fn merge_base_for_diff_two_commits(
139 repo: &Repository,
140 a: ObjectId,
141 b: ObjectId,
142) -> std::result::Result<ObjectId, MergeBaseForDiffError> {
143 let bases = merge_bases_first_vs_rest(repo, a, &[b])
144 .map_err(|e| MergeBaseForDiffError::Other(e.to_string()))?;
145 match bases.len() {
146 0 => Err(MergeBaseForDiffError::None),
147 1 => Ok(bases[0]),
148 _ => Err(MergeBaseForDiffError::Multiple),
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub enum MergeBaseForDiffError {
155 None,
157 Multiple,
159 Other(String),
161}
162
163pub fn merge_bases_octopus(repo: &Repository, commits: &[ObjectId]) -> Result<Vec<ObjectId>> {
174 let mut cache = CommitGraphCache::new(repo);
175 let mut iter = commits.iter();
176 let Some(&first) = iter.next() else {
177 return Ok(Vec::new());
178 };
179 let mut common = cache.ancestor_closure(first)?;
180 for &oid in iter {
181 let set = cache.ancestor_closure(oid)?;
182 common.retain(|item| set.contains(item));
183 }
184 let mut bases = reduce_to_best(common, &mut cache)?;
185 sort_merge_bases_for_git_output(&mut cache, &mut bases)?;
186 Ok(bases)
187}
188
189pub fn merge_bases_all(repo: &Repository, commits: &[ObjectId]) -> Result<Vec<ObjectId>> {
198 if commits.is_empty() {
199 return Ok(Vec::new());
200 }
201 if commits.len() == 1 {
202 return Ok(vec![commits[0]]);
203 }
204 if commits.len() == 2 {
205 let mut cache = CommitGraphCache::new(repo);
206 match direct_ancestor_relation(&mut cache, commits[0], commits[1])? {
209 FirstParentRelation::Same | FirstParentRelation::FirstIsAncestor => {
210 return Ok(vec![commits[0]]);
211 }
212 FirstParentRelation::SecondIsAncestor => return Ok(vec![commits[1]]),
213 FirstParentRelation::Unrelated => {}
214 }
215 return all_merge_bases_vs_union(&mut cache, commits[0], &commits[1..2]);
216 }
217 merge_bases_octopus(repo, commits)
218}
219
220pub(crate) fn merge_bases_all_vs_union(
227 repo: &Repository,
228 one: ObjectId,
229 others: &[ObjectId],
230) -> Result<Vec<ObjectId>> {
231 all_merge_bases_vs_union(&mut CommitGraphCache::new(repo), one, others)
232}
233
234fn all_merge_bases_vs_union(
235 cache: &mut CommitGraphCache<'_>,
236 one: ObjectId,
237 others: &[ObjectId],
238) -> Result<Vec<ObjectId>> {
239 let mut walked = walk_intersecting_histories(cache, one, others, true)?;
242 if walked.len() > 1 {
243 let candidates: OidHashSet = walked.iter().copied().collect();
244 let best: OidHashSet = reduce_to_best(candidates, cache)?.into_iter().collect();
245 walked.retain(|oid| best.contains(oid));
246 }
247 Ok(walked)
248}
249
250pub fn is_ancestor(repo: &Repository, ancestor: ObjectId, descendant: ObjectId) -> Result<bool> {
256 if ancestor == descendant {
257 return Ok(true);
258 }
259 let mut cache = CommitGraphCache::new(repo);
260 cache.is_ancestor(ancestor, descendant)
261}
262
263pub(crate) fn is_ancestor_unless_rewound(
277 repo: &Repository,
278 ancestor: ObjectId,
279 descendant: ObjectId,
280) -> Result<bool> {
281 if ancestor == descendant {
282 return Ok(true);
283 }
284 let mut cache = CommitGraphCache::new(repo);
285 if !cache.has_parent_rewrites()?
286 && first_parent_chain_reaches_newer(&mut cache, ancestor, descendant).unwrap_or(false)
287 {
288 return Ok(false);
289 }
290 cache.is_ancestor(ancestor, descendant)
291}
292
293fn first_parent_chain_reaches_newer(
296 cache: &mut CommitGraphCache<'_>,
297 from: ObjectId,
298 target: ObjectId,
299) -> Result<bool> {
300 let target_time = cache.commit_time(target)?;
301 let mut cur = from;
302 while cache.commit_time(cur)? >= target_time {
303 let Some(&parent) = cache.parents_of(cur)?.first() else {
304 return Ok(false);
305 };
306 if parent == target {
307 return Ok(true);
308 }
309 cur = parent;
310 }
311 Ok(false)
312}
313
314pub(crate) fn wants_covered_by_haves(
339 repo: &Repository,
340 wants: &[ObjectId],
341 have_commits: &[ObjectId],
342) -> Result<bool> {
343 if have_commits.is_empty() {
344 return Ok(false);
345 }
346 let mut cache = CommitGraphCache::new(repo);
347 let mut covered = oid_hash_set();
349 let mut oldest_have = i64::MAX;
350 for &have in have_commits {
351 covered.insert(have);
352 covered.extend(cache.parents_of(have)?);
353 oldest_have = oldest_have.min(cache.commit_time(have)?);
354 }
355 let mut explored = oid_hash_set();
357 for &want in wants {
358 if covered.contains(&want) {
359 continue;
360 }
361 if !explored.insert(want) {
362 return Ok(false);
363 }
364 let mut path = vec![(want, cache.parents_of(want)?, 0usize)];
365 let mut reached = false;
366 while let Some((_, parents, next)) = path.last_mut() {
367 let Some(&parent) = parents.get(*next) else {
368 path.pop();
369 continue;
370 };
371 *next += 1;
372 if covered.contains(&parent) {
373 reached = true;
374 break;
375 }
376 if cache.commit_time(parent)? < oldest_have || !explored.insert(parent) {
377 continue;
378 }
379 let grandparents = cache.parents_of(parent)?;
380 path.push((parent, grandparents, 0));
381 }
382 if !reached {
383 return Ok(false);
384 }
385 covered.extend(path.into_iter().map(|(oid, _, _)| oid));
386 }
387 Ok(true)
388}
389
390pub fn is_ancestor_odb_walk(
420 odb: &crate::odb::Odb,
421 ancestor: ObjectId,
422 descendant: ObjectId,
423) -> Result<bool> {
424 is_ancestor_odb_walk_with_graph(odb, None, ancestor, descendant)
425}
426
427pub(crate) fn is_ancestor_odb_walk_with_graph(
437 odb: &crate::odb::Odb,
438 graph: Option<&crate::commit_graph_file::CommitGraphChain>,
439 ancestor: ObjectId,
440 descendant: ObjectId,
441) -> Result<bool> {
442 if ancestor == descendant {
443 return Ok(true);
444 }
445 let Some(ancestor_linkage) = read_commit_linkage(odb, graph, &ancestor)? else {
446 return Ok(false);
447 };
448 let Some(descendant_linkage) = read_commit_linkage(odb, graph, &descendant)? else {
449 return Ok(false);
450 };
451 let ancestor_generation = graph
452 .map(|g| g.commit_generation_for_walk(&ancestor))
453 .filter(|&generation| generation != UNRANKED_COMMIT);
454 let cannot_reach_ancestor = |oid: &ObjectId| {
455 let (Some(graph), Some(limit)) = (graph, ancestor_generation) else {
456 return false;
457 };
458 let generation = graph.commit_generation_for_walk(oid);
459 generation != UNRANKED_COMMIT && generation <= limit
460 };
461
462 const FROM_ANCESTOR: u8 = 1;
470 const FROM_DESCENDANT: u8 = 2;
471 let mut paint: OidHashMap<u8> = oid_hash_map();
472 let mut parents_of: OidHashMap<Vec<ObjectId>> = oid_hash_map();
473 let mut times: OidHashMap<i64> = oid_hash_map();
474 let mut descendant_only = oid_hash_set();
475 let rank =
480 |oid: &ObjectId| graph.map_or(UNRANKED_COMMIT, |g| g.commit_generation_for_walk(oid));
481 let mut heap = BinaryHeap::new();
482 for (oid, color, (time, parents)) in [
483 (ancestor, FROM_ANCESTOR, ancestor_linkage),
484 (descendant, FROM_DESCENDANT, descendant_linkage),
485 ] {
486 paint.insert(oid, color);
487 parents_of.insert(oid, parents);
488 times.insert(oid, time);
489 heap.push((rank(&oid), time, oid));
490 }
491 descendant_only.insert(descendant);
492
493 while !descendant_only.is_empty() {
494 let Some((_, _, oid)) = heap.pop() else {
495 break;
496 };
497 descendant_only.remove(&oid);
498 let color = paint.get(&oid).copied().unwrap_or_default();
499 let parents = parents_of.get(&oid).cloned().unwrap_or_default();
500 for parent in parents {
501 let old_color = paint.get(&parent).copied().unwrap_or_default();
502 let new_color = old_color | color;
503 if new_color == old_color {
504 continue;
505 }
506 if parent == ancestor {
507 return Ok(true);
508 }
509 if new_color == FROM_DESCENDANT && cannot_reach_ancestor(&parent) {
510 continue;
511 }
512 let time = match times.get(&parent) {
513 Some(&time) => time,
514 None => {
515 let linkage = match read_commit_linkage(odb, graph, &parent) {
516 Err(Error::ObjectNotFound(_)) => None,
517 other => other?,
518 };
519 let Some((time, grandparents)) = linkage else {
520 continue;
521 };
522 times.insert(parent, time);
523 parents_of.insert(parent, grandparents);
524 time
525 }
526 };
527 paint.insert(parent, new_color);
528 if new_color == FROM_DESCENDANT {
529 descendant_only.insert(parent);
530 } else {
531 descendant_only.remove(&parent);
532 }
533 heap.push((rank(&parent), time, parent));
534 }
535 }
536 Ok(false)
537}
538
539fn read_commit_linkage(
543 odb: &crate::odb::Odb,
544 graph: Option<&crate::commit_graph_file::CommitGraphChain>,
545 oid: &ObjectId,
546) -> Result<Option<(i64, Vec<ObjectId>)>> {
547 if let Some((parents, time)) = graph.and_then(|g| g.graph_commit(oid)) {
548 return Ok(Some((time, parents)));
549 }
550 let object = odb.read(oid)?;
551 if object.kind != ObjectKind::Commit {
552 return Ok(None);
553 }
554 let commit = CommitRef::parse(&object.data)?;
555 Ok(Some((
556 commit.committer_timestamp(),
557 commit.parents().to_vec(),
558 )))
559}
560
561pub fn resolve_fork_point_reflog_ref(repo: &Repository, spec: &str) -> String {
568 if spec == "HEAD" || spec.starts_with("refs/") {
569 return spec.to_string();
570 }
571
572 let logs_dir = repo.git_dir().join("logs");
573 let candidates = [
574 spec.to_string(),
575 format!("refs/heads/{spec}"),
576 format!("refs/remotes/{spec}"),
577 ];
578
579 for candidate in candidates {
580 if logs_dir.join(&candidate).is_file() {
581 return candidate;
582 }
583 }
584
585 format!("refs/heads/{spec}")
586}
587
588fn select_best_fork_point(repo: &Repository, candidates: &[ObjectId]) -> Result<Option<ObjectId>> {
590 if candidates.is_empty() {
591 return Ok(None);
592 }
593
594 let mut best = oid_hash_set();
595 for &candidate in candidates {
596 let mut dominated = false;
597 for &other in candidates {
598 if candidate == other {
599 continue;
600 }
601 if is_ancestor(repo, candidate, other)? {
602 dominated = true;
603 break;
604 }
605 }
606 if !dominated {
607 best.insert(candidate);
608 }
609 }
610
611 Ok(candidates.iter().copied().find(|oid| best.contains(oid)))
612}
613
614pub fn fork_point(
630 repo: &Repository,
631 upstream_spec: &str,
632 upstream_tip: ObjectId,
633 head: ObjectId,
634) -> Result<ObjectId> {
635 let reflog_ref = if upstream_suffix_info(upstream_spec).is_some() {
636 resolve_upstream_symbolic_name(repo, upstream_spec)?
637 } else {
638 resolve_fork_point_reflog_ref(repo, upstream_spec)
639 };
640
641 let entries = read_reflog(repo.git_dir(), &reflog_ref)
642 .map_err(|e| Error::Message(format!("failed to read reflog for '{reflog_ref}': {e}")))?;
643
644 let mut candidates = Vec::new();
645 let mut seen = oid_hash_set();
646
647 for entry in entries.iter().rev() {
648 let oid = if entry.message.starts_with("checkout:") {
649 entry.old_oid
650 } else {
651 entry.new_oid
652 };
653 if !seen.insert(oid) {
654 continue;
655 }
656 if is_ancestor(repo, oid, head)? {
657 candidates.push(oid);
658 }
659 }
660
661 if let Some(fp) = select_best_fork_point(repo, &candidates)? {
662 return Ok(fp);
663 }
664
665 let mut bases = merge_bases_first_vs_rest(repo, upstream_tip, &[head])?;
666 if bases.is_empty() {
667 return Err(Error::Message(
668 "no merge base found between upstream and HEAD".to_owned(),
669 ));
670 }
671 bases.sort();
672 Ok(bases[0])
673}
674
675pub fn ancestor_closure(repo: &Repository, tip: ObjectId) -> Result<HashSet<ObjectId>> {
681 let mut cache = CommitGraphCache::new(repo);
682 Ok(cache.ancestor_closure(tip)?.into_iter().collect())
683}
684
685pub fn count_symmetric_ahead_behind(
694 repo: &Repository,
695 local: ObjectId,
696 other: ObjectId,
697) -> Result<(usize, usize)> {
698 let (ahead, behind, _, _) = ahead_behind_of_target_commits(repo, local, other, 0, 0)?;
699 Ok((ahead, behind))
700}
701
702pub fn ahead_behind_of_target_commits(
714 repo: &Repository,
715 head: ObjectId,
716 target: ObjectId,
717 retain_newest_ahead: usize,
718 retain_newest_behind: usize,
719) -> Result<(usize, usize, Vec<ObjectId>, Vec<ObjectId>)> {
720 if head == target {
721 return Ok((0, 0, Vec::new(), Vec::new()));
722 }
723 let mut cache = CommitGraphCache::new(repo);
724 let paint = SidePaint::compute(&mut cache, &[head], &[target])?;
725 let ahead = paint.count_only(PAINT_LEFT);
726 let behind = paint.count_only(PAINT_RIGHT);
727 let behind_paint = (retain_newest_behind > 0 && behind > 0).then(|| paint.swapped());
728 let ahead_oids = if retain_newest_ahead > 0 && ahead > 0 {
729 let mut state = ReachableWalkState::new(repo, &[head], &[target])?;
731 state.paint = Some(paint);
732 state.emit_until(
733 repo,
734 Some(retain_newest_ahead),
735 ReachableWalkCommitterCutoffs::default(),
736 )?
737 } else {
738 Vec::new()
739 };
740 let behind_oids = if let Some(behind_paint) = behind_paint {
741 let mut state = ReachableWalkState::new(repo, &[target], &[head])?;
742 state.paint = Some(behind_paint);
743 state.emit_until(
744 repo,
745 Some(retain_newest_behind),
746 ReachableWalkCommitterCutoffs::default(),
747 )?
748 } else {
749 Vec::new()
750 };
751 Ok((ahead, behind, ahead_oids, behind_oids))
752}
753
754#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
769pub struct ReachableWalkCommitterCutoffs {
770 pub since: Option<i64>,
772 pub until: Option<i64>,
774}
775
776#[derive(Debug, Clone, Copy)]
778pub enum ReachableWalkLimit {
779 StopAfter(usize),
781 CountAllRetainNewest(usize),
783 Unlimited,
785}
786
787#[derive(Clone, Copy, Debug, Eq, PartialEq)]
789struct WalkDateKey {
790 oid: ObjectId,
791 time: i64,
792 seq: u64,
794}
795
796impl Ord for WalkDateKey {
797 fn cmp(&self, other: &Self) -> Ordering {
798 match self.time.cmp(&other.time) {
799 Ordering::Equal => match other.seq.cmp(&self.seq) {
800 Ordering::Equal => self.oid.cmp(&other.oid),
801 ord => ord,
802 },
803 ord => ord,
804 }
805 }
806}
807
808impl PartialOrd for WalkDateKey {
809 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
810 Some(self.cmp(other))
811 }
812}
813
814#[derive(Clone, Debug)]
821pub struct ReachableWalkState {
822 heap: BinaryHeap<WalkDateKey>,
823 done: OidHashSet,
824 queued: OidHashSet,
825 seeded: OidHashSet,
827 seq: u64,
828 hide: Vec<ObjectId>,
829 paint: Option<SidePaint>,
832 use_commit_graph: bool,
834 hide_paint_stop: HidePaintStop,
836}
837
838#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
840enum HidePaintStop {
841 #[default]
844 Converged,
845 TipsResolved,
852 TipsResolvedDateSlop,
859}
860
861#[derive(Clone, Debug, PartialEq, Eq)]
863pub struct ReachableWalkSnapshot {
864 pub heap: Vec<(i64, u64, ObjectId)>,
866 pub done: Vec<ObjectId>,
868 pub queued: Vec<ObjectId>,
870 pub seeded: Vec<ObjectId>,
872 pub seq: u64,
874 pub hide: Vec<ObjectId>,
876}
877
878impl ReachableWalkState {
879 pub fn new(repo: &Repository, tips: &[ObjectId], hide: &[ObjectId]) -> Result<Self> {
885 Self::with_commit_graph(repo, tips, hide, true)
886 }
887
888 pub(crate) fn with_commit_graph(
891 repo: &Repository,
892 tips: &[ObjectId],
893 hide: &[ObjectId],
894 use_commit_graph: bool,
895 ) -> Result<Self> {
896 let mut cache = CommitGraphCache::with_commit_graph(repo, use_commit_graph);
897 Self::seed(&mut cache, tips, hide, use_commit_graph)
898 }
899
900 fn seed(
901 cache: &mut CommitGraphCache<'_>,
902 tips: &[ObjectId],
903 hide: &[ObjectId],
904 use_commit_graph: bool,
905 ) -> Result<Self> {
906 let mut state = Self {
907 heap: BinaryHeap::new(),
908 done: oid_hash_set(),
909 queued: oid_hash_set(),
910 seeded: oid_hash_set(),
911 seq: 0,
912 hide: hide.to_vec(),
913 paint: None,
914 use_commit_graph,
915 hide_paint_stop: HidePaintStop::default(),
916 };
917 for &tip in tips {
918 if state.hide.contains(&tip) {
919 continue;
920 }
921 if state.queued.insert(tip) {
922 state.push_on_heap(cache, tip, true)?;
923 }
924 }
925 Ok(state)
926 }
927
928 pub(crate) fn page_retaining_commits(
935 repo: &Repository,
936 resume: Option<ReachableWalkSnapshot>,
937 tips: &[ObjectId],
938 hide: &[ObjectId],
939 limit: usize,
940 cutoffs: ReachableWalkCommitterCutoffs,
941 ) -> Result<(Self, Vec<ObjectId>, WalkedCommits)> {
942 repo.odb().with_pack_read_context(|| {
945 Self::page_retaining_commits_in_context(repo, resume, tips, hide, limit, cutoffs)
946 })
947 }
948
949 fn page_retaining_commits_in_context(
950 repo: &Repository,
951 resume: Option<ReachableWalkSnapshot>,
952 tips: &[ObjectId],
953 hide: &[ObjectId],
954 limit: usize,
955 cutoffs: ReachableWalkCommitterCutoffs,
956 ) -> Result<(Self, Vec<ObjectId>, WalkedCommits)> {
957 const MAX_PRESIZE: usize = 1 << 16;
959 let mut cache = CommitGraphCache::new(repo);
960 cache.retained = Some(WalkedCommits::with_capacity(
961 limit.saturating_add(16).min(MAX_PRESIZE),
962 ));
963 let mut state = match resume {
964 Some(snapshot) => Self::from_snapshot(snapshot),
965 None => Self::seed(&mut cache, tips, hide, true)?,
966 };
967 let commits = state.emit_with(&mut cache, Some(limit), cutoffs)?;
968 let mut retained = cache.retained.take().unwrap_or_default();
969 let emitted_present = commits.iter().filter(|oid| retained.contains(oid)).count();
970 if emitted_present < retained.len() {
971 let emitted: OidHashSet = commits.iter().copied().collect();
972 retained.retain(|oid| emitted.contains(oid));
973 }
974 Ok((state, commits, retained))
975 }
976
977 #[must_use]
979 pub fn from_snapshot(snapshot: ReachableWalkSnapshot) -> Self {
980 let ReachableWalkSnapshot {
981 heap,
982 done,
983 queued,
984 seeded,
985 seq,
986 hide,
987 } = snapshot;
988 Self {
989 heap: heap
990 .into_iter()
991 .map(|(time, seq, oid)| WalkDateKey { oid, time, seq })
992 .collect(),
993 done: done.into_iter().collect(),
994 queued: queued.into_iter().collect(),
995 seeded: seeded.into_iter().collect(),
996 seq,
997 paint: None,
998 hide,
999 use_commit_graph: true,
1000 hide_paint_stop: HidePaintStop::default(),
1001 }
1002 }
1003
1004 #[must_use]
1006 pub fn snapshot(&self) -> ReachableWalkSnapshot {
1007 ReachableWalkSnapshot {
1008 heap: self
1009 .heap
1010 .iter()
1011 .map(|key| (key.time, key.seq, key.oid))
1012 .collect(),
1013 done: self.done.iter().copied().collect(),
1014 queued: self.queued.iter().copied().collect(),
1015 seeded: self.seeded.iter().copied().collect(),
1016 seq: self.seq,
1017 hide: self.hide.clone(),
1018 }
1019 }
1020
1021 #[must_use]
1023 pub fn has_pending(&self) -> bool {
1024 !self.heap.is_empty()
1025 }
1026
1027 #[must_use]
1033 pub fn has_emittable_pending(&self, cutoffs: ReachableWalkCommitterCutoffs) -> bool {
1034 if self.heap.is_empty() {
1035 return false;
1036 }
1037 if cutoffs
1038 .since
1039 .is_some_and(|since| self.heap.iter().all(|key| key.time < since))
1040 {
1041 return false;
1042 }
1043 true
1044 }
1045
1046 fn ensure_paint(&mut self, cache: &mut CommitGraphCache<'_>) -> Result<()> {
1049 if self.paint.is_some() || self.hide.is_empty() {
1050 return Ok(());
1051 }
1052 let pending: Vec<ObjectId> = self.heap.iter().map(|key| key.oid).collect();
1053 self.paint = Some(SidePaint::compute_hiding(
1054 cache,
1055 &pending,
1056 &self.hide,
1057 self.hide_paint_stop,
1058 )?);
1059 Ok(())
1060 }
1061
1062 fn is_hidden(&mut self, cache: &mut CommitGraphCache<'_>, oid: ObjectId) -> Result<bool> {
1063 if self.hide.is_empty() {
1064 return Ok(false);
1065 }
1066 if self.hide.contains(&oid) {
1067 return Ok(true);
1068 }
1069 self.ensure_paint(cache)?;
1070 let Some(paint) = &self.paint else {
1071 return Ok(false);
1072 };
1073 if let Some(reached) = paint.reached_by(oid, PAINT_RIGHT) {
1074 return Ok(reached);
1075 }
1076 if self.seeded.contains(&oid) {
1078 let peeled = peel_to_commit_for_merge_base(cache.repo, oid).unwrap_or(oid);
1079 if let Some(reached) = paint.reached_by(peeled, PAINT_RIGHT) {
1080 return Ok(reached);
1081 }
1082 }
1083 Ok(true)
1086 }
1087
1088 fn push_on_heap(
1089 &mut self,
1090 cache: &mut CommitGraphCache<'_>,
1091 oid: ObjectId,
1092 is_tip: bool,
1093 ) -> Result<()> {
1094 if is_tip {
1095 self.seeded.insert(oid);
1096 }
1097 let time = cache.commit_time_for_heap(oid)?;
1098 let seq = self.seq;
1099 self.seq += 1;
1100 self.heap.push(WalkDateKey { oid, time, seq });
1101 Ok(())
1102 }
1103
1104 fn after_processed(&mut self, cache: &mut CommitGraphCache<'_>, oid: ObjectId) -> Result<()> {
1105 for parent in cache.parents_of(oid)? {
1106 if self.queued.insert(parent) {
1107 if self.is_hidden(cache, parent)? {
1110 self.done.insert(parent);
1111 continue;
1112 }
1113 self.push_on_heap(cache, parent, false)?;
1114 }
1115 }
1116 Ok(())
1117 }
1118
1119 fn pop_next_emit(
1120 &mut self,
1121 cache: &mut CommitGraphCache<'_>,
1122 cutoffs: ReachableWalkCommitterCutoffs,
1123 ) -> Result<Option<ObjectId>> {
1124 self.ensure_paint(cache)?;
1125 loop {
1126 let Some(key) = self.heap.pop() else {
1127 return Ok(None);
1128 };
1129 let oid = key.oid;
1130 if !self.done.insert(oid) {
1131 continue;
1132 }
1133 if cutoffs.since.is_some_and(|since| key.time < since) {
1134 self.after_processed(cache, oid)?;
1135 let stop = self.heap.is_empty()
1136 || self
1137 .heap
1138 .iter()
1139 .all(|pending| pending.time < cutoffs.since.unwrap_or(i64::MIN));
1140 if stop {
1141 self.heap.clear();
1142 return Ok(None);
1143 }
1144 continue;
1145 }
1146 let hidden = self.is_hidden(cache, oid)?;
1147 if hidden {
1148 self.after_processed(cache, oid)?;
1149 continue;
1150 }
1151 if cutoffs.until.is_some_and(|until| key.time > until) {
1152 self.after_processed(cache, oid)?;
1153 continue;
1154 }
1155 self.after_processed(cache, oid)?;
1156 return Ok(Some(oid));
1157 }
1158 }
1159
1160 pub fn emit_until(
1166 &mut self,
1167 repo: &Repository,
1168 limit: Option<usize>,
1169 cutoffs: ReachableWalkCommitterCutoffs,
1170 ) -> Result<Vec<ObjectId>> {
1171 let mut cache = CommitGraphCache::with_commit_graph(repo, self.use_commit_graph);
1172 self.emit_with(&mut cache, limit, cutoffs)
1173 }
1174
1175 fn emit_with(
1176 &mut self,
1177 cache: &mut CommitGraphCache<'_>,
1178 limit: Option<usize>,
1179 cutoffs: ReachableWalkCommitterCutoffs,
1180 ) -> Result<Vec<ObjectId>> {
1181 let mut out = Vec::new();
1182 while let Some(oid) = self.pop_next_emit(cache, cutoffs)? {
1183 out.push(oid);
1184 if limit.is_some_and(|n| out.len() >= n) {
1185 break;
1186 }
1187 }
1188 Ok(out)
1189 }
1190}
1191
1192pub fn walk_commits_reachable_excluding_ancestors_of(
1198 repo: &Repository,
1199 tips: &[ObjectId],
1200 hide: &[ObjectId],
1201 limit: ReachableWalkLimit,
1202) -> Result<(usize, Vec<ObjectId>)> {
1203 walk_commits_reachable_excluding_ancestors_of_cutoffs(
1204 repo,
1205 tips,
1206 hide,
1207 limit,
1208 ReachableWalkCommitterCutoffs::default(),
1209 )
1210}
1211
1212pub fn walk_commits_reachable_excluding_ancestors_of_cutoffs(
1214 repo: &Repository,
1215 tips: &[ObjectId],
1216 hide: &[ObjectId],
1217 limit: ReachableWalkLimit,
1218 cutoffs: ReachableWalkCommitterCutoffs,
1219) -> Result<(usize, Vec<ObjectId>)> {
1220 if tips.is_empty() {
1221 return Ok((0, Vec::new()));
1222 }
1223
1224 let mut state = ReachableWalkState::new(repo, tips, hide)?;
1225 let (stop_early_at, retain_newest) = match limit {
1226 ReachableWalkLimit::StopAfter(n) => (Some(n), None),
1227 ReachableWalkLimit::CountAllRetainNewest(n) => (None, Some(n)),
1228 ReachableWalkLimit::Unlimited => (None, None),
1229 };
1230
1231 if let Some(k) = retain_newest {
1232 let mut cache = CommitGraphCache::new(repo);
1233 let mut out = Vec::new();
1234 let mut emitted = 0usize;
1235 while let Some(oid) = state.pop_next_emit(&mut cache, cutoffs)? {
1236 emitted += 1;
1237 if out.len() < k {
1238 out.push(oid);
1239 }
1240 }
1241 return Ok((emitted, out));
1242 }
1243
1244 let out = state.emit_until(repo, stop_early_at, cutoffs)?;
1245 Ok((out.len(), out))
1246}
1247
1248pub(crate) fn count_commits_left_behind(
1264 repo: &Repository,
1265 tips: &[ObjectId],
1266 hide: &[ObjectId],
1267 retain_newest: usize,
1268) -> Result<(usize, Vec<ObjectId>)> {
1269 let mut state = ReachableWalkState::new(repo, tips, hide)?;
1270 let mut cache = CommitGraphCache::new(repo);
1271 if cache.graft_parents.is_empty() && cache.shallow_boundaries.is_empty() {
1272 state.hide_paint_stop = if cache.graph.is_some() {
1273 HidePaintStop::TipsResolved
1274 } else {
1275 HidePaintStop::TipsResolvedDateSlop
1276 };
1277 }
1278 let mut newest = Vec::new();
1279 let mut total = 0usize;
1280 while let Some(oid) =
1281 state.pop_next_emit(&mut cache, ReachableWalkCommitterCutoffs::default())?
1282 {
1283 total += 1;
1284 if newest.len() < retain_newest {
1285 newest.push(oid);
1286 }
1287 }
1288 Ok((total, newest))
1289}
1290
1291pub fn ahead_of_target_commits(
1298 repo: &Repository,
1299 head: ObjectId,
1300 target: ObjectId,
1301 retain_newest: usize,
1302) -> Result<(usize, Vec<ObjectId>)> {
1303 if head == target {
1304 return Ok((0, Vec::new()));
1305 }
1306 walk_commits_reachable_excluding_ancestors_of(
1307 repo,
1308 std::slice::from_ref(&head),
1309 &[target],
1310 ReachableWalkLimit::CountAllRetainNewest(retain_newest),
1311 )
1312}
1313
1314#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1316pub struct ReachableCountFilter {
1317 pub min_parents: Option<usize>,
1319 pub max_parents: Option<usize>,
1321 pub first_parent_only: bool,
1323 pub since: Option<i64>,
1325 pub until: Option<i64>,
1327}
1328
1329fn try_count_diff_via_stored_commit_bitmaps(
1330 repo: &Repository,
1331 tip: ObjectId,
1332 hide: ObjectId,
1333) -> Result<Option<usize>> {
1334 let Some(index) = repo
1335 .odb()
1336 .with_pack_read_context(|| BitmapIndex::open(repo))
1337 .map_err(|e| Error::CorruptObject(format!("pack bitmap: {e}")))?
1338 else {
1339 return Ok(None);
1340 };
1341 let Some(tip_bm) = index.commit_bitmap(&tip) else {
1342 return Ok(None);
1343 };
1344 let Some(hide_bm) = index.commit_bitmap(&hide) else {
1345 return Ok(None);
1346 };
1347 let mut reachable = Bitmap::new();
1348 tip_bm.or_into(&mut reachable);
1349 let mut hidden = Bitmap::new();
1350 hide_bm.or_into(&mut hidden);
1351 reachable.and_not_assign(&hidden);
1352 let mut commit_type = Bitmap::new();
1353 index
1354 .type_bitmap(ObjectKind::Commit)
1355 .expand_into_bitmap(&mut commit_type)
1356 .map_err(|e| Error::CorruptObject(format!("pack bitmap: {e}")))?;
1357 commit_type.and_assign(&reachable);
1358 Ok(Some(commit_type.count_ones()))
1359}
1360
1361pub fn count_commits_reachable_excluding_ancestors_of(
1374 repo: &Repository,
1375 tips: &[ObjectId],
1376 hide: &[ObjectId],
1377 filter: ReachableCountFilter,
1378) -> Result<usize> {
1379 if tips.is_empty() {
1380 return Ok(0);
1381 }
1382 let mut cache = CommitGraphCache::new(repo);
1383
1384 let unfiltered = filter.min_parents.is_none()
1385 && filter.max_parents.is_none()
1386 && filter.since.is_none()
1387 && filter.until.is_none();
1388 let generations_bound_paint = cache.graph.is_some() && !cache.has_parent_rewrites()?;
1390 if !generations_bound_paint || hide.is_empty() {
1391 if let Some(n) = try_count_without_paint(&mut cache, tips, hide, filter, unfiltered)? {
1392 return Ok(n);
1393 }
1394 }
1395
1396 let paint = match (generations_bound_paint, filter.first_parent_only) {
1397 (true, true) => SidePaint::compute_hiding_first_parent(&mut cache, tips, hide)?,
1398 (true, false) => {
1399 SidePaint::compute_hiding(&mut cache, tips, hide, HidePaintStop::TipsResolved)?
1400 }
1401 (false, _) => SidePaint::compute_hiding(&mut cache, tips, hide, HidePaintStop::Converged)?,
1402 };
1403 if unfiltered && !filter.first_parent_only {
1404 return Ok(paint.count_only(PAINT_LEFT));
1405 }
1406
1407 let mut sides = paint.sides;
1411 let mut stack = Vec::with_capacity(tips.len());
1412 for &tip in tips {
1413 if let Some(tip) = try_peel_to_commit_for_merge_base(repo, tip)? {
1414 stack.push(tip);
1415 }
1416 }
1417 let mut count = 0usize;
1418 while let Some(oid) = stack.pop() {
1419 if sides.remove(&oid) != Some(PAINT_LEFT) {
1420 continue;
1421 }
1422 let mut parents = cache.parents_of(oid)?;
1423 let parent_count = parents.len();
1424 let time = cache.commit_time(oid)?;
1425 let in_range = filter.since.is_none_or(|since| time >= since)
1426 && filter.until.is_none_or(|until| time <= until);
1427 let parent_ok = parent_count >= filter.min_parents.unwrap_or(0)
1428 && parent_count <= filter.max_parents.unwrap_or(usize::MAX);
1429 if in_range && parent_ok {
1430 count += 1;
1431 }
1432 if filter.first_parent_only {
1433 parents.truncate(1);
1434 }
1435 stack.extend(parents);
1436 }
1437 Ok(count)
1438}
1439
1440pub(crate) fn first_parent_hidden_frontier(
1459 repo: &Repository,
1460 tips: &[ObjectId],
1461 hide: &[ObjectId],
1462 use_commit_graph: bool,
1463) -> Result<HashSet<ObjectId>> {
1464 let mut cache = CommitGraphCache::with_commit_graph(repo, use_commit_graph);
1465 if cache.graph.is_some() && cache.has_parent_rewrites()? {
1468 cache = CommitGraphCache::with_commit_graph(repo, false);
1469 }
1470 let paint = SidePaint::compute_hiding_first_parent(&mut cache, tips, hide)?;
1471
1472 let mut frontier = HashSet::new();
1473 let mut seen = oid_hash_set();
1474 let mut stack = tips.to_vec();
1475 while let Some(oid) = stack.pop() {
1476 if !seen.insert(oid) {
1477 continue;
1478 }
1479 if paint.sides.get(&oid) != Some(&PAINT_LEFT) {
1481 frontier.insert(oid);
1482 continue;
1483 }
1484 if let Some(&parent) = cache.parents_of(oid)?.first() {
1485 stack.push(parent);
1486 }
1487 }
1488 Ok(frontier)
1489}
1490
1491fn try_count_without_paint(
1494 cache: &mut CommitGraphCache<'_>,
1495 tips: &[ObjectId],
1496 hide: &[ObjectId],
1497 filter: ReachableCountFilter,
1498 unfiltered: bool,
1499) -> Result<Option<usize>> {
1500 let single = (tips.len() == 1 && hide.len() == 1).then(|| (tips[0], hide[0]));
1501 let relation = single.map(|(tip, hide)| first_parent_chain_relation(cache, tip, hide));
1502 if matches!(
1503 relation,
1504 Some(FirstParentRelation::Same | FirstParentRelation::FirstIsAncestor)
1505 ) {
1506 return Ok(Some(0));
1507 }
1508
1509 if unfiltered && (hide.is_empty() || !filter.first_parent_only) {
1512 if let Some(graph) = &cache.graph {
1513 let shallow: HashSet<ObjectId> = cache.shallow_boundaries.iter().copied().collect();
1514 if let Some(n) =
1515 graph.count_reachable_commits(tips, hide, filter.first_parent_only, &shallow)
1516 {
1517 return Ok(Some(n));
1518 }
1519 }
1520 }
1521
1522 let Some((tip, hide)) = single else {
1523 return Ok(None);
1524 };
1525 if relation == Some(FirstParentRelation::SecondIsAncestor) {
1528 if let Some(n) = try_count_on_first_parent_chain_excluding_hide(cache, tip, hide, filter)? {
1529 return Ok(Some(n));
1530 }
1531 }
1532 if unfiltered && !filter.first_parent_only {
1533 return try_count_diff_via_stored_commit_bitmaps(cache.repo, tip, hide);
1534 }
1535 Ok(None)
1536}
1537
1538pub fn try_linear_chain_commits_with_cutoffs(
1543 repo: &Repository,
1544 tip: ObjectId,
1545 limit: usize,
1546 cutoffs: ReachableWalkCommitterCutoffs,
1547) -> Result<Option<Vec<ObjectId>>> {
1548 let mut cache = CommitGraphCache::new(repo);
1549 let mut out = Vec::with_capacity(limit.min(64));
1550 let mut stack = vec![tip];
1551 while let Some(cur) = stack.pop() {
1552 if out.len() >= limit {
1553 break;
1554 }
1555 let parents = cache.parents_of(cur)?;
1556 if parents.len() > 1 {
1557 return Ok(None);
1558 }
1559 let time = cache.commit_time(cur)?;
1560 let below_since = cutoffs.since.is_some_and(|since| time < since);
1561 let above_until = cutoffs.until.is_some_and(|until| time > until);
1562 if !below_since && !above_until {
1563 out.push(cur);
1564 }
1565 if below_since {
1566 stack.extend(parents);
1567 continue;
1568 }
1569 if let Some(&parent) = parents.first() {
1570 stack.push(parent);
1571 }
1572 }
1573 Ok(Some(out))
1574}
1575
1576fn try_count_on_first_parent_chain_excluding_hide(
1582 cache: &mut CommitGraphCache<'_>,
1583 tip: ObjectId,
1584 hide: ObjectId,
1585 filter: ReachableCountFilter,
1586) -> Result<Option<usize>> {
1587 let mut count = 0usize;
1588 let mut cur = tip;
1589 while cur != hide {
1590 let parents = cache.parents_of(cur)?;
1591 if !filter.first_parent_only && parents.len() > 1 {
1592 return Ok(None);
1593 }
1594 let parent_count = parents.len();
1595 let time = cache.commit_time(cur)?;
1596 let in_range = filter.since.is_none_or(|since| time >= since)
1597 && filter.until.is_none_or(|until| time <= until);
1598 let parent_ok = parent_count >= filter.min_parents.unwrap_or(0)
1599 && parent_count <= filter.max_parents.unwrap_or(usize::MAX);
1600 if in_range && parent_ok {
1601 count += 1;
1602 }
1603 cur = first_parent_oid(cache, cur)?;
1604 }
1605 Ok(Some(count))
1606}
1607
1608pub fn commits_reachable_excluding_ancestors_of(
1610 repo: &Repository,
1611 tips: &[ObjectId],
1612 hide: &[ObjectId],
1613 limit: Option<usize>,
1614) -> Result<Vec<ObjectId>> {
1615 commits_reachable_excluding_ancestors_of_cutoffs(
1616 repo,
1617 tips,
1618 hide,
1619 limit,
1620 ReachableWalkCommitterCutoffs::default(),
1621 )
1622}
1623
1624pub fn commits_reachable_excluding_ancestors_of_cutoffs(
1626 repo: &Repository,
1627 tips: &[ObjectId],
1628 hide: &[ObjectId],
1629 limit: Option<usize>,
1630 cutoffs: ReachableWalkCommitterCutoffs,
1631) -> Result<Vec<ObjectId>> {
1632 let walk_limit = match limit {
1633 Some(n) => ReachableWalkLimit::StopAfter(n),
1634 None => ReachableWalkLimit::Unlimited,
1635 };
1636 let (_emitted, oids) = walk_commits_reachable_excluding_ancestors_of_cutoffs(
1637 repo, tips, hide, walk_limit, cutoffs,
1638 )?;
1639 Ok(oids)
1640}
1641
1642pub(crate) fn commits_reachable_excluding_ancestors_of_with_graph(
1645 repo: &Repository,
1646 tips: &[ObjectId],
1647 hide: &[ObjectId],
1648 limit: Option<usize>,
1649 cutoffs: ReachableWalkCommitterCutoffs,
1650 use_commit_graph: bool,
1651) -> Result<Vec<ObjectId>> {
1652 if tips.is_empty() {
1653 return Ok(Vec::new());
1654 }
1655 ReachableWalkState::with_commit_graph(repo, tips, hide, use_commit_graph)?
1656 .emit_until(repo, limit, cutoffs)
1657}
1658
1659pub fn independent_commits(repo: &Repository, commits: &[ObjectId]) -> Result<Vec<ObjectId>> {
1668 let mut cache = CommitGraphCache::new(repo);
1669 let mut out = Vec::new();
1670 for (i, &candidate) in commits.iter().enumerate() {
1671 let mut reachable = false;
1672 for (j, &other) in commits.iter().enumerate() {
1673 if i == j {
1674 continue;
1675 }
1676 if cache.ancestor_closure(other)?.contains(&candidate) {
1677 reachable = true;
1678 break;
1679 }
1680 }
1681 if !reachable {
1682 out.push(candidate);
1683 }
1684 }
1685 Ok(out)
1686}
1687
1688pub fn branch_base_for_tip(
1704 repo: &Repository,
1705 tip: ObjectId,
1706 bases: &[ObjectId],
1707) -> Result<Option<usize>> {
1708 if bases.is_empty() {
1709 return Ok(None);
1710 }
1711
1712 let tip_chain = first_parent_chain(repo, tip)?;
1713 let tip_positions: OidHashMap<usize> = tip_chain
1714 .iter()
1715 .copied()
1716 .enumerate()
1717 .map(|(index, oid)| (oid, index))
1718 .collect();
1719
1720 let mut best: Option<(usize, usize)> = None;
1721 for (base_index, &base) in bases.iter().enumerate() {
1722 for oid in first_parent_chain(repo, base)? {
1723 let Some(&tip_position) = tip_positions.get(&oid) else {
1724 continue;
1725 };
1726 match best {
1727 None => best = Some((tip_position, base_index)),
1728 Some((best_position, best_index))
1729 if tip_position < best_position
1730 || (tip_position == best_position && base_index < best_index) =>
1731 {
1732 best = Some((tip_position, base_index));
1733 }
1734 _ => {}
1735 }
1736 break;
1737 }
1738 }
1739
1740 Ok(best.map(|(_, index)| index))
1741}
1742
1743fn first_parent_chain(repo: &Repository, start: ObjectId) -> Result<Vec<ObjectId>> {
1744 let mut chain = Vec::new();
1745 let mut current = Some(start);
1746 while let Some(oid) = current {
1747 chain.push(oid);
1748 current = first_parent(repo, oid)?;
1749 }
1750 Ok(chain)
1751}
1752
1753fn first_parent(repo: &Repository, oid: ObjectId) -> Result<Option<ObjectId>> {
1754 let commit = repo.odb().read_commit(&oid)?;
1755 Ok(commit.parents.first().copied())
1756}
1757
1758fn ensure_is_commit(repo: &Repository, oid: ObjectId) -> Result<()> {
1759 let object = repo.odb().read(&oid)?;
1760 if object.kind != ObjectKind::Commit {
1761 return Err(Error::CorruptObject(format!(
1762 "object {oid} is not a commit"
1763 )));
1764 }
1765 Ok(())
1766}
1767
1768const UNRANKED_COMMIT: u64 = (1u64 << 63) - 1;
1769const LEGACY_RANK_CAP: u64 = 0x3FFF_FFFF;
1770
1771const MARK_TIP_A: u8 = 1 << 0;
1772const MARK_TIP_B: u8 = 1 << 1;
1773const MARK_PRUNED: u8 = 1 << 2;
1774const MARK_BASE: u8 = 1 << 3;
1775const MARK_QUEUED: u8 = 1 << 4;
1776
1777#[cfg(test)]
1778pub(crate) mod intersect_walk_test_hooks {
1779 use std::cell::Cell;
1780
1781 thread_local! {
1782 static INTERSECT_WALK_STEPS: Cell<usize> = const { Cell::new(0) };
1783 }
1784
1785 pub fn reset_intersect_walk_steps() {
1786 INTERSECT_WALK_STEPS.with(|c| c.set(0));
1787 }
1788
1789 pub fn intersect_walk_steps() -> usize {
1790 INTERSECT_WALK_STEPS.with(|c| c.get())
1791 }
1792
1793 pub(crate) fn record_intersect_walk_step() {
1794 INTERSECT_WALK_STEPS.with(|c| c.set(c.get().saturating_add(1)));
1795 }
1796
1797 thread_local! {
1798 static COMMIT_PARENT_LOOKUPS: Cell<usize> = const { Cell::new(0) };
1799 }
1800
1801 pub fn reset_commit_parent_lookups() {
1802 COMMIT_PARENT_LOOKUPS.with(|c| c.set(0));
1803 }
1804
1805 pub fn commit_parent_lookups() -> usize {
1806 COMMIT_PARENT_LOOKUPS.with(|c| c.get())
1807 }
1808
1809 pub(crate) fn record_commit_parent_lookup() {
1810 COMMIT_PARENT_LOOKUPS.with(|c| c.set(c.get().saturating_add(1)));
1811 }
1812
1813 thread_local! {
1814 static COMMIT_PARENT_QUERIES: Cell<usize> = const { Cell::new(0) };
1816 }
1817
1818 pub fn reset_commit_parent_queries() {
1819 COMMIT_PARENT_QUERIES.with(|c| c.set(0));
1820 }
1821
1822 pub fn commit_parent_queries() -> usize {
1823 COMMIT_PARENT_QUERIES.with(|c| c.get())
1824 }
1825
1826 pub(crate) fn record_commit_parent_query() {
1827 COMMIT_PARENT_QUERIES.with(|c| c.set(c.get().saturating_add(1)));
1828 }
1829}
1830
1831#[derive(Eq, PartialEq)]
1832struct RankedCommit {
1833 rank: u64,
1834 committed_at: i64,
1835 queued_at: u64,
1838 oid: ObjectId,
1839}
1840
1841impl Ord for RankedCommit {
1842 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1843 self.rank
1844 .cmp(&other.rank)
1845 .then_with(|| self.committed_at.cmp(&other.committed_at))
1846 .then_with(|| other.queued_at.cmp(&self.queued_at))
1847 }
1848}
1849
1850impl PartialOrd for RankedCommit {
1851 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1852 Some(self.cmp(other))
1853 }
1854}
1855
1856struct OpenQueueTally {
1857 open_on_a: isize,
1858 open_on_b: isize,
1859 open_on_both: isize,
1860 newest_rank_seen: u64,
1861 rank_stop: u64,
1862 queued: u64,
1864}
1865
1866impl OpenQueueTally {
1867 fn apply_label(&mut self, label: u8, delta: isize) {
1868 let side = label & (MARK_TIP_A | MARK_TIP_B | MARK_PRUNED);
1869 if side == MARK_TIP_A | MARK_TIP_B | MARK_PRUNED {
1870 return;
1871 }
1872 if side == MARK_TIP_A | MARK_TIP_B {
1873 self.open_on_both += delta;
1874 } else if side == MARK_TIP_A {
1875 self.open_on_a += delta;
1876 } else if side == MARK_TIP_B {
1877 self.open_on_b += delta;
1878 }
1879 }
1880}
1881
1882fn first_parent_oid(cache: &mut CommitGraphCache<'_>, oid: ObjectId) -> Result<ObjectId> {
1883 let parents = cache.parents_of(oid)?;
1884 parents
1885 .first()
1886 .copied()
1887 .ok_or_else(|| Error::ObjectNotFound(format!("{oid} has no parent")))
1888}
1889
1890fn direct_ancestor_relation(
1894 cache: &mut CommitGraphCache<'_>,
1895 first: ObjectId,
1896 second: ObjectId,
1897) -> Result<FirstParentRelation> {
1898 if first == second {
1899 return Ok(FirstParentRelation::Same);
1900 }
1901 let first_gen = commit_rank_for_walk(cache, first);
1902 let second_gen = commit_rank_for_walk(cache, second);
1903 if first_gen == UNRANKED_COMMIT || second_gen == UNRANKED_COMMIT {
1904 return Ok(first_parent_chain_relation(cache, first, second));
1905 }
1906 if first_gen == second_gen {
1907 return Ok(FirstParentRelation::Unrelated);
1908 }
1909 let (ancestor, ancestor_gen, mut cur, mut gen, found) = if first_gen < second_gen {
1910 (
1911 first,
1912 first_gen,
1913 second,
1914 second_gen,
1915 FirstParentRelation::FirstIsAncestor,
1916 )
1917 } else {
1918 (
1919 second,
1920 second_gen,
1921 first,
1922 first_gen,
1923 FirstParentRelation::SecondIsAncestor,
1924 )
1925 };
1926 while gen > ancestor_gen {
1927 cur = first_parent_oid(cache, cur)?;
1928 gen = commit_rank_for_walk(cache, cur);
1929 if gen == UNRANKED_COMMIT {
1930 return Ok(FirstParentRelation::Unrelated);
1931 }
1932 }
1933 Ok(if cur == ancestor {
1934 found
1935 } else {
1936 FirstParentRelation::Unrelated
1937 })
1938}
1939
1940#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1942enum FirstParentRelation {
1943 Same,
1945 FirstIsAncestor,
1947 SecondIsAncestor,
1949 Unrelated,
1952}
1953
1954fn first_parent_chain_relation(
1964 cache: &mut CommitGraphCache<'_>,
1965 first: ObjectId,
1966 second: ObjectId,
1967) -> FirstParentRelation {
1968 if first == second {
1969 return FirstParentRelation::Same;
1970 }
1971 let mut seen_first = oid_hash_set();
1972 seen_first.insert(first);
1973 let mut seen_second = oid_hash_set();
1974 seen_second.insert(second);
1975 let mut cur_first = Some(first);
1976 let mut cur_second = Some(second);
1977 while cur_first.is_some() || cur_second.is_some() {
1978 let first_time = cur_first.and_then(|oid| cache.commit_time(oid).ok());
1981 let second_time = cur_second.and_then(|oid| cache.commit_time(oid).ok());
1982 let (step_first, step_second) = match (cur_first, cur_second) {
1983 (Some(_), Some(_)) => match (first_time, second_time) {
1984 (Some(a), Some(b)) if a > b => (true, false),
1985 (Some(a), Some(b)) if a < b => (false, true),
1986 _ => (true, true),
1987 },
1988 (first, second) => (first.is_some(), second.is_some()),
1989 };
1990 if let Some(cur) = cur_first.filter(|_| step_first) {
1991 cur_first = first_parent_oid(cache, cur).ok();
1992 if let Some(next) = cur_first {
1993 if next == second {
1994 return FirstParentRelation::SecondIsAncestor;
1995 }
1996 if seen_second.contains(&next) {
1997 return FirstParentRelation::Unrelated;
1998 }
1999 seen_first.insert(next);
2000 }
2001 }
2002 if let Some(cur) = cur_second.filter(|_| step_second) {
2003 cur_second = first_parent_oid(cache, cur).ok();
2004 if let Some(next) = cur_second {
2005 if next == first {
2006 return FirstParentRelation::FirstIsAncestor;
2007 }
2008 if seen_first.contains(&next) {
2009 return FirstParentRelation::Unrelated;
2010 }
2011 seen_second.insert(next);
2012 }
2013 }
2014 }
2015 FirstParentRelation::Unrelated
2016}
2017
2018fn commit_graph_rank(cache: &CommitGraphCache<'_>, oid: ObjectId) -> Option<u64> {
2020 let graph = cache.graph.as_ref()?;
2021 let rank = graph.commit_generation_for_walk(&oid);
2022 if rank == UNRANKED_COMMIT {
2023 None
2024 } else {
2025 Some(rank)
2026 }
2027}
2028
2029fn commit_rank_for_walk(cache: &mut CommitGraphCache<'_>, oid: ObjectId) -> u64 {
2030 if let Some(rank) = commit_graph_rank(cache, oid) {
2031 return rank;
2032 }
2033 UNRANKED_COMMIT
2034}
2035
2036fn push_commit_heap_key(
2039 cache: &mut CommitGraphCache<'_>,
2040 heap: &mut BinaryHeap<PaintKey>,
2041 seq: &mut u64,
2042 oid: ObjectId,
2043) -> Result<()> {
2044 let rank = commit_rank_for_walk(cache, oid);
2045 let committed_at = cache.commit_time(oid)?;
2046 heap.push(PaintKey {
2047 rank,
2048 committed_at,
2049 seq: std::cmp::Reverse(*seq),
2050 oid,
2051 });
2052 *seq += 1;
2053 Ok(())
2054}
2055
2056const PAINT_LEFT: u8 = 1;
2058const PAINT_RIGHT: u8 = 2;
2060const PAINT_BOTH: u8 = PAINT_LEFT | PAINT_RIGHT;
2061
2062#[derive(Clone, Debug, Default)]
2073struct SidePaint {
2074 sides: OidHashMap<u8>,
2075}
2076
2077impl SidePaint {
2078 fn compute(
2084 cache: &mut CommitGraphCache<'_>,
2085 left: &[ObjectId],
2086 right: &[ObjectId],
2087 ) -> Result<Self> {
2088 Self::paint(cache, left, right, PaintWalk::default())
2089 }
2090
2091 fn compute_hiding(
2099 cache: &mut CommitGraphCache<'_>,
2100 tips: &[ObjectId],
2101 hide: &[ObjectId],
2102 stop: HidePaintStop,
2103 ) -> Result<Self> {
2104 let walk = PaintWalk {
2105 skip_missing_right: true,
2106 open_while_left_only: matches!(
2107 stop,
2108 HidePaintStop::TipsResolved | HidePaintStop::TipsResolvedDateSlop
2109 ),
2110 date_slop: stop == HidePaintStop::TipsResolvedDateSlop,
2111 ..PaintWalk::default()
2112 };
2113 Self::paint(cache, tips, hide, walk)
2114 }
2115
2116 fn compute_hiding_first_parent(
2125 cache: &mut CommitGraphCache<'_>,
2126 tips: &[ObjectId],
2127 hide: &[ObjectId],
2128 ) -> Result<Self> {
2129 let walk = PaintWalk {
2130 skip_missing_right: true,
2131 open_while_left_only: true,
2132 left_first_parent_only: true,
2133 ..PaintWalk::default()
2134 };
2135 Self::paint(cache, tips, hide, walk)
2136 }
2137
2138 fn paint(
2139 cache: &mut CommitGraphCache<'_>,
2140 left: &[ObjectId],
2141 right: &[ObjectId],
2142 mut walk: PaintWalk,
2143 ) -> Result<Self> {
2144 cache.prime_tips(left);
2145 cache.prime_tips(right);
2146 for (tips, side) in [(left, PAINT_LEFT), (right, PAINT_RIGHT)] {
2147 for &tip in tips {
2148 let Some(tip) = cache.peel_tip(tip)? else {
2149 continue;
2150 };
2151 walk.mark(cache, tip, side)?;
2152 }
2153 }
2154 let mut slop = PAINT_SLOP;
2155 loop {
2156 if walk.open > 0 {
2157 slop = PAINT_SLOP;
2158 } else if !walk.unranked_hide_may_reach_left(&mut slop) {
2159 break;
2160 }
2161 let Some(node) = walk.heap.pop() else {
2162 break;
2163 };
2164 if walk.queued.remove(&node.oid) == Some(true) {
2165 walk.open -= 1;
2166 }
2167 walk.popped.insert(node.oid);
2168 let side = walk.sides.get(&node.oid).copied().unwrap_or(0);
2169 if walk.date_slop && side == PAINT_LEFT && node.rank == UNRANKED_COMMIT {
2170 walk.left_floor = Some(
2171 walk.left_floor
2172 .map_or(node.committed_at, |floor| floor.min(node.committed_at)),
2173 );
2174 }
2175 for (i, parent) in cache.parents_of(node.oid)?.into_iter().enumerate() {
2176 let side = if i > 0 && walk.left_first_parent_only {
2177 side & PAINT_RIGHT
2178 } else {
2179 side
2180 };
2181 walk.mark(cache, parent, side)?;
2182 }
2183 }
2184 Ok(Self { sides: walk.sides })
2185 }
2186
2187 fn count_only(&self, side: u8) -> usize {
2189 self.sides.values().filter(|&&s| s == side).count()
2190 }
2191
2192 fn swapped(&self) -> Self {
2194 let sides = self
2195 .sides
2196 .iter()
2197 .map(|(&oid, &s)| {
2198 let flipped = match s {
2199 PAINT_LEFT => PAINT_RIGHT,
2200 PAINT_RIGHT => PAINT_LEFT,
2201 other => other,
2202 };
2203 (oid, flipped)
2204 })
2205 .collect();
2206 Self { sides }
2207 }
2208
2209 fn reached_by(&self, oid: ObjectId, side: u8) -> Option<bool> {
2211 self.sides.get(&oid).map(|s| s & side != 0)
2212 }
2213}
2214
2215#[derive(Default)]
2217struct PaintWalk {
2218 sides: OidHashMap<u8>,
2219 queued: OidHashMap<bool>,
2221 popped: OidHashSet,
2222 heap: BinaryHeap<PaintKey>,
2223 seq: u64,
2224 open: usize,
2226 skip_missing_right: bool,
2228 open_while_left_only: bool,
2231 date_slop: bool,
2234 left_floor: Option<i64>,
2237 left_first_parent_only: bool,
2239}
2240
2241const PAINT_SLOP: usize = 5;
2245
2246const UNRANKED_SKEW_ALLOWANCE: time::Duration = time::Duration::DAY;
2250
2251impl PaintWalk {
2252 fn unranked_hide_may_reach_left(&self, slop: &mut usize) -> bool {
2260 if !self.date_slop {
2261 return false;
2262 }
2263 let (Some(floor), Some(next)) = (self.left_floor, self.heap.peek()) else {
2264 return false;
2265 };
2266 if next.rank != UNRANKED_COMMIT {
2267 return false;
2268 }
2269 let horizon = floor.saturating_sub(UNRANKED_SKEW_ALLOWANCE.whole_seconds());
2270 if next.committed_at >= horizon {
2271 return true;
2272 }
2273 if *slop == 0 {
2274 return false;
2275 }
2276 *slop -= 1;
2277 true
2278 }
2279
2280 fn mark(&mut self, cache: &mut CommitGraphCache<'_>, oid: ObjectId, side: u8) -> Result<()> {
2287 let before = self.sides.get(&oid).copied().unwrap_or(0);
2288 let after = before | side;
2289 if after == before {
2290 return Ok(());
2291 }
2292 self.sides.insert(oid, after);
2293 let repaint = self.popped.contains(&oid);
2294 if let Some(is_open) = self.queued.get_mut(&oid) {
2295 if *is_open && after == PAINT_BOTH && !repaint {
2296 *is_open = false;
2297 self.open -= 1;
2298 }
2299 return Ok(());
2300 }
2301 let committed_at = match cache.commit_time(oid) {
2302 Ok(time) => time,
2303 Err(Error::ObjectNotFound(_))
2305 if self.skip_missing_right && after & PAINT_RIGHT != 0 =>
2306 {
2307 return Ok(());
2308 }
2309 Err(e) => return Err(e),
2310 };
2311 let rank = commit_rank_for_walk(cache, oid);
2312 let unresolved = if self.open_while_left_only && (rank != UNRANKED_COMMIT || self.date_slop)
2313 {
2314 after == PAINT_LEFT
2315 } else {
2316 after != PAINT_BOTH
2317 };
2318 let is_open = unresolved || repaint;
2319 self.queued.insert(oid, is_open);
2320 if is_open {
2321 self.open += 1;
2322 }
2323 self.heap.push(PaintKey {
2324 rank,
2325 committed_at,
2326 seq: std::cmp::Reverse(self.seq),
2327 oid,
2328 });
2329 self.seq += 1;
2330 Ok(())
2331 }
2332}
2333
2334#[derive(Eq, Ord, PartialEq, PartialOrd)]
2337struct PaintKey {
2338 rank: u64,
2339 committed_at: i64,
2340 seq: std::cmp::Reverse<u64>,
2341 oid: ObjectId,
2342}
2343
2344fn sort_merge_bases_for_git_output(
2346 cache: &mut CommitGraphCache<'_>,
2347 bases: &mut [ObjectId],
2348) -> Result<()> {
2349 let mut keyed = Vec::with_capacity(bases.len());
2350 for &oid in bases.iter() {
2351 keyed.push((cache.commit_time(oid)?, oid));
2352 }
2353 keyed.sort_by_key(|a| std::cmp::Reverse(a.0));
2354 for (slot, (_, oid)) in keyed.into_iter().enumerate() {
2355 bases[slot] = oid;
2356 }
2357 Ok(())
2358}
2359
2360fn enqueue_commit_for_intersect_walk(
2361 cache: &mut CommitGraphCache<'_>,
2362 pending: &mut BinaryHeap<RankedCommit>,
2363 tally: &mut OpenQueueTally,
2364 labels: &mut OidHashMap<u8>,
2365 oid: ObjectId,
2366 extra: u8,
2367) -> Result<()> {
2368 let slot = labels.entry(oid).or_insert(0);
2369 let before = *slot;
2370 let merged = before | extra;
2371 *slot = merged;
2372 if before & MARK_QUEUED != 0 {
2373 tally.apply_label(before, -1);
2374 tally.apply_label(merged, 1);
2375 return Ok(());
2376 }
2377 *slot = merged | MARK_QUEUED;
2378 let rank = commit_rank_for_walk(cache, oid);
2379 let committed_at = cache.commit_time(oid)?;
2380 tally.queued += 1;
2381 pending.push(RankedCommit {
2382 rank,
2383 committed_at,
2384 queued_at: tally.queued,
2385 oid,
2386 });
2387 tally.apply_label(merged | MARK_QUEUED, 1);
2388 Ok(())
2389}
2390
2391fn dequeue_commit_for_intersect_walk(
2392 pending: &mut BinaryHeap<RankedCommit>,
2393 tally: &mut OpenQueueTally,
2394 labels: &mut OidHashMap<u8>,
2395 allow_rank_stop: bool,
2396) -> Option<ObjectId> {
2397 let node = pending.pop()?;
2398 tally.newest_rank_seen = node.rank;
2399 let before = labels.get(&node.oid).copied().unwrap_or(0);
2400 labels.insert(node.oid, before & !MARK_QUEUED);
2401 if tally.open_on_both == 0 {
2402 if tally.open_on_a == 0 && tally.open_on_b == 0 {
2403 return None;
2407 } else if allow_rank_stop
2408 && (tally.open_on_a == 0 || tally.open_on_b == 0)
2409 && node.rank != UNRANKED_COMMIT
2410 && node.rank < tally.rank_stop
2411 {
2412 return None;
2413 }
2414 }
2415 tally.apply_label(before, -1);
2416 Some(node.oid)
2417}
2418
2419fn walk_intersecting_histories(
2421 cache: &mut CommitGraphCache<'_>,
2422 tip_a: ObjectId,
2423 tips_b: &[ObjectId],
2424 collect_every_base: bool,
2425) -> Result<Vec<ObjectId>> {
2426 let mut pending = BinaryHeap::new();
2427 let mut labels: OidHashMap<u8> = oid_hash_map();
2428 let mut tally = OpenQueueTally {
2429 open_on_a: 0,
2430 open_on_b: 0,
2431 open_on_both: 0,
2432 newest_rank_seen: UNRANKED_COMMIT,
2433 rank_stop: LEGACY_RANK_CAP,
2434 queued: 0,
2435 };
2436 let mut discovered: Vec<ObjectId> = Vec::new();
2437
2438 labels.insert(tip_a, MARK_TIP_A);
2439 enqueue_commit_for_intersect_walk(cache, &mut pending, &mut tally, &mut labels, tip_a, 0)?;
2440 for &tip_b in tips_b {
2441 enqueue_commit_for_intersect_walk(
2442 cache,
2443 &mut pending,
2444 &mut tally,
2445 &mut labels,
2446 tip_b,
2447 MARK_TIP_B,
2448 )?;
2449 }
2450
2451 while let Some(oid) = dequeue_commit_for_intersect_walk(
2452 &mut pending,
2453 &mut tally,
2454 &mut labels,
2455 !collect_every_base,
2456 ) {
2457 #[cfg(test)]
2458 intersect_walk_test_hooks::record_intersect_walk_step();
2459 let mut propagate =
2460 labels.get(&oid).copied().unwrap_or(0) & (MARK_TIP_A | MARK_TIP_B | MARK_PRUNED);
2461 if propagate == MARK_TIP_A | MARK_TIP_B {
2462 let slot = labels.entry(oid).or_insert(0);
2463 if *slot & MARK_BASE == 0 {
2464 *slot |= MARK_BASE;
2465 discovered.push(oid);
2466 if !collect_every_base && tally.newest_rank_seen < tally.rank_stop {
2467 break;
2468 }
2469 }
2470 propagate |= MARK_PRUNED;
2471 }
2472 for parent in cache.parents_of(oid)? {
2473 let have = labels.get(&parent).copied().unwrap_or(0);
2474 if (have & propagate) == propagate {
2475 continue;
2476 }
2477 enqueue_commit_for_intersect_walk(
2478 cache,
2479 &mut pending,
2480 &mut tally,
2481 &mut labels,
2482 parent,
2483 propagate,
2484 )?;
2485 }
2486 }
2487
2488 let mut bases = Vec::new();
2489 for oid in discovered {
2490 if labels.get(&oid).copied().unwrap_or(0) & MARK_PRUNED == 0 {
2491 bases.push(oid);
2492 }
2493 }
2494 if !collect_every_base && !bases.is_empty() {
2495 let candidates: OidHashSet = bases.into_iter().collect();
2496 bases = reduce_to_best(candidates, cache)?;
2497 }
2498 sort_merge_bases_for_git_output(cache, &mut bases)?;
2499 Ok(bases)
2500}
2501
2502fn reduce_to_best(
2503 candidates: OidHashSet,
2504 cache: &mut CommitGraphCache<'_>,
2505) -> Result<Vec<ObjectId>> {
2506 if candidates.is_empty() {
2507 return Ok(Vec::new());
2508 }
2509 let mut best = BTreeSet::new();
2510 for &candidate in &candidates {
2511 let mut better_found = false;
2512 for &other in &candidates {
2513 if candidate == other {
2514 continue;
2515 }
2516 if cache.is_ancestor(candidate, other)? {
2517 better_found = true;
2518 break;
2519 }
2520 }
2521 if !better_found {
2522 best.insert(candidate);
2523 }
2524 }
2525 Ok(best.into_iter().collect())
2526}
2527
2528const PRIME_TIPS_MIN: usize = 64;
2531
2532struct CommitGraphCache<'r> {
2533 repo: &'r Repository,
2534 closures: OidHashMap<OidHashSet>,
2535 promisor_stop: OidHashSet,
2536 shallow_boundaries: OidHashSet,
2537 graft_parents: OidHashMap<Vec<ObjectId>>,
2539 graph: Option<crate::commit_graph_file::CommitGraphChain>,
2543 single_pack: Option<crate::pack::SingleLocalPackReader>,
2545 single_pack_unavailable: bool,
2547 retained: Option<WalkedCommits>,
2550 retain_reads: bool,
2553 read_buf: Vec<u8>,
2557 header_primed: OidHashSet,
2559}
2560
2561impl<'r> CommitGraphCache<'r> {
2562 fn new(repo: &'r Repository) -> Self {
2563 Self::with_commit_graph(repo, true)
2564 }
2565
2566 fn with_commit_graph(repo: &'r Repository, use_commit_graph: bool) -> Self {
2568 let cfg = repo.config().unwrap_or_default();
2569 let promisor_stop = if repo_treats_promisor_packs(repo.git_dir(), &cfg) {
2578 read_promisor_missing_oids(repo.git_dir())
2579 .into_iter()
2580 .collect::<OidHashSet>()
2581 } else {
2582 oid_hash_set()
2583 };
2584 let graph = if use_commit_graph {
2585 repo.odb().commit_graph_chain().map(|arc| (*arc).clone())
2586 } else {
2587 None
2588 };
2589 let shallow_boundaries = oid_hash_set_from(&load_shallow_boundaries(repo.git_dir()));
2590 let graft_parents = load_graft_parents(repo.git_dir()).into_iter().collect();
2591 Self {
2592 repo,
2593 closures: oid_hash_map(),
2594 promisor_stop,
2595 shallow_boundaries,
2596 graft_parents,
2597 graph,
2598 single_pack: None,
2599 single_pack_unavailable: false,
2600 retained: None,
2601 retain_reads: false,
2602 read_buf: Vec::new(),
2603 header_primed: oid_hash_set(),
2604 }
2605 }
2606
2607 fn read_object_into(&mut self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<ObjectKind> {
2613 self.ensure_single_pack_reader();
2614 if let Some(reader) = self.single_pack.as_mut() {
2615 match reader.read_object_into_with_delta_policy(oid, true, buf) {
2616 Err(Error::ObjectNotFound(_)) => {}
2617 read => return read,
2618 }
2619 }
2620 self.repo.odb().read_into_caching_delta_bases(oid, buf)
2621 }
2622
2623 fn has_parent_rewrites(&self) -> Result<bool> {
2625 if !self.graft_parents.is_empty() {
2626 return Ok(true);
2627 }
2628 let mut has_replacements = false;
2629 if let Some(prefix) = self.repo.replace_ref_base_if_enabled() {
2630 self.repo.refs().for_each_ref(&prefix, &mut |_| {
2631 has_replacements = true;
2632 std::ops::ControlFlow::Break(())
2633 })?;
2634 }
2635 Ok(has_replacements)
2636 }
2637
2638 fn grafted_parents(&self, oid: ObjectId, parents: Vec<ObjectId>) -> Vec<ObjectId> {
2639 self.graft_parents.get(&oid).cloned().unwrap_or(parents)
2640 }
2641
2642 fn with_linkage<R>(
2643 &self,
2644 f: impl FnOnce(&mut crate::repo_caches::CommitLinkageCache) -> R,
2645 ) -> R {
2646 let mut guard = self
2647 .repo
2648 .caches()
2649 .commit_linkage()
2650 .lock()
2651 .unwrap_or_else(|e| e.into_inner());
2652 f(&mut guard)
2653 }
2654
2655 fn ensure_single_pack_reader(&mut self) {
2660 if self.single_pack.is_some()
2661 || self.single_pack_unavailable
2662 || !self.repo.odb().uses_files_primary()
2663 {
2664 return;
2665 }
2666 let objects_dir = self.repo.odb().objects_dir();
2667 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
2668 return;
2669 }
2670 match crate::pack::SingleLocalPackReader::open_allow_loose(objects_dir) {
2671 Ok(Some(reader)) => self.single_pack = Some(reader),
2672 _ => self.single_pack_unavailable = true,
2674 }
2675 }
2676
2677 fn ancestor_closure(&mut self, start: ObjectId) -> Result<OidHashSet> {
2678 if let Some(existing) = self.closures.get(&start) {
2679 return Ok(existing.clone());
2680 }
2681
2682 let mut visited = oid_hash_set();
2683 let mut queue = VecDeque::new();
2684 queue.push_back(start);
2685 while let Some(oid) = queue.pop_front() {
2686 if !visited.insert(oid) {
2687 continue;
2688 }
2689 for parent in self.parents_of(oid)? {
2690 queue.push_back(parent);
2691 }
2692 }
2693 self.closures.insert(start, visited.clone());
2694 Ok(visited)
2695 }
2696
2697 fn parents_of(&mut self, oid: ObjectId) -> Result<Vec<ObjectId>> {
2698 #[cfg(test)]
2699 intersect_walk_test_hooks::record_commit_parent_query();
2700 if let Some(parents) = self.with_linkage(|link| link.parents.get(&oid).cloned()) {
2701 return Ok(parents);
2702 }
2703 #[cfg(test)]
2704 intersect_walk_test_hooks::record_commit_parent_lookup();
2705 if self.promisor_stop.contains(&oid) {
2706 let parents = self.grafted_parents(oid, Vec::new());
2707 self.with_linkage(|link| {
2708 link.parents.insert(oid, parents.clone());
2709 link.times.insert(oid, 0);
2710 });
2711 return Ok(parents);
2712 }
2713 if self.shallow_boundaries.contains(&oid) {
2714 let commit_oid =
2715 peel_to_commit_for_merge_base(self.repo, oid).map_err(|e| match e {
2716 Error::InvalidRef(msg) => Error::CorruptObject(msg),
2717 other => other,
2718 })?;
2719 let object = self.repo.odb().read(&commit_oid)?;
2720 if object.kind != ObjectKind::Commit {
2721 return Err(Error::CorruptObject(format!(
2722 "object {commit_oid} is not a commit"
2723 )));
2724 }
2725 let commit = CommitRef::parse(&object.data)?;
2726 let time = commit.committer_timestamp();
2727 let parents = self.grafted_parents(oid, Vec::new());
2728 self.with_linkage(|link| {
2729 link.times.insert(oid, time);
2730 link.parents.insert(oid, parents.clone());
2731 });
2732 return Ok(parents);
2733 }
2734 if let Some(graph) = &self.graph {
2737 if let Some((parents, ctime)) = graph.graph_commit(&oid) {
2738 let parents = if self.shallow_boundaries.contains(&oid) {
2739 Vec::new()
2740 } else {
2741 parents
2742 .into_iter()
2743 .filter(|p| !self.promisor_stop.contains(p))
2744 .collect()
2745 };
2746 let parents = self.grafted_parents(oid, parents);
2747 self.with_linkage(|link| {
2748 link.times.insert(oid, ctime);
2749 link.parents.insert(oid, parents.clone());
2750 });
2751 return Ok(parents);
2752 }
2753 }
2754 let mut buf = std::mem::take(&mut self.read_buf);
2757 let parents = self.parents_from_object(oid, &mut buf);
2758 self.read_buf = buf;
2759 parents
2760 }
2761
2762 fn parents_from_object(&mut self, oid: ObjectId, buf: &mut Vec<u8>) -> Result<Vec<ObjectId>> {
2765 let effective_oid = self.repo.object_id_after_replace(&oid);
2766 let direct_commit =
2767 effective_oid == oid && self.read_object_into(&oid, buf)? == ObjectKind::Commit;
2768 let commit_oid = if direct_commit {
2769 oid
2770 } else {
2771 let commit_oid =
2772 peel_to_commit_for_merge_base(self.repo, oid).map_err(|e| match e {
2773 Error::InvalidRef(msg) => Error::CorruptObject(msg),
2774 other => other,
2775 })?;
2776 let kind =
2777 self.read_object_into(&self.repo.object_id_after_replace(&commit_oid), buf)?;
2778 if kind != ObjectKind::Commit {
2779 return Err(Error::CorruptObject(format!(
2780 "object {commit_oid} is not a commit"
2781 )));
2782 }
2783 commit_oid
2784 };
2785 let linkage = CommitRef::parse(buf)?;
2786 let (time, parsed_parents) = (linkage.committer_timestamp(), linkage.parents().to_vec());
2787 if self.retain_reads && effective_oid == oid && commit_oid == oid {
2788 if let Some(retained) = self.retained.as_mut() {
2789 retained.insert(oid, buf);
2790 }
2791 }
2792 Ok(self.record_linkage(oid, commit_oid, time, &parsed_parents))
2793 }
2794
2795 fn record_linkage(
2798 &mut self,
2799 oid: ObjectId,
2800 commit_oid: ObjectId,
2801 time: i64,
2802 parsed_parents: &[ObjectId],
2803 ) -> Vec<ObjectId> {
2804 let parents: Vec<ObjectId> = if self.shallow_boundaries.contains(&commit_oid) {
2805 Vec::new()
2806 } else {
2807 parsed_parents
2808 .iter()
2809 .copied()
2810 .filter(|p| !self.promisor_stop.contains(p))
2811 .collect()
2812 };
2813 let parents = self.grafted_parents(oid, parents);
2814 self.with_linkage(|link| {
2815 link.times.insert(oid, time);
2816 link.parents.insert(oid, parents.clone());
2817 });
2818 parents
2819 }
2820
2821 fn is_plain_tip(&self, oid: &ObjectId) -> bool {
2823 self.repo.object_id_after_replace(oid) == *oid
2824 && !self.promisor_stop.contains(oid)
2825 && !self.shallow_boundaries.contains(oid)
2826 }
2827
2828 fn graph_lists(&self, oid: &ObjectId) -> bool {
2829 self.graph
2830 .as_ref()
2831 .is_some_and(|graph| graph.find_commit(oid).is_some())
2832 }
2833
2834 fn prime_tips(&mut self, tips: &[ObjectId]) {
2844 if tips.len() < PRIME_TIPS_MIN {
2845 return;
2846 }
2847 let pending: Vec<ObjectId> = tips
2848 .iter()
2849 .filter(|oid| {
2850 !self.header_primed.contains(*oid)
2851 && self.is_plain_tip(oid)
2852 && !self.graph_lists(oid)
2853 })
2854 .copied()
2855 .collect();
2856 let repo = self.repo;
2857 let read_linkage = |oid: &ObjectId| -> Option<(i64, Vec<ObjectId>)> {
2858 let mut buf = Vec::new();
2859 if !repo
2860 .odb()
2861 .read_packed_commit_header_block_into(oid, &mut buf)
2862 .ok()?
2863 {
2864 return None;
2865 }
2866 let commit = CommitRef::parse(&buf).ok()?;
2867 Some((commit.committer_timestamp(), commit.parents().to_vec()))
2868 };
2869 let read = crate::rev_list::map_commits_in_parallel(&pending, read_linkage);
2870 for (oid, linkage) in pending.into_iter().zip(read) {
2871 if let Some((time, parents)) = linkage {
2872 self.record_linkage(oid, oid, time, &parents);
2873 self.header_primed.insert(oid);
2874 }
2875 }
2876 }
2877
2878 fn peel_tip(&mut self, oid: ObjectId) -> Result<Option<ObjectId>> {
2881 if self.header_primed.contains(&oid) || (self.is_plain_tip(&oid) && self.graph_lists(&oid))
2882 {
2883 return Ok(Some(oid));
2884 }
2885 try_peel_to_commit_for_merge_base(self.repo, oid)
2886 }
2887
2888 fn commit_time(&mut self, oid: ObjectId) -> Result<i64> {
2892 if let Some(t) = self.with_linkage(|link| link.times.get(&oid).copied()) {
2893 return Ok(t);
2894 }
2895 self.parents_of(oid)?;
2896 Ok(self
2897 .with_linkage(|link| link.times.get(&oid).copied())
2898 .unwrap_or(0))
2899 }
2900
2901 fn commit_time_for_heap(&mut self, oid: ObjectId) -> Result<i64> {
2904 self.retain_reads = self.retained.is_some();
2905 let time = self.commit_time(oid);
2906 self.retain_reads = false;
2907 time
2908 }
2909
2910 fn is_ancestor(&mut self, ancestor: ObjectId, descendant: ObjectId) -> Result<bool> {
2920 use std::collections::BinaryHeap;
2921
2922 let ancestor = peel_to_commit_for_merge_base(self.repo, ancestor).unwrap_or(ancestor);
2924 let descendant = peel_to_commit_for_merge_base(self.repo, descendant).unwrap_or(descendant);
2925 if ancestor == descendant {
2926 return Ok(true);
2927 }
2928
2929 let a_time = self.commit_time(ancestor)?;
2930
2931 const SLOP: i32 = 100;
2935 let mut slop = SLOP;
2936
2937 let mut heap: BinaryHeap<(i64, ObjectId)> = BinaryHeap::new();
2938 let mut visited: OidHashSet = oid_hash_set();
2939 let d_time = self.commit_time(descendant)?;
2940 visited.insert(descendant);
2941 heap.push((d_time, descendant));
2942
2943 while let Some((time, oid)) = heap.pop() {
2944 if oid == ancestor {
2945 return Ok(true);
2946 }
2947 if time < a_time {
2948 slop -= 1;
2949 if slop <= 0 {
2950 return Ok(false);
2951 }
2952 } else {
2953 slop = SLOP;
2954 }
2955 for parent in self.parents_of(oid)? {
2956 if visited.insert(parent) {
2957 let pt = self.commit_time(parent)?;
2958 heap.push((pt, parent));
2959 }
2960 }
2961 }
2962 Ok(false)
2963 }
2964
2965 fn tips_merged_from_bases(
2971 &mut self,
2972 bases: &[ObjectId],
2973 tips: &[ObjectId],
2974 ) -> Result<OidHashSet> {
2975 let mut merged = oid_hash_set();
2976 if bases.is_empty() || tips.is_empty() {
2977 return Ok(merged);
2978 }
2979
2980 let mut tip_gens: Vec<(ObjectId, u64)> = tips
2981 .iter()
2982 .map(|&tip| (tip, commit_rank_for_walk(self, tip)))
2983 .collect();
2984 tip_gens.sort_by_key(|(_, gen)| *gen);
2985 let mut min_gen_index = 0usize;
2986 let mut min_generation = tip_gens[0].1;
2987
2988 let tip_set: OidHashSet = tips.iter().copied().collect();
2989
2990 let mut stack: Vec<ObjectId> = bases.to_vec();
2991
2992 let mut seen: OidHashSet = oid_hash_set();
2993 while let Some(oid) = stack.pop() {
2994 if !seen.insert(oid) {
2995 continue;
2996 }
2997 if tip_set.contains(&oid) {
2998 merged.insert(oid);
2999 while min_gen_index < tip_gens.len() && merged.contains(&tip_gens[min_gen_index].0)
3000 {
3001 min_gen_index += 1;
3002 }
3003 if min_gen_index >= tip_gens.len() {
3004 break;
3005 }
3006 min_generation = tip_gens[min_gen_index].1;
3007 }
3008 for parent in self.parents_of(oid)? {
3009 if seen.contains(&parent) {
3010 continue;
3011 }
3012 let p_gen = commit_rank_for_walk(self, parent);
3013 if p_gen != UNRANKED_COMMIT && p_gen < min_generation {
3014 continue;
3015 }
3016 stack.push(parent);
3017 }
3018 }
3019 Ok(merged)
3020 }
3021}
3022
3023#[derive(Clone)]
3025struct TipIndexBitset {
3026 words: Vec<u64>,
3027}
3028
3029impl TipIndexBitset {
3030 fn with_capacity(tips: usize) -> Self {
3031 Self {
3032 words: vec![0; tips.div_ceil(64)],
3033 }
3034 }
3035
3036 fn set(&mut self, index: usize) {
3037 self.words[index / 64] |= 1_u64 << (index % 64);
3038 }
3039
3040 fn is_empty(&self) -> bool {
3041 self.words.iter().all(|w| *w == 0)
3042 }
3043
3044 fn or_assign(&mut self, other: &Self) -> bool {
3046 let mut changed = false;
3047 for (slot, &bits) in self.words.iter_mut().zip(&other.words) {
3048 let merged = *slot | bits;
3049 if merged != *slot {
3050 *slot = merged;
3051 changed = true;
3052 }
3053 }
3054 changed
3055 }
3056
3057 fn from_index(tip_count: usize, index: usize) -> Self {
3058 let mut set = Self::with_capacity(tip_count);
3059 set.set(index);
3060 set
3061 }
3062
3063 fn iter_indices(&self) -> impl Iterator<Item = usize> + '_ {
3064 self.words.iter().enumerate().flat_map(|(word_idx, word)| {
3065 let base = word_idx * 64;
3066 (0..64).filter_map(move |bit| {
3067 if *word & (1_u64 << bit) != 0 {
3068 Some(base + bit)
3069 } else {
3070 None
3071 }
3072 })
3073 })
3074 }
3075}
3076
3077struct RefContainsWalk {
3081 tip_count: usize,
3082 pending: OidHashMap<TipIndexBitset>,
3083 queued: OidHashSet,
3084 popped: OidHashSet,
3085 heap: BinaryHeap<PaintKey>,
3086 seq: u64,
3087}
3088
3089impl RefContainsWalk {
3090 fn new(tip_count: usize) -> Self {
3091 Self {
3092 tip_count,
3093 pending: oid_hash_map(),
3094 queued: oid_hash_set(),
3095 popped: oid_hash_set(),
3096 heap: BinaryHeap::new(),
3097 seq: 0,
3098 }
3099 }
3100
3101 fn seed(
3102 &mut self,
3103 cache: &mut CommitGraphCache<'_>,
3104 tip_index: usize,
3105 oid: ObjectId,
3106 ) -> Result<()> {
3107 self.enqueue(
3108 cache,
3109 oid,
3110 TipIndexBitset::from_index(self.tip_count, tip_index),
3111 )
3112 }
3113
3114 fn enqueue(
3115 &mut self,
3116 cache: &mut CommitGraphCache<'_>,
3117 oid: ObjectId,
3118 bits: TipIndexBitset,
3119 ) -> Result<()> {
3120 let entry = self
3121 .pending
3122 .entry(oid)
3123 .or_insert_with(|| TipIndexBitset::with_capacity(self.tip_count));
3124 if !entry.or_assign(&bits) {
3125 return Ok(());
3126 }
3127 if self.popped.contains(&oid) {
3128 self.popped.remove(&oid);
3131 self.queued.remove(&oid);
3132 }
3133 if self.queued.insert(oid) {
3134 push_commit_heap_key(cache, &mut self.heap, &mut self.seq, oid)?;
3135 }
3136 Ok(())
3137 }
3138
3139 fn pop(&mut self) -> Option<(ObjectId, TipIndexBitset)> {
3140 let node = self.heap.pop()?;
3141 self.queued.remove(&node.oid);
3142 self.popped.insert(node.oid);
3143 let bits = self.pending.remove(&node.oid)?;
3144 Some((node.oid, bits))
3145 }
3146}
3147
3148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3150pub(crate) struct RefContainsTipState {
3151 pub contains_match: bool,
3153 pub no_contains_match: bool,
3155}
3156
3157pub(crate) struct RefContainsMemo<'repo> {
3161 graph: CommitGraphCache<'repo>,
3162 known: OidHashMap<bool>,
3163 min_target_gen: Option<u64>,
3164 negative_pending: Vec<ObjectId>,
3165 negative_frontier: BinaryHeap<PaintKey>,
3166 negative_seq: u64,
3167}
3168
3169struct ContainsFrame {
3170 oid: ObjectId,
3171 parents: Vec<ObjectId>,
3172 next_parent: usize,
3173}
3174
3175impl<'repo> RefContainsMemo<'repo> {
3176 pub(crate) fn new(repo: &'repo Repository, targets: &[ObjectId]) -> Result<Option<Self>> {
3179 let mut graph = CommitGraphCache::new(repo);
3180 if graph.has_parent_rewrites()? {
3181 return Ok(None);
3182 }
3183 let min_target_gen = min_ranked_generation(&graph, targets);
3184 let mut known: OidHashMap<bool> = targets.iter().map(|&oid| (oid, true)).collect();
3185 let mut negative_pending = Vec::new();
3186 if graph.graph.is_none() && targets.len() == 1 {
3187 for parent in graph.parents_of(targets[0])? {
3190 if parent == targets[0] {
3191 return Err(Error::CorruptObject(format!(
3192 "commit ancestry cycle at {parent}"
3193 )));
3194 }
3195 if known.insert(parent, false).is_none() {
3196 negative_pending.push(parent);
3197 }
3198 }
3199 }
3200 Ok(Some(Self {
3201 graph,
3202 known,
3203 min_target_gen,
3204 negative_pending,
3205 negative_frontier: BinaryHeap::new(),
3206 negative_seq: 0,
3207 }))
3208 }
3209
3210 fn known(&self, oid: ObjectId) -> Option<bool> {
3211 self.known.get(&oid).copied().or_else(|| {
3212 should_prune_ref_filter_parent(&self.graph, oid, self.min_target_gen).then_some(false)
3213 })
3214 }
3215
3216 fn advance_negative_boundary(&mut self, active: &OidHashMap<usize>) -> Result<Option<usize>> {
3217 for oid in self.negative_pending.drain(..) {
3218 push_commit_heap_key(
3219 &mut self.graph,
3220 &mut self.negative_frontier,
3221 &mut self.negative_seq,
3222 oid,
3223 )?;
3224 }
3225 let mut cut: Option<usize> = None;
3226 if let Some(key) = self.negative_frontier.pop() {
3227 for parent in self.graph.parents_of(key.oid)? {
3228 match self.known.get(&parent) {
3229 Some(true) => {
3230 return Err(Error::CorruptObject(format!(
3231 "commit ancestry cycle at {parent}"
3232 )))
3233 }
3234 Some(false) => {}
3235 None => {
3236 self.known.insert(parent, false);
3237 push_commit_heap_key(
3240 &mut self.graph,
3241 &mut self.negative_frontier,
3242 &mut self.negative_seq,
3243 parent,
3244 )?;
3245 }
3246 }
3247 if let Some(&index) = active.get(&parent) {
3248 cut = Some(cut.map_or(index, |old| old.min(index)));
3249 }
3250 }
3251 }
3252 Ok(cut)
3253 }
3254
3255 pub(crate) fn contains(&mut self, tip: ObjectId) -> Result<bool> {
3258 if let Some(known) = self.known(tip) {
3259 return Ok(known);
3260 }
3261 let mut active = oid_hash_map();
3262 active.insert(tip, 0);
3263 let mut stack = vec![ContainsFrame {
3264 oid: tip,
3265 parents: self.graph.parents_of(tip)?,
3266 next_parent: 0,
3267 }];
3268 let mut visited = 1usize;
3269 while !stack.is_empty() {
3270 if visited >= 1024 {
3274 if let Some(cut) = self.advance_negative_boundary(&active)? {
3275 for frame in stack.drain(cut..) {
3278 self.known.insert(frame.oid, false);
3279 active.remove(&frame.oid);
3280 }
3281 continue;
3282 }
3283 }
3284 let Some(frame) = stack.last_mut() else {
3285 break;
3286 };
3287 if self.known(frame.oid) == Some(false) {
3288 active.remove(&frame.oid);
3289 stack.pop();
3290 continue;
3291 }
3292 let Some(&parent) = frame.parents.get(frame.next_parent) else {
3293 self.known.insert(frame.oid, false);
3294 active.remove(&frame.oid);
3295 stack.pop();
3296 continue;
3297 };
3298 frame.next_parent += 1;
3299 match self.known(parent) {
3300 Some(true) => {
3301 for frame in stack {
3302 self.known.insert(frame.oid, true);
3303 }
3304 return Ok(true);
3305 }
3306 Some(false) => continue,
3307 None => {}
3308 }
3309 if active.insert(parent, stack.len()).is_some() {
3310 return Err(Error::CorruptObject(format!(
3311 "commit ancestry cycle at {parent}"
3312 )));
3313 }
3314 visited += 1;
3315 stack.push(ContainsFrame {
3316 oid: parent,
3317 parents: self.graph.parents_of(parent)?,
3318 next_parent: 0,
3319 });
3320 }
3321 Ok(false)
3322 }
3323}
3324
3325pub(crate) struct RefContainsBatch<'repo> {
3327 graph: CommitGraphCache<'repo>,
3328 tips: Vec<ObjectId>,
3329 tip_state: Vec<RefContainsTipState>,
3330 tip_index: OidHashMap<usize>,
3331}
3332
3333impl<'repo> RefContainsBatch<'repo> {
3334 pub(crate) fn new(repo: &'repo Repository) -> Self {
3336 Self {
3337 graph: CommitGraphCache::new(repo),
3338 tips: Vec::new(),
3339 tip_state: Vec::new(),
3340 tip_index: oid_hash_map(),
3341 }
3342 }
3343
3344 pub(crate) fn register_tip(&mut self, tip: ObjectId) -> usize {
3347 if let Some(&idx) = self.tip_index.get(&tip) {
3348 return idx;
3349 }
3350 let idx = self.tips.len();
3351 self.tips.push(tip);
3352 self.tip_state.push(RefContainsTipState {
3353 contains_match: false,
3354 no_contains_match: false,
3355 });
3356 self.tip_index.insert(tip, idx);
3357 idx
3358 }
3359
3360 pub(crate) fn run(
3366 &mut self,
3367 contains_targets: &[ObjectId],
3368 no_contains_targets: &[ObjectId],
3369 ) -> Result<()> {
3370 if self.tips.is_empty() || (contains_targets.is_empty() && no_contains_targets.is_empty()) {
3371 return Ok(());
3372 }
3373
3374 if self.graph.has_parent_rewrites()? {
3375 self.graph.graph = None;
3376 }
3377 self.graph.ensure_single_pack_reader();
3378
3379 for state in &mut self.tip_state {
3380 state.contains_match = contains_targets.is_empty();
3381 state.no_contains_match = false;
3382 }
3383
3384 let contains_set: OidHashSet = contains_targets.iter().copied().collect();
3385 let no_contains_set: OidHashSet = no_contains_targets.iter().copied().collect();
3386
3387 let filter_targets: Vec<ObjectId> = contains_targets
3388 .iter()
3389 .chain(no_contains_targets.iter())
3390 .copied()
3391 .collect();
3392 let min_target_gen = min_ranked_generation(&self.graph, &filter_targets);
3393 let need_contains = !contains_targets.is_empty();
3394 let need_no_contains = !no_contains_targets.is_empty();
3395
3396 let tip_count = self.tips.len();
3397 let mut walk = RefContainsWalk::new(tip_count);
3398 for (i, &tip) in self.tips.iter().enumerate() {
3399 walk.seed(&mut self.graph, i, tip)?;
3400 }
3401
3402 let tip_still_active = |state: &RefContainsTipState| -> bool {
3403 (need_contains && !state.contains_match)
3404 || (need_no_contains && !state.no_contains_match)
3405 };
3406
3407 while let Some((oid, indices_set)) = walk.pop() {
3408 if indices_set.is_empty() {
3409 continue;
3410 }
3411
3412 if contains_set.contains(&oid) {
3413 for i in indices_set.iter_indices() {
3414 self.tip_state[i].contains_match = true;
3415 }
3416 }
3417 if no_contains_set.contains(&oid) {
3418 for i in indices_set.iter_indices() {
3419 self.tip_state[i].no_contains_match = true;
3420 }
3421 }
3422
3423 let mut active = TipIndexBitset::with_capacity(tip_count);
3424 for i in indices_set.iter_indices() {
3425 if tip_still_active(&self.tip_state[i]) {
3426 active.set(i);
3427 }
3428 }
3429 if active.is_empty() {
3430 if self.tip_state.iter().all(|s| !tip_still_active(s)) {
3431 break;
3432 }
3433 continue;
3434 }
3435
3436 for parent in self.graph.parents_of(oid)? {
3437 if should_prune_ref_filter_parent(&self.graph, parent, min_target_gen) {
3438 continue;
3439 }
3440 walk.enqueue(&mut self.graph, parent, active.clone())?;
3441 }
3442 }
3443 Ok(())
3444 }
3445
3446 #[must_use]
3448 pub(crate) fn tip_state(&self, index: usize) -> &RefContainsTipState {
3449 &self.tip_state[index]
3450 }
3451}
3452
3453fn min_ranked_generation(graph: &CommitGraphCache<'_>, targets: &[ObjectId]) -> Option<u64> {
3454 let mut min_gen: Option<u64> = None;
3455 for &target in targets {
3456 let gen = commit_graph_rank(graph, target)?;
3457 min_gen = Some(min_gen.map_or(gen, |m| m.min(gen)));
3458 }
3459 min_gen
3460}
3461
3462fn should_prune_ref_filter_parent(
3463 graph: &CommitGraphCache<'_>,
3464 parent: ObjectId,
3465 min_target_gen: Option<u64>,
3466) -> bool {
3467 let Some(min_gen) = min_target_gen else {
3468 return false;
3469 };
3470 let Some(p_gen) = commit_graph_rank(graph, parent) else {
3471 return false;
3472 };
3473 p_gen < min_gen
3474}
3475
3476pub(crate) fn branch_tips_merged_into(
3484 repo: &Repository,
3485 bases: &[ObjectId],
3486 tips: &[ObjectId],
3487) -> Result<OidHashSet> {
3488 let mut cache = CommitGraphCache::new(repo);
3489 cache.tips_merged_from_bases(bases, tips)
3490}
3491
3492#[cfg(test)]
3493mod tests {
3494 use super::*;
3495 use crate::environment::RepositoryOptions;
3496 use crate::odb::Odb;
3497 use crate::repo::{InitOptions, Repository};
3498 use std::fs;
3499 use tempfile::tempdir;
3500
3501 #[test]
3502 fn containment_memo_shares_successes_and_exhausted_histories() -> Result<()> {
3503 for with_graph in [false, true] {
3504 let dir = tempdir()?;
3505 let repo = open_test_bare(&dir)?;
3506 let root = write_commit_same_time(repo.odb(), &[], "root", 100)?;
3507 let target = write_commit_same_time(repo.odb(), &[root], "target", 1000)?;
3508 let side = write_commit_same_time(repo.odb(), &[root], "side", 200)?;
3509 let child = write_commit_same_time(repo.odb(), &[target], "skewed", 150)?;
3510 let merge = write_commit_same_time(repo.odb(), &[side, child], "merge", 160)?;
3511 let repo = if with_graph {
3512 with_commit_graph_over_refs(&dir, repo, &[("refs/heads/main", merge)])?
3513 } else {
3514 repo
3515 };
3516 let mut memo = RefContainsMemo::new(&repo, &[target])?.expect("unrewritten parents");
3517 assert!(memo.contains(merge)?);
3518 assert_eq!(memo.known(side), Some(false));
3519 assert_eq!(memo.known.get(&child), Some(&true));
3520 assert!(!memo.contains(side)?);
3521 assert!(memo.contains(child)?);
3522 assert!(memo.contains(target)?);
3523 }
3524 Ok(())
3525 }
3526
3527 #[test]
3528 fn containment_memo_meets_old_side_history_without_reading_the_root() -> Result<()> {
3529 let dir = tempdir()?;
3530 let repo = open_test_bare(&dir)?;
3531 let root = write_commit_same_time(repo.odb(), &[], "root", 100)?;
3532 let mut trunk = root;
3533 let mut fork = root;
3534 for i in 0..5000 {
3535 trunk = write_commit_same_time(repo.odb(), &[trunk], &format!("trunk {i}"), 1000)?;
3536 if i == 4100 {
3537 fork = trunk;
3538 }
3539 }
3540 let target = trunk;
3541 let mut side = fork;
3542 for i in 0..2 {
3543 side = write_commit_same_time(repo.odb(), &[side], &format!("side {i}"), 200)?;
3544 }
3545 let mut memo = RefContainsMemo::new(&repo, &[target])?.expect("unrewritten parents");
3546 assert!(!memo.contains(side)?);
3547 assert!(
3548 !memo.known.contains_key(&root),
3549 "old history must stay unread"
3550 );
3551 assert!(!memo.contains(fork)?);
3552 Ok(())
3553 }
3554
3555 #[test]
3556 fn containment_memo_defers_parent_rewrites_to_batch() -> Result<()> {
3557 for replace in [false, true] {
3558 let dir = tempdir()?;
3559 let repo = open_test_bare(&dir)?;
3560 let old = write_commit_same_time(repo.odb(), &[], "old", 100)?;
3561 let target = write_commit_same_time(repo.odb(), &[], "target", 1000)?;
3562 let replacement = write_commit_same_time(repo.odb(), &[target], "replacement", 1100)?;
3563 let repo = with_commit_graph_over_refs(
3564 &dir,
3565 repo,
3566 &[("refs/heads/old", old), ("refs/heads/new", replacement)],
3567 )?;
3568 if replace {
3569 crate::refs::write_ref(
3570 repo.git_dir(),
3571 &format!("refs/replace/{old}"),
3572 &replacement,
3573 )?;
3574 } else {
3575 fs::create_dir_all(repo.git_dir().join("info"))?;
3576 fs::write(
3577 repo.git_dir().join("info/grafts"),
3578 format!("{old} {target}\n"),
3579 )?;
3580 }
3581 let repo = Repository::open(dir.path(), &RepositoryOptions::empty())?;
3582 assert!(RefContainsMemo::new(&repo, &[target])?.is_none());
3583 let mut batch = RefContainsBatch::new(&repo);
3584 let index = batch.register_tip(old);
3585 batch.run(&[target], &[])?;
3586 assert!(batch.graph.graph.is_none());
3587 assert!(batch.tip_state(index).contains_match);
3588 }
3589 Ok(())
3590 }
3591
3592 fn empty_tree(odb: &Odb) -> Result<ObjectId> {
3593 odb.write_loose_materialize(ObjectKind::Tree, b"")
3594 }
3595
3596 fn write_commit(
3597 odb: &Odb,
3598 parents: &[ObjectId],
3599 msg: &str,
3600 author_time: i64,
3601 committer_time: i64,
3602 ) -> Result<ObjectId> {
3603 let tree = empty_tree(odb)?;
3604 let mut body = format!("tree {tree}\n");
3605 for p in parents {
3606 body.push_str(&format!("parent {p}\n"));
3607 }
3608 body.push_str(&format!(
3609 "author T <t@e.com> {author_time} +0000\ncommitter T <t@e.com> {committer_time} +0000\n\n{msg}\n"
3610 ));
3611 odb.write_loose_materialize(ObjectKind::Commit, body.as_bytes())
3612 }
3613
3614 fn write_commit_same_time(
3615 odb: &Odb,
3616 parents: &[ObjectId],
3617 msg: &str,
3618 time: i64,
3619 ) -> Result<ObjectId> {
3620 write_commit(odb, parents, msg, time, time)
3621 }
3622
3623 fn open_test_bare(dir: &tempfile::TempDir) -> Result<Repository> {
3624 Repository::init(
3625 dir.path(),
3626 &InitOptions {
3627 bare: true,
3628
3629 ref_storage: crate::RefStorageFormat::Files,
3630 ..InitOptions::default()
3631 },
3632 )?;
3633 Repository::open(dir.path(), &RepositoryOptions::empty())
3634 }
3635
3636 #[test]
3637 fn parents_of_reads_through_one_buffer_without_session_copies() -> Result<()> {
3638 let dir = tempdir()?;
3639 let repo = open_test_bare(&dir)?;
3640 let root = write_commit_same_time(repo.odb(), &[], "root", 100)?;
3641 let mid = write_commit_same_time(repo.odb(), &[root], "mid", 200)?;
3642 let tip = write_commit_same_time(repo.odb(), &[mid], "tip with a longer message", 300)?;
3643 let tag = repo.odb().write_loose_materialize(
3644 ObjectKind::Tag,
3645 format!("object {tip}\ntype commit\ntag v1\ntagger T <t@e.com> 300 +0000\n\nv1\n")
3646 .as_bytes(),
3647 )?;
3648
3649 let mut cache = CommitGraphCache::with_commit_graph(&repo, false);
3650 cache.retained = Some(WalkedCommits::default());
3651 cache.retain_reads = true;
3652 assert_eq!(cache.parents_of(tip)?, vec![mid]);
3653 assert_eq!(cache.parents_of(mid)?, vec![root]);
3654 assert_eq!(cache.parents_of(root)?, Vec::<ObjectId>::new());
3655 for oid in [tip, mid, root] {
3656 assert!(
3657 !repo.odb().session_read_is_cached(&oid),
3658 "walked commit {oid} must not be copied into the session read cache"
3659 );
3660 }
3661 use crate::porcelain::log::CommitReuse;
3662 let mut retained = cache.retained.take().expect("retention enabled");
3663 for (oid, message) in [(tip, "tip with a longer message\n"), (root, "root\n")] {
3664 let data = retained
3665 .take_commit(&oid)
3666 .expect("walked commit is retained")?;
3667 assert_eq!(data.message, message);
3668 }
3669
3670 cache.retain_reads = false;
3671 assert_eq!(cache.parents_of(tag)?, vec![mid]);
3672 let _ = repo.odb().read(&mid)?;
3673 assert!(repo.odb().session_read_is_cached(&mid));
3674 Ok(())
3675 }
3676
3677 #[test]
3678 fn merge_bases_all_vs_union_matches_a_real_merge_of_the_others() -> Result<()> {
3679 let dir = tempdir().map_err(Error::Io)?;
3680 let repo = open_test_bare(&dir)?;
3681 let odb = repo.odb();
3682 let root = write_commit_same_time(&odb, &[], "root", 100)?;
3683 let a1 = write_commit_same_time(&odb, &[root], "a1", 200)?;
3684 let b1 = write_commit_same_time(&odb, &[root], "b1", 300)?;
3685 let ours = write_commit_same_time(&odb, &[a1, b1], "ours", 400)?;
3686 let theirs = write_commit_same_time(&odb, &[b1, a1], "theirs", 500)?;
3687 let c1 = write_commit_same_time(&odb, &[root], "c1", 600)?;
3688 let merged = write_commit_same_time(&odb, &[ours, c1], "virtual", 700)?;
3689 let other = write_commit_same_time(&odb, &[theirs, c1], "other", 800)?;
3690
3691 assert_eq!(merge_bases_all(&repo, &[ours, theirs])?, vec![b1, a1]);
3692 assert_eq!(
3693 merge_bases_all_vs_union(&repo, other, &[ours, c1])?,
3694 merge_bases_all(&repo, &[other, merged])?,
3695 );
3696 assert_eq!(
3697 merge_bases_all_vs_union(&repo, other, &[ours, c1])?,
3698 vec![c1, b1, a1]
3699 );
3700 assert_eq!(merge_bases_all_vs_union(&repo, a1, &[a1, b1])?, vec![a1]);
3701 Ok(())
3702 }
3703
3704 #[test]
3705 fn merge_bases_with_equal_dates_keep_discovery_order() -> Result<()> {
3706 let dir = tempdir().map_err(Error::Io)?;
3707 let repo = open_test_bare(&dir)?;
3708 let odb = repo.odb();
3709 let root = write_commit_same_time(&odb, &[], "root", 100)?;
3710 let sides: Vec<ObjectId> = (0..4)
3711 .map(|i| write_commit_same_time(&odb, &[root], &format!("side {i}"), 100))
3712 .collect::<Result<_>>()?;
3713 let left = write_commit_same_time(&odb, &sides, "left", 100)?;
3714 let mut reversed = sides.clone();
3715 reversed.reverse();
3716 let right = write_commit_same_time(&odb, &reversed, "right", 100)?;
3717 assert_eq!(merge_bases_all(&repo, &[left, right])?, sides);
3719 assert_eq!(merge_bases_all(&repo, &[right, left])?, reversed);
3720 Ok(())
3721 }
3722
3723 #[test]
3724 fn merge_base_unranked_deep_linear_finds_base_without_full_history_scan() -> Result<()> {
3725 use super::intersect_walk_test_hooks::{
3726 commit_parent_lookups, reset_commit_parent_lookups, reset_intersect_walk_steps,
3727 };
3728
3729 let dir = tempdir().map_err(Error::Io)?;
3730 let repo = open_test_bare(&dir)?;
3731 let mut parent = write_commit_same_time(&repo.odb(), &[], "root", 100)?;
3732 for i in 1..3000 {
3733 parent = write_commit_same_time(&repo.odb(), &[parent], &format!("c{i}"), 100 + i)?;
3734 }
3735 let tip = parent;
3736 let mut old = tip;
3737 for _ in 0..1500 {
3738 old = first_parent_oid(&mut CommitGraphCache::new(&repo), old)?;
3739 }
3740 {
3741 let mut link = repo
3742 .caches()
3743 .commit_linkage()
3744 .lock()
3745 .unwrap_or_else(|e| e.into_inner());
3746 link.parents.clear();
3747 link.times.clear();
3748 }
3749 reset_intersect_walk_steps();
3750 reset_commit_parent_lookups();
3751 {
3752 let mut link = repo
3753 .caches()
3754 .commit_linkage()
3755 .lock()
3756 .unwrap_or_else(|e| e.into_inner());
3757 link.parents.clear();
3758 link.times.clear();
3759 }
3760 let bases = merge_bases_first_vs_rest(&repo, tip, &[old])?;
3761 assert_eq!(bases, vec![old]);
3762 let parent_reads = commit_parent_lookups();
3763 assert!(
3764 parent_reads <= 1600,
3765 "merge-base should not parse every commit on a 3000-commit chain \
3766 (parent lookups {parent_reads}, intersect dequeue steps {})",
3767 super::intersect_walk_test_hooks::intersect_walk_steps()
3768 );
3769 assert!(
3770 parent_reads >= 1400,
3771 "sanity: should walk from tip toward merge base (parent lookups {parent_reads})"
3772 );
3773 Ok(())
3774 }
3775
3776 #[test]
3779 fn merge_base_of_short_branches_stops_above_the_shared_tail() -> Result<()> {
3780 use super::intersect_walk_test_hooks::{intersect_walk_steps, reset_intersect_walk_steps};
3781
3782 let dir = tempdir().map_err(Error::Io)?;
3783 let repo = open_test_bare(&dir)?;
3784 let chain = write_chain(&repo, 2000, |i| 100 + i as i64)?;
3785 let fork = chain[1000];
3786 let mut sides = [fork, fork];
3787 for (side, tip) in sides.iter_mut().enumerate() {
3788 for i in 0..3 {
3789 *tip = write_commit_same_time(
3790 &repo.odb(),
3791 &[*tip],
3792 &format!("side{side}-{i}"),
3793 5_000 + i,
3794 )?;
3795 }
3796 }
3797 for all in [false, true] {
3798 clear_commit_linkage(&repo);
3799 reset_intersect_walk_steps();
3800 let bases = if all {
3801 merge_bases_all(&repo, &sides)?
3802 } else {
3803 merge_bases_first_vs_rest(&repo, sides[0], &sides[1..])?
3804 };
3805 assert_eq!(bases, vec![fork], "all={all}");
3806 let steps = intersect_walk_steps();
3807 assert!(
3808 steps <= 16,
3809 "all={all}: walk dequeued {steps} commits for a 7-commit fork"
3810 );
3811 }
3812 let tip = chain[chain.len() - 1];
3813 for pair in [[tip, fork], [fork, tip]] {
3814 reset_intersect_walk_steps();
3815 assert_eq!(merge_bases_all(&repo, &pair)?, vec![fork]);
3816 assert_eq!(
3817 intersect_walk_steps(),
3818 0,
3819 "an ancestor tip is the only merge base, found on the first-parent chain"
3820 );
3821 }
3822 Ok(())
3823 }
3824
3825 fn clear_commit_linkage(repo: &Repository) {
3826 let mut link = repo
3827 .caches()
3828 .commit_linkage()
3829 .lock()
3830 .unwrap_or_else(|e| e.into_inner());
3831 link.parents.clear();
3832 link.times.clear();
3833 }
3834
3835 fn write_chain(
3837 repo: &Repository,
3838 len: usize,
3839 time: impl Fn(usize) -> i64,
3840 ) -> Result<Vec<ObjectId>> {
3841 let mut chain = vec![write_commit_same_time(&repo.odb(), &[], "c0", time(0))?];
3842 for i in 1..len {
3843 let parent = chain[i - 1];
3844 chain.push(write_commit_same_time(
3845 &repo.odb(),
3846 &[parent],
3847 &format!("c{i}"),
3848 time(i),
3849 )?);
3850 }
3851 Ok(chain)
3852 }
3853
3854 fn relation_with_lookups(
3856 repo: &Repository,
3857 first: ObjectId,
3858 second: ObjectId,
3859 ) -> (FirstParentRelation, usize) {
3860 use super::intersect_walk_test_hooks::{
3861 commit_parent_lookups, reset_commit_parent_lookups,
3862 };
3863 clear_commit_linkage(repo);
3864 reset_commit_parent_lookups();
3865 let relation = first_parent_chain_relation(&mut CommitGraphCache::new(repo), first, second);
3866 (relation, commit_parent_lookups())
3867 }
3868
3869 #[test]
3870 fn first_parent_chain_relation_stops_near_the_answer() -> Result<()> {
3871 let dir = tempdir().map_err(Error::Io)?;
3872 let repo = open_test_bare(&dir)?;
3873 let chain = write_chain(&repo, 3000, |i| 100 + i as i64)?;
3874 let tip = chain[2999];
3875 let old = chain[2499];
3876
3877 let (relation, lookups) = relation_with_lookups(&repo, tip, old);
3878 assert_eq!(relation, FirstParentRelation::SecondIsAncestor);
3879 assert!(
3880 lookups <= 510,
3881 "descendant first: must not walk the ancestor's 2500-commit chain ({lookups})"
3882 );
3883
3884 let (relation, lookups) = relation_with_lookups(&repo, old, tip);
3885 assert_eq!(relation, FirstParentRelation::FirstIsAncestor);
3886 assert!(
3887 lookups <= 510,
3888 "ancestor first: must not walk the ancestor's 2500-commit chain ({lookups})"
3889 );
3890
3891 assert_eq!(
3892 relation_with_lookups(&repo, tip, tip).0,
3893 FirstParentRelation::Same
3894 );
3895 Ok(())
3896 }
3897
3898 #[test]
3899 fn first_parent_chain_relation_equal_and_skewed_dates_stay_correct() -> Result<()> {
3900 let dir = tempdir().map_err(Error::Io)?;
3901 let repo = open_test_bare(&dir)?;
3902
3903 let flat = write_chain(&repo, 2000, |_| 100)?;
3904 let (relation, lookups) = relation_with_lookups(&repo, flat[1999], flat[1499]);
3905 assert_eq!(relation, FirstParentRelation::SecondIsAncestor);
3906 assert!(
3907 lookups <= 1010,
3908 "equal dates step both chains in lockstep ({lookups})"
3909 );
3910 let (relation, _) = relation_with_lookups(&repo, flat[1499], flat[1999]);
3911 assert_eq!(relation, FirstParentRelation::FirstIsAncestor);
3912
3913 let skewed = write_chain(&repo, 300, |i| 10_000 - i as i64)?;
3916 assert_eq!(
3917 relation_with_lookups(&repo, skewed[299], skewed[100]).0,
3918 FirstParentRelation::SecondIsAncestor
3919 );
3920 assert_eq!(
3921 relation_with_lookups(&repo, skewed[100], skewed[299]).0,
3922 FirstParentRelation::FirstIsAncestor
3923 );
3924 Ok(())
3925 }
3926
3927 #[test]
3928 fn first_parent_chain_relation_forked_branches_stop_where_chains_meet() -> Result<()> {
3929 let dir = tempdir().map_err(Error::Io)?;
3930 let repo = open_test_bare(&dir)?;
3931 let trunk = write_chain(&repo, 2000, |i| 100 + i as i64)?;
3932 let fork = trunk[1999];
3933 let mut tip_a = fork;
3934 let mut tip_b = fork;
3935 for i in 0..20 {
3936 tip_a = write_commit_same_time(&repo.odb(), &[tip_a], &format!("a{i}"), 5_000 + i)?;
3937 tip_b = write_commit_same_time(&repo.odb(), &[tip_b], &format!("b{i}"), 6_000 + i)?;
3938 }
3939 let (relation, lookups) = relation_with_lookups(&repo, tip_a, tip_b);
3940 assert_eq!(relation, FirstParentRelation::Unrelated);
3941 assert!(
3942 lookups <= 60,
3943 "forked tips must stop at the fork, not scan the 2000-commit trunk ({lookups})"
3944 );
3945
3946 let merge = write_commit_same_time(&repo.odb(), &[tip_a, tip_b], "merge", 7_000)?;
3948 assert_eq!(
3949 relation_with_lookups(&repo, merge, tip_b).0,
3950 FirstParentRelation::Unrelated
3951 );
3952 assert_eq!(
3953 relation_with_lookups(&repo, merge, tip_a).0,
3954 FirstParentRelation::SecondIsAncestor
3955 );
3956 Ok(())
3957 }
3958
3959 #[test]
3960 fn graph_range_counts_are_bounded_by_distance() -> Result<()> {
3961 use super::intersect_walk_test_hooks::{
3962 commit_parent_lookups, reset_commit_parent_lookups,
3963 };
3964 use crate::rev_list::{rev_list, RevListOptions};
3965
3966 let dir = tempdir().map_err(Error::Io)?;
3967 let repo = open_test_bare(&dir)?;
3968 let chain = write_chain(&repo, 3000, |i| 100 + i as i64)?;
3969 std::fs::write(
3970 repo.git_dir().join("refs/heads/main"),
3971 format!("{}\n", chain[2999]),
3972 )?;
3973 crate::maintenance::write_commit_graph(
3974 &repo,
3975 &crate::maintenance::CommitGraphOptions::default(),
3976 )?;
3977 let mut lookups = Vec::new();
3978 for low_level in [false, true] {
3979 clear_commit_linkage(&repo);
3980 reset_commit_parent_lookups();
3981 let count = if low_level {
3982 rev_list(
3983 &repo,
3984 &[chain[2999].to_hex()],
3985 &[chain[2979].to_hex()],
3986 &RevListOptions {
3987 count: true,
3988 use_commit_graph: true,
3989 ..Default::default()
3990 },
3991 )?
3992 .commit_count()
3993 } else {
3994 repo.rev_walk().count(
3995 &["main~20..main"],
3996 &crate::revwalk::RevWalkListOptions::default(),
3997 )?
3998 };
3999 assert_eq!(count, 20);
4000 lookups.push(commit_parent_lookups());
4001 }
4002 assert!(
4003 lookups.iter().all(|&n| n <= 60),
4004 "graph-backed range counts must stay near the range, parent lookups: {lookups:?}"
4005 );
4006 Ok(())
4007 }
4008
4009 #[test]
4010 fn count_excluding_ancestors_on_deep_chain_is_bounded_by_distance() -> Result<()> {
4011 use super::intersect_walk_test_hooks::{
4012 commit_parent_lookups, reset_commit_parent_lookups,
4013 };
4014
4015 let dir = tempdir().map_err(Error::Io)?;
4016 let repo = open_test_bare(&dir)?;
4017 let chain = write_chain(&repo, 3000, |i| 100 + i as i64)?;
4018 let tip = chain[2999];
4019 let old = chain[2499];
4020
4021 clear_commit_linkage(&repo);
4022 reset_commit_parent_lookups();
4023 let filter = ReachableCountFilter::default();
4024 assert_eq!(
4025 count_commits_reachable_excluding_ancestors_of(&repo, &[tip], &[old], filter)?,
4026 500
4027 );
4028 assert_eq!(
4029 count_commits_reachable_excluding_ancestors_of(&repo, &[old], &[tip], filter)?,
4030 0
4031 );
4032 assert_eq!(merge_bases_first_vs_rest(&repo, old, &[tip])?, vec![old]);
4033 assert_eq!(merge_bases_first_vs_rest(&repo, tip, &[old])?, vec![old]);
4034 let lookups = commit_parent_lookups();
4035 assert!(
4036 lookups <= 520,
4037 "left/right counts on a deep chain must not walk to the root ({lookups})"
4038 );
4039
4040 let since = ReachableCountFilter {
4041 since: Some(100 + 2900),
4042 ..ReachableCountFilter::default()
4043 };
4044 assert_eq!(
4045 count_commits_reachable_excluding_ancestors_of(&repo, &[tip], &[old], since)?,
4046 100
4047 );
4048 Ok(())
4049 }
4050
4051 fn write_merge_trunk(repo: &Repository, len: usize) -> Result<Vec<ObjectId>> {
4056 let odb = repo.odb();
4057 let mut trunk = vec![write_commit_same_time(&odb, &[], "t0", 0)?];
4058 for i in 1..len {
4059 let base = if i % 7 == 0 && i > 30 {
4060 trunk[i - 30]
4061 } else {
4062 trunk[i - 1]
4063 };
4064 let side = write_commit_same_time(&odb, &[base], &format!("s{i}"), 10 * i as i64)?;
4065 trunk.push(write_commit_same_time(
4066 &odb,
4067 &[trunk[i - 1], side],
4068 &format!("t{i}"),
4069 10 * i as i64 + 5,
4070 )?);
4071 }
4072 Ok(trunk)
4073 }
4074
4075 #[test]
4076 fn count_excluding_ancestors_on_merge_heavy_range_is_linear() -> Result<()> {
4077 use super::intersect_walk_test_hooks::{
4078 commit_parent_lookups, commit_parent_queries, reset_commit_parent_lookups,
4079 reset_commit_parent_queries,
4080 };
4081
4082 const LEN: usize = 1600;
4083 const OLD: usize = 600;
4084 const RANGE: usize = 2 * (LEN - 1 - OLD);
4085
4086 let dir = tempdir().map_err(Error::Io)?;
4087 let repo = open_test_bare(&dir)?;
4088 let trunk = write_merge_trunk(&repo, LEN)?;
4089 let tip = trunk[LEN - 1];
4090 let old = trunk[OLD];
4091 let merges_in_range = LEN - 1 - OLD;
4092 let newest_ten = 10 * (LEN - 10) as i64;
4093 let oldest_ten = 10 * (OLD + 10) as i64 + 5;
4094 let cases = [
4095 (ReachableCountFilter::default(), RANGE),
4096 (
4097 ReachableCountFilter {
4098 first_parent_only: true,
4099 ..ReachableCountFilter::default()
4100 },
4101 merges_in_range,
4102 ),
4103 (
4104 ReachableCountFilter {
4105 min_parents: Some(2),
4106 ..ReachableCountFilter::default()
4107 },
4108 merges_in_range,
4109 ),
4110 (
4111 ReachableCountFilter {
4112 max_parents: Some(1),
4113 ..ReachableCountFilter::default()
4114 },
4115 merges_in_range,
4116 ),
4117 (
4118 ReachableCountFilter {
4119 since: Some(newest_ten),
4120 ..ReachableCountFilter::default()
4121 },
4122 20,
4123 ),
4124 (
4125 ReachableCountFilter {
4126 until: Some(oldest_ten),
4127 ..ReachableCountFilter::default()
4128 },
4129 20,
4130 ),
4131 (
4132 ReachableCountFilter {
4133 since: Some(newest_ten),
4134 first_parent_only: true,
4135 ..ReachableCountFilter::default()
4136 },
4137 10,
4138 ),
4139 ];
4140
4141 let listed = commits_reachable_excluding_ancestors_of(&repo, &[tip], &[old], None)?;
4142 assert_eq!(listed.len(), RANGE);
4143
4144 for with_graph in [false, true] {
4145 if with_graph {
4146 std::fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))?;
4147 crate::maintenance::write_commit_graph(
4148 &repo,
4149 &crate::maintenance::CommitGraphOptions::default(),
4150 )?;
4151 }
4152 for (filter, expected) in cases {
4153 clear_commit_linkage(&repo);
4154 reset_commit_parent_lookups();
4155 reset_commit_parent_queries();
4156 assert_eq!(
4157 count_commits_reachable_excluding_ancestors_of(&repo, &[tip], &[old], filter)?,
4158 expected,
4159 "{filter:?}, commit-graph: {with_graph}"
4160 );
4161 let (lookups, queries) = (commit_parent_lookups(), commit_parent_queries());
4162 assert!(
4163 lookups <= RANGE + 200 && queries <= 4 * RANGE,
4164 "counting must walk the range once, not re-check ancestry per commit \
4165 ({filter:?}, commit-graph: {with_graph}, lookups {lookups}, queries {queries})"
4166 );
4167 assert_eq!(
4168 count_commits_reachable_excluding_ancestors_of(&repo, &[old], &[tip], filter)?,
4169 0
4170 );
4171 }
4172 }
4173 Ok(())
4174 }
4175
4176 #[test]
4177 fn ahead_behind_one_commit_on_deep_chain_is_linear_not_quadratic() -> Result<()> {
4178 use super::intersect_walk_test_hooks::{
4179 commit_parent_lookups, reset_commit_parent_lookups,
4180 };
4181
4182 let dir = tempdir().map_err(Error::Io)?;
4183 let repo = open_test_bare(&dir)?;
4184 let chain = write_chain(&repo, 3000, |i| 100 + i as i64)?;
4185 let tip = chain[2999];
4186 let upstream = chain[2998];
4187
4188 clear_commit_linkage(&repo);
4189 reset_commit_parent_lookups();
4190 let (ahead, behind, retained, _) =
4191 ahead_behind_of_target_commits(&repo, tip, upstream, 1, 0)?;
4192 assert_eq!((ahead, behind), (1, 0));
4193 assert_eq!(retained, vec![tip]);
4194 let lookups = commit_parent_lookups();
4195 assert!(
4196 lookups <= 6500,
4197 "one-commit-ahead status must not re-check ancestry per walked commit ({lookups})"
4198 );
4199
4200 clear_commit_linkage(&repo);
4201 reset_commit_parent_lookups();
4202 let (ahead2, behind2, _, behind_retained) =
4203 ahead_behind_of_target_commits(&repo, upstream, tip, 0, 1)?;
4204 assert_eq!((ahead2, behind2), (0, 1));
4205 assert_eq!(behind_retained, vec![tip]);
4206 let lookups = commit_parent_lookups();
4207 assert!(
4208 lookups <= 6500,
4209 "one-commit-behind status must not re-check ancestry per walked commit ({lookups})"
4210 );
4211 Ok(())
4212 }
4213
4214 #[test]
4215 fn merge_base_ranked_sibling_tips_do_not_scan_trunk_on_direct_probe() -> Result<()> {
4216 use super::intersect_walk_test_hooks::{
4217 commit_parent_lookups, reset_commit_parent_lookups,
4218 };
4219
4220 const TRUNK_LEN: usize = 2001;
4221 const BRANCH_LEN: usize = 20;
4222
4223 let dir = tempdir().map_err(Error::Io)?;
4224 let repo = open_test_bare(&dir)?;
4225 let mut parent = write_commit_same_time(&repo.odb(), &[], "root", 100)?;
4226 for i in 1..TRUNK_LEN {
4227 parent = write_commit_same_time(
4228 &repo.odb(),
4229 &[parent],
4230 &format!("trunk{i}"),
4231 100 + i as i64,
4232 )?;
4233 }
4234 let fork = parent;
4235 let mut tip_a = fork;
4236 for i in 0..BRANCH_LEN {
4237 tip_a = write_commit_same_time(
4238 &repo.odb(),
4239 &[tip_a],
4240 &format!("a{i}"),
4241 100 + TRUNK_LEN as i64 + i as i64,
4242 )?;
4243 }
4244 let mut tip_b = fork;
4245 for i in 0..BRANCH_LEN {
4246 tip_b = write_commit_same_time(
4247 &repo.odb(),
4248 &[tip_b],
4249 &format!("b{i}"),
4250 100 + TRUNK_LEN as i64 + BRANCH_LEN as i64 + i as i64,
4251 )?;
4252 }
4253 use crate::refs::write_ref;
4254 write_ref(&repo.git_dir(), "refs/heads/branch-a", &tip_a)?;
4255 write_ref(&repo.git_dir(), "refs/heads/branch-b", &tip_b)?;
4256 write_ref(&repo.git_dir(), "refs/heads/main", &fork)?;
4257 repo.odb().write_commit_graph()?;
4258 assert!(
4259 dir.path().join("objects/info/commit-graph").is_file(),
4260 "test fixture must write a commit-graph file"
4261 );
4262 let repo = open_test_bare(&dir)?;
4263 {
4264 let mut cache = CommitGraphCache::new(&repo);
4265 assert_ne!(
4266 commit_rank_for_walk(&mut cache, tip_a),
4267 UNRANKED_COMMIT,
4268 "commit-graph must supply generations for ranked merge-base probes"
4269 );
4270 assert_ne!(commit_rank_for_walk(&mut cache, tip_b), UNRANKED_COMMIT);
4271 assert_ne!(commit_rank_for_walk(&mut cache, fork), UNRANKED_COMMIT);
4272 }
4273
4274 reset_commit_parent_lookups();
4275 for _ in 0..10 {
4276 let bases = merge_bases_first_vs_rest(&repo, tip_a, &[tip_b])?;
4277 assert_eq!(bases, vec![fork]);
4278 }
4279 let parent_reads = commit_parent_lookups();
4280 assert!(
4281 parent_reads < 600,
4282 "ranked direct-ancestor probes must not walk the {TRUNK_LEN}-commit trunk \
4283 (parent lookups {parent_reads} across 10 calls; full trunk scan would be \
4284 ~{TRUNK_LEN} per failed probe)"
4285 );
4286 Ok(())
4287 }
4288
4289 #[test]
4290 fn commit_rank_for_walk_unranked_without_commit_graph_despite_dates() -> Result<()> {
4291 let dir = tempdir().map_err(Error::Io)?;
4292 let repo = open_test_bare(&dir)?;
4293 let old = write_commit_same_time(&repo.odb(), &[], "old", 946_684_800)?;
4294 let new = write_commit_same_time(&repo.odb(), &[old], "new", 1_893_456_000)?;
4295 let mut cache = CommitGraphCache::new(&repo);
4296 assert_eq!(
4297 commit_rank_for_walk(&mut cache, new),
4298 UNRANKED_COMMIT,
4299 "generation probes must not treat committer dates as ranks without a commit-graph"
4300 );
4301 assert_eq!(commit_rank_for_walk(&mut cache, old), UNRANKED_COMMIT);
4302 Ok(())
4303 }
4304
4305 #[test]
4306 fn merge_base_linear_ancestor_uses_generation_shortcut() -> Result<()> {
4307 let dir = tempdir().map_err(Error::Io)?;
4308 let repo = open_test_bare(&dir)?;
4309 let mut parent = write_commit_same_time(&repo.odb(), &[], "root", 100)?;
4310 for i in 1..50 {
4311 parent = write_commit_same_time(&repo.odb(), &[parent], &format!("c{i}"), 100 + i)?;
4312 }
4313 let tip = parent;
4314 let old = {
4315 let mut p = tip;
4316 for _ in 0..20 {
4317 p = first_parent_oid(&mut CommitGraphCache::new(&repo), p)?;
4318 }
4319 p
4320 };
4321 let bases = merge_bases_first_vs_rest(&repo, tip, &[old])?;
4322 assert_eq!(bases, vec![old]);
4323 Ok(())
4324 }
4325
4326 #[test]
4327 fn commits_reachable_excluding_respects_limit() -> Result<()> {
4328 let dir = tempdir().map_err(Error::Io)?;
4329 let repo = open_test_bare(&dir)?;
4330 let c1 = write_commit_same_time(&repo.odb(), &[], "one", 100)?;
4331 let c2 = write_commit_same_time(&repo.odb(), &[c1], "two", 200)?;
4332 let c3 = write_commit_same_time(&repo.odb(), &[c2], "three", 300)?;
4333
4334 let all = commits_reachable_excluding_ancestors_of(&repo, &[c3], &[], None)?;
4335 assert_eq!(all, vec![c3, c2, c1]);
4336
4337 let limited = commits_reachable_excluding_ancestors_of(&repo, &[c3], &[], Some(2))?;
4338 assert_eq!(limited, vec![c3, c2]);
4339
4340 let (total, retained) = ahead_of_target_commits(&repo, c3, c1, 1)?;
4341 assert_eq!(total, 2);
4342 assert_eq!(retained, vec![c3]);
4343
4344 let ahead = commits_reachable_excluding_ancestors_of(&repo, &[c3], &[c1], None)?;
4345 assert_eq!(ahead, vec![c3, c2]);
4346
4347 Ok(())
4348 }
4349
4350 #[test]
4351 fn commits_reachable_excluding_honors_grafts() -> Result<()> {
4352 let dir = tempdir().map_err(Error::Io)?;
4353 let repo = open_test_bare(&dir)?;
4354 let root = write_commit_same_time(&repo.odb(), &[], "root", 100)?;
4355 let mid = write_commit_same_time(&repo.odb(), &[root], "mid", 200)?;
4356 let tip = write_commit_same_time(&repo.odb(), &[mid], "tip", 300)?;
4357 let graft_path = repo.git_dir().join("info/grafts");
4358 fs::create_dir_all(graft_path.parent().expect("graft parent")).map_err(Error::Io)?;
4359 fs::write(&graft_path, format!("{tip} {root}\n")).map_err(Error::Io)?;
4360 let out = commits_reachable_excluding_ancestors_of(&repo, &[tip], &[root], None)?;
4361 assert_eq!(out, vec![tip]);
4362 Ok(())
4363 }
4364
4365 #[test]
4366 fn commits_reachable_excluding_stops_at_shallow_boundary() -> Result<()> {
4367 let dir = tempdir().map_err(Error::Io)?;
4368 let repo = open_test_bare(&dir)?;
4369 let missing: ObjectId = "0000000000000000000000000000000000000001"
4370 .parse()
4371 .expect("oid");
4372 let tip = write_commit_same_time(&repo.odb(), &[missing], "tip", 100)?;
4373
4374 fs::write(repo.git_dir().join("shallow"), format!("{tip}\n")).map_err(Error::Io)?;
4375
4376 let out = commits_reachable_excluding_ancestors_of(&repo, &[tip], &[], None)?;
4377 assert_eq!(out, vec![tip]);
4378
4379 Ok(())
4380 }
4381
4382 #[test]
4383 fn count_symmetric_ahead_behind_and_branch_base() -> Result<()> {
4384 let dir = tempdir().map_err(Error::Io)?;
4385 let repo = open_test_bare(&dir)?;
4386 let c1 = write_commit_same_time(&repo.odb(), &[], "one", 100)?;
4387 let c2 = write_commit_same_time(&repo.odb(), &[c1], "two", 200)?;
4388 let c3 = write_commit_same_time(&repo.odb(), &[c2], "three", 300)?;
4389 let (ahead, behind) = count_symmetric_ahead_behind(&repo, c3, c1)?;
4390 assert_eq!(ahead, 2);
4391 assert_eq!(behind, 0);
4392 let (ahead2, behind2, retained, _) = ahead_behind_of_target_commits(&repo, c3, c1, 1, 0)?;
4393 assert_eq!((ahead2, behind2), (2, 0));
4394 assert_eq!(retained.len(), 1);
4395 assert_eq!(retained[0], c3);
4396 let idx = branch_base_for_tip(&repo, c3, &[c1, c2])?;
4397 assert!(idx.is_some());
4398 Ok(())
4399 }
4400
4401 #[test]
4402 fn merge_base_for_diff_helpers() -> Result<()> {
4403 let dir = tempdir().map_err(Error::Io)?;
4404 let repo = open_test_bare(&dir)?;
4405 let c1 = write_commit_same_time(&repo.odb(), &[], "one", 100)?;
4406 let c2 = write_commit_same_time(&repo.odb(), &[c1], "two", 200)?;
4407 let mb = merge_base_for_diff_two_commits(&repo, c2, c1).expect("mb");
4408 assert_eq!(mb, c1);
4409 let mb_idx = merge_base_for_diff_index(&repo, c2, c1).expect("mb idx");
4410 assert_eq!(mb_idx, c1);
4411 Ok(())
4412 }
4413
4414 #[test]
4415 fn reachable_walk_since_cutoff_terminates_without_pending() -> Result<()> {
4416 let dir = tempdir().map_err(Error::Io)?;
4417 let repo = open_test_bare(&dir)?;
4418 let c1 = write_commit(&repo.odb(), &[], "one", 100, 100)?;
4419 let c2 = write_commit(&repo.odb(), &[c1], "two", 200, 200)?;
4420 let c3 = write_commit(&repo.odb(), &[c2], "three", 300, 300)?;
4421 let tip = write_commit(&repo.odb(), &[c3], "tip", 400, 400)?;
4422
4423 let cutoffs = ReachableWalkCommitterCutoffs {
4424 since: Some(250),
4425 until: None,
4426 };
4427 let mut state = ReachableWalkState::new(&repo, &[tip], &[])?;
4428 let page1 = state.emit_until(&repo, Some(1), cutoffs)?;
4429 assert_eq!(page1.len(), 1);
4430 assert_eq!(page1[0], tip);
4431 assert!(state.has_pending());
4432
4433 let page2 = state.emit_until(&repo, Some(10), cutoffs)?;
4434 assert_eq!(page2, vec![c3]);
4435 assert!(
4436 !state.has_pending(),
4437 "walk must finish once every reachable commit is older than --since"
4438 );
4439
4440 let all = state.emit_until(&repo, None, cutoffs)?;
4441 assert!(all.is_empty());
4442
4443 Ok(())
4444 }
4445
4446 #[test]
4447 fn commits_reachable_errors_on_missing_parent_without_shallow() -> Result<()> {
4448 let dir = tempdir().map_err(Error::Io)?;
4449 let repo = open_test_bare(&dir)?;
4450 let missing: ObjectId = "0000000000000000000000000000000000000001"
4451 .parse()
4452 .expect("oid");
4453 let tip = write_commit_same_time(&repo.odb(), &[missing], "tip", 100)?;
4454
4455 let err = commits_reachable_excluding_ancestors_of(&repo, &[tip], &[], None).unwrap_err();
4456 assert!(matches!(err, Error::ObjectNotFound(_)));
4457
4458 Ok(())
4459 }
4460
4461 #[test]
4462 fn odb_walk_sees_quarantined_commits_that_repo_odb_cannot() -> Result<()> {
4463 let dir = tempdir().map_err(Error::Io)?;
4464 let repo = open_test_bare(&dir)?;
4465 let old = write_commit_same_time(&repo.odb(), &[], "old", 100)?;
4466 let quarantine =
4467 crate::receive_quarantine::ReceiveQuarantine::create(&repo.odb(), repo.git_dir())?;
4468 let q_odb = quarantine.odb();
4469 let mid = write_commit_same_time(&q_odb, &[old], "mid", 200)?;
4470 let new = write_commit_same_time(&q_odb, &[mid], "new", 300)?;
4471
4472 assert!(!repo.odb().exists(&new));
4473 assert!(is_ancestor_odb_walk(&q_odb, old, new)?);
4474 assert!(!is_ancestor_odb_walk(&q_odb, new, old)?);
4475 Ok(())
4476 }
4477
4478 #[test]
4479 fn odb_walk_handles_merges_skew_and_non_commits() -> Result<()> {
4480 let dir = tempdir().map_err(Error::Io)?;
4481 let repo = open_test_bare(&dir)?;
4482 let odb = repo.odb();
4483 let root = write_commit_same_time(&odb, &[], "root", 1_000)?;
4484 let skewed = write_commit_same_time(&odb, &[root], "skewed", 9_000)?;
4486 let mut main = root;
4487 for i in 0..20 {
4488 main = write_commit_same_time(&odb, &[main], &format!("m{i}"), 2_000 + i)?;
4489 }
4490 let merge = write_commit_same_time(&odb, &[main, skewed], "merge", 3_000)?;
4491 let unrelated = write_commit_same_time(&odb, &[], "unrelated", 2_500)?;
4492
4493 assert!(is_ancestor_odb_walk(&odb, root, merge)?);
4494 assert!(is_ancestor_odb_walk(&odb, skewed, merge)?);
4495 assert!(is_ancestor_odb_walk(&odb, merge, merge)?);
4496 assert!(!is_ancestor_odb_walk(&odb, merge, root)?);
4497 assert!(!is_ancestor_odb_walk(&odb, unrelated, merge)?);
4498
4499 let tree = empty_tree(&odb)?;
4500 assert!(!is_ancestor_odb_walk(&odb, tree, merge)?);
4501 assert!(!is_ancestor_odb_walk(&odb, root, tree)?);
4502 Ok(())
4503 }
4504
4505 #[test]
4506 fn odb_walk_treats_missing_parents_as_history_end() -> Result<()> {
4507 let dir = tempdir().map_err(Error::Io)?;
4508 let repo = open_test_bare(&dir)?;
4509 let odb = repo.odb();
4510 let missing: ObjectId = "0000000000000000000000000000000000000001"
4511 .parse()
4512 .expect("oid");
4513 let base = write_commit_same_time(&odb, &[], "base", 50)?;
4514 let tip = write_commit_same_time(&odb, &[missing], "tip", 100)?;
4515
4516 assert!(!is_ancestor_odb_walk(&odb, base, tip)?);
4517 assert!(matches!(
4518 is_ancestor_odb_walk(&odb, missing, base),
4519 Err(Error::ObjectNotFound(_))
4520 ));
4521 Ok(())
4522 }
4523
4524 const YEAR_2035: i64 = 2_051_222_400;
4526 const YEAR_2000: i64 = 946_684_800;
4527
4528 #[test]
4529 fn odb_walk_follows_descendants_dated_before_the_ancestor() -> Result<()> {
4530 let dir = tempdir().map_err(Error::Io)?;
4531 let repo = open_test_bare(&dir)?;
4532 let odb = repo.odb();
4533 let root = write_commit_same_time(&odb, &[], "root", YEAR_2000 - 10)?;
4534 let ancestor = write_commit_same_time(&odb, &[root], "future", YEAR_2035)?;
4535 let mut tip = ancestor;
4536 for i in 0..250 {
4537 tip = write_commit_same_time(&odb, &[tip], &format!("skewed{i}"), YEAR_2000 + i)?;
4538 }
4539 let unrelated = write_commit_same_time(&odb, &[], "unrelated", YEAR_2035)?;
4540
4541 assert!(is_ancestor_odb_walk(&odb, ancestor, tip)?);
4542 assert!(is_ancestor_odb_walk(&odb, root, tip)?);
4543 assert!(!is_ancestor_odb_walk(&odb, unrelated, tip)?);
4544 assert!(!is_ancestor_odb_walk(&odb, tip, ancestor)?);
4545 Ok(())
4546 }
4547
4548 #[test]
4549 fn odb_walk_matches_exhaustive_reachability_on_random_skewed_histories() -> Result<()> {
4550 let mut state = 0x2545_f491_4f6c_dd1d_u64;
4551 let mut next = move |bound: usize| {
4552 state ^= state << 13;
4553 state ^= state >> 7;
4554 state ^= state << 17;
4555 usize::try_from(state % bound as u64).unwrap_or_default()
4556 };
4557 for round in 0..4 {
4558 let dir = tempdir().map_err(Error::Io)?;
4559 let repo = open_test_bare(&dir)?;
4560 let odb = repo.odb();
4561 let quarantine =
4562 crate::receive_quarantine::ReceiveQuarantine::create(&odb, repo.git_dir())?;
4563 let q_odb = quarantine.odb();
4564 let graphed = 40;
4565 let mut commits: Vec<ObjectId> = Vec::new();
4566 let mut parents_of: Vec<Vec<usize>> = Vec::new();
4567 for i in 0..70 {
4568 let mut parents = Vec::new();
4569 if i > 0 && next(10) != 0 {
4570 parents.push(next(i));
4571 if next(3) == 0 {
4572 let other = next(i);
4573 if !parents.contains(&other) {
4574 parents.push(other);
4575 }
4576 }
4577 }
4578 let parent_oids: Vec<ObjectId> = parents.iter().map(|&p| commits[p]).collect();
4579 let time = 1_000_000 + i64::try_from(next(5_000)).unwrap_or_default();
4580 let target = if i < graphed { &odb } else { &q_odb };
4581 commits.push(write_commit_same_time(
4582 target,
4583 &parent_oids,
4584 &format!("r{round}c{i}"),
4585 time,
4586 )?);
4587 parents_of.push(parents);
4588 if i == graphed - 1 {
4589 for (n, oid) in commits.iter().enumerate() {
4590 fs::write(
4591 repo.git_dir().join(format!("refs/heads/b{n}")),
4592 format!("{oid}\n"),
4593 )
4594 .map_err(Error::Io)?;
4595 }
4596 odb.write_commit_graph()?;
4597 }
4598 }
4599 let graph =
4600 crate::commit_graph_file::CommitGraphChain::load(&repo.git_dir().join("objects"))
4601 .expect("commit-graph");
4602
4603 let reaches = |from: usize, to: usize| {
4604 let mut stack = vec![from];
4605 let mut seen = vec![false; parents_of.len()];
4606 while let Some(n) = stack.pop() {
4607 if n == to {
4608 return true;
4609 }
4610 if !std::mem::replace(&mut seen[n], true) {
4611 stack.extend(&parents_of[n]);
4612 }
4613 }
4614 false
4615 };
4616 for a in 0..commits.len() {
4617 for d in 0..commits.len() {
4618 let expected = reaches(d, a);
4619 assert_eq!(
4620 is_ancestor_odb_walk(&q_odb, commits[a], commits[d])?,
4621 expected,
4622 "round {round}: is {a} an ancestor of {d}"
4623 );
4624 assert_eq!(
4625 is_ancestor_odb_walk_with_graph(
4626 &q_odb,
4627 Some(&graph),
4628 commits[a],
4629 commits[d]
4630 )?,
4631 expected,
4632 "round {round} with commit-graph: is {a} an ancestor of {d}"
4633 );
4634 }
4635 }
4636 }
4637 Ok(())
4638 }
4639
4640 #[test]
4641 fn odb_walk_with_graph_bounds_by_generation_not_by_date() -> Result<()> {
4642 let dir = tempdir().map_err(Error::Io)?;
4643 let repo = open_test_bare(&dir)?;
4644 let odb = repo.odb();
4645 let root = write_commit_same_time(&odb, &[], "root", YEAR_2000 - 10)?;
4646 let ancestor = write_commit_same_time(&odb, &[root], "future", YEAR_2035)?;
4647 let mut graphed_tip = ancestor;
4648 for i in 0..150 {
4649 graphed_tip =
4650 write_commit_same_time(&odb, &[graphed_tip], &format!("g{i}"), YEAR_2000 + i)?;
4651 }
4652 let unrelated = write_commit_same_time(&odb, &[], "unrelated", YEAR_2035 + 1)?;
4653 let side = write_commit_same_time(&odb, &[root], "side", YEAR_2035 + 5)?;
4654 fs::write(
4655 repo.git_dir().join("refs/heads/main"),
4656 format!("{graphed_tip}\n"),
4657 )
4658 .map_err(Error::Io)?;
4659 fs::write(
4660 repo.git_dir().join("refs/heads/u"),
4661 format!("{unrelated}\n"),
4662 )
4663 .map_err(Error::Io)?;
4664 fs::write(repo.git_dir().join("refs/heads/side"), format!("{side}\n"))
4665 .map_err(Error::Io)?;
4666 odb.write_commit_graph()?;
4667 let graph =
4668 crate::commit_graph_file::CommitGraphChain::load(&repo.git_dir().join("objects"))
4669 .expect("commit-graph");
4670 assert_ne!(graph.commit_generation_for_walk(&ancestor), UNRANKED_COMMIT);
4671 assert_ne!(
4672 graph.commit_generation_for_walk(&graphed_tip),
4673 UNRANKED_COMMIT
4674 );
4675
4676 let quarantine =
4678 crate::receive_quarantine::ReceiveQuarantine::create(&odb, repo.git_dir())?;
4679 let q_odb = quarantine.odb();
4680 let mut pushed_tip = graphed_tip;
4681 for i in 0..120 {
4682 pushed_tip =
4683 write_commit_same_time(&q_odb, &[pushed_tip], &format!("p{i}"), YEAR_2000 + i)?;
4684 }
4685
4686 let walk = |a, d| is_ancestor_odb_walk_with_graph(&q_odb, Some(&graph), a, d);
4687 assert!(walk(ancestor, graphed_tip)?);
4688 assert!(walk(ancestor, pushed_tip)?);
4689 assert!(walk(root, pushed_tip)?);
4690 assert!(walk(graphed_tip, pushed_tip)?);
4691 assert!(!walk(unrelated, pushed_tip)?);
4692 assert!(!walk(side, pushed_tip)?);
4693 assert!(!walk(pushed_tip, ancestor)?);
4694 for (a, d) in [
4695 (ancestor, pushed_tip),
4696 (unrelated, pushed_tip),
4697 (side, graphed_tip),
4698 ] {
4699 assert_eq!(walk(a, d)?, is_ancestor_odb_walk(&q_odb, a, d)?);
4700 }
4701 Ok(())
4702 }
4703
4704 #[test]
4705 fn left_behind_walk_ignores_hidden_ref_forked_near_root() -> Result<()> {
4706 use super::intersect_walk_test_hooks::{
4707 commit_parent_lookups, reset_commit_parent_lookups,
4708 };
4709
4710 let dir = tempdir().map_err(Error::Io)?;
4711 let repo = open_test_bare(&dir)?;
4712 let trunk = write_chain(&repo, 2000, |i| 1_000 + i as i64)?;
4713 let old_side = write_commit_same_time(&repo.odb(), &[trunk[1]], "old side", 1_002)?;
4714 let stray = write_commit_same_time(&repo.odb(), &[trunk[1999]], "stray", 5_000)?;
4715 let hide = [trunk[1999], old_side];
4716 let repo = with_commit_graph_over_refs(
4717 &dir,
4718 repo,
4719 &[
4720 ("refs/heads/main", trunk[1999]),
4721 ("refs/tags/old", old_side),
4722 ],
4723 )?;
4724
4725 clear_commit_linkage(&repo);
4726 reset_commit_parent_lookups();
4727 let exhaustive = walk_commits_reachable_excluding_ancestors_of(
4728 &repo,
4729 &[stray],
4730 &hide,
4731 ReachableWalkLimit::CountAllRetainNewest(20),
4732 )?;
4733 let exhaustive_lookups = commit_parent_lookups();
4734 clear_commit_linkage(&repo);
4735 reset_commit_parent_lookups();
4736 let bounded = count_commits_left_behind(&repo, &[stray], &hide, 20)?;
4737 let lookups = commit_parent_lookups();
4738
4739 assert_eq!(bounded, (1, vec![stray]));
4740 assert_eq!(bounded, exhaustive);
4741 assert!(
4742 exhaustive_lookups >= 1900,
4743 "the converging paint walks the trunk down to the old fork ({exhaustive_lookups})"
4744 );
4745 assert!(
4746 lookups <= 50,
4747 "an old hidden side branch must not walk the trunk to its fork ({lookups})"
4748 );
4749 Ok(())
4750 }
4751
4752 fn with_commit_graph_over_refs(
4755 dir: &tempfile::TempDir,
4756 repo: Repository,
4757 refs: &[(&str, ObjectId)],
4758 ) -> Result<Repository> {
4759 for (name, oid) in refs {
4760 crate::refs::write_ref(repo.git_dir(), name, oid)?;
4761 }
4762 repo.write_commit_graph(&crate::maintenance::CommitGraphOptions::default())?;
4763 drop(repo);
4764 Repository::open(dir.path(), &RepositoryOptions::empty())
4765 }
4766
4767 struct SkewedHistory {
4770 old_tip: ObjectId,
4771 skewed: ObjectId,
4772 hide: Vec<ObjectId>,
4773 refs: Vec<(&'static str, ObjectId)>,
4774 }
4775
4776 fn write_skewed_history(repo: &Repository) -> Result<SkewedHistory> {
4777 let odb = repo.odb();
4778 let base = write_commit_same_time(&odb, &[], "base", 100)?;
4779 let skewed = write_commit_same_time(&odb, &[base], "dated ahead", 1_000)?;
4780 let old_tip = write_commit_same_time(&odb, &[skewed], "old tip", 1_100)?;
4781 let behind = write_commit_same_time(&odb, &[skewed], "dated behind", 150)?;
4782 let held = write_commit_same_time(&odb, &[behind], "held", 200)?;
4783 let mut filler = base;
4784 for i in 0..10 {
4785 filler = write_commit_same_time(&odb, &[filler], &format!("filler {i}"), 500 + i)?;
4786 }
4787 Ok(SkewedHistory {
4788 old_tip,
4789 skewed,
4790 hide: vec![base, held, filler],
4791 refs: vec![
4792 ("refs/heads/main", base),
4793 ("refs/heads/held", held),
4794 ("refs/heads/filler", filler),
4795 ],
4796 })
4797 }
4798
4799 #[test]
4800 fn left_behind_walk_is_exact_under_clock_skew_without_commit_graph() -> Result<()> {
4801 let dir = tempdir().map_err(Error::Io)?;
4802 let repo = open_test_bare(&dir)?;
4803 let history = write_skewed_history(&repo)?;
4804
4805 let bounded = count_commits_left_behind(&repo, &[history.old_tip], &history.hide, 20)?;
4806
4807 assert_eq!(
4808 bounded,
4809 (1, vec![history.old_tip]),
4810 "{} is reachable from refs/heads/held",
4811 history.skewed
4812 );
4813 Ok(())
4814 }
4815
4816 #[test]
4817 fn left_behind_walk_is_exact_under_clock_skew_with_commit_graph() -> Result<()> {
4818 let dir = tempdir().map_err(Error::Io)?;
4819 let repo = open_test_bare(&dir)?;
4820 let history = write_skewed_history(&repo)?;
4821 let repo = with_commit_graph_over_refs(&dir, repo, &history.refs)?;
4822
4823 let bounded = count_commits_left_behind(&repo, &[history.old_tip], &history.hide, 20)?;
4824
4825 assert_eq!(bounded, (1, vec![history.old_tip]));
4826 Ok(())
4827 }
4828
4829 #[test]
4830 fn left_behind_walk_hides_commits_a_descendant_ref_reaches() -> Result<()> {
4831 let dir = tempdir().map_err(Error::Io)?;
4832 let repo = open_test_bare(&dir)?;
4833 let odb = repo.odb();
4834 let base = write_commit_same_time(&odb, &[], "base", 100)?;
4835 let shared = write_commit_same_time(&odb, &[base], "shared", 200)?;
4836 let kept = write_commit_same_time(&odb, &[shared], "kept by tag", 300)?;
4837 let lost_a = write_commit_same_time(&odb, &[shared], "lost a", 400)?;
4838 let lost_b = write_commit_same_time(&odb, &[lost_a], "lost b", 500)?;
4839 let target = write_commit_same_time(&odb, &[base], "target", 600)?;
4840 let hide = [target, kept];
4841
4842 let bounded = count_commits_left_behind(&repo, &[lost_b], &hide, 20)?;
4843 let exhaustive = walk_commits_reachable_excluding_ancestors_of(
4844 &repo,
4845 &[lost_b],
4846 &hide,
4847 ReachableWalkLimit::CountAllRetainNewest(20),
4848 )?;
4849
4850 assert_eq!(bounded, (2, vec![lost_b, lost_a]));
4851 assert_eq!(bounded, exhaustive);
4852 let capped = count_commits_left_behind(&repo, &[lost_b], &hide, 1)?;
4853 assert_eq!(capped, (2, vec![lost_b]));
4854 Ok(())
4855 }
4856
4857 #[test]
4858 fn left_behind_walk_without_commit_graph_stops_near_the_tips() -> Result<()> {
4859 use super::intersect_walk_test_hooks::{
4860 commit_parent_lookups, reset_commit_parent_lookups,
4861 };
4862
4863 const HOUR: i64 = 3_600;
4864 let dir = tempdir().map_err(Error::Io)?;
4865 let repo = open_test_bare(&dir)?;
4866 let trunk = write_chain(&repo, 2000, |i| 1_000_000 + HOUR * i as i64)?;
4867 let old_tag = trunk[3];
4868 let wip =
4869 write_commit_same_time(&repo.odb(), &[trunk[1999]], "wip", 1_000_000 + HOUR * 2000)?;
4870 let hide = [trunk[1999], old_tag];
4871
4872 clear_commit_linkage(&repo);
4873 reset_commit_parent_lookups();
4874 let exhaustive = walk_commits_reachable_excluding_ancestors_of(
4875 &repo,
4876 &[wip],
4877 &hide,
4878 ReachableWalkLimit::CountAllRetainNewest(20),
4879 )?;
4880 let exhaustive_lookups = commit_parent_lookups();
4881 clear_commit_linkage(&repo);
4882 reset_commit_parent_lookups();
4883 let bounded = count_commits_left_behind(&repo, &[wip], &hide, 20)?;
4884 let lookups = commit_parent_lookups();
4885
4886 assert_eq!(bounded, (1, vec![wip]));
4887 assert_eq!(bounded, exhaustive);
4888 assert!(
4889 exhaustive_lookups >= 1900,
4890 "the converging paint walks down to the old tag ({exhaustive_lookups})"
4891 );
4892 assert!(
4894 lookups <= 24 + PAINT_SLOP + 10,
4895 "without a commit-graph an old tag must not walk history ({lookups})"
4896 );
4897 Ok(())
4898 }
4899
4900 #[test]
4901 fn left_behind_walk_without_commit_graph_repaints_within_the_slop() -> Result<()> {
4902 const DAY: i64 = 86_400;
4903 let dir = tempdir().map_err(Error::Io)?;
4904 let repo = open_test_bare(&dir)?;
4905 let odb = repo.odb();
4906 let shared = write_commit_same_time(&odb, &[], "shared", 100 * DAY)?;
4909 let old_tip = write_commit_same_time(&odb, &[shared], "old tip", 101 * DAY)?;
4910 let mut held = shared;
4911 for i in 0..3 {
4912 held = write_commit_same_time(&odb, &[held], &format!("held {i}"), 10 * DAY + i)?;
4913 }
4914
4915 let bounded = count_commits_left_behind(&repo, &[old_tip], &[held], 20)?;
4916
4917 assert_eq!(bounded, (1, vec![old_tip]));
4918 Ok(())
4919 }
4920
4921 #[test]
4922 fn left_behind_walk_with_nothing_hidden_counts_all_history() -> Result<()> {
4923 let dir = tempdir().map_err(Error::Io)?;
4924 let repo = open_test_bare(&dir)?;
4925 let chain = write_chain(&repo, 30, |i| 100 + i as i64)?;
4926
4927 let (total, newest) = count_commits_left_behind(&repo, &[chain[29]], &[], 3)?;
4928
4929 assert_eq!(total, 30);
4930 assert_eq!(newest, vec![chain[29], chain[28], chain[27]]);
4931 Ok(())
4932 }
4933
4934 fn packed_over_refs(
4937 dir: &tempfile::TempDir,
4938 repo: Repository,
4939 refs: &[(String, ObjectId)],
4940 ) -> Result<Repository> {
4941 for (name, oid) in refs {
4942 crate::refs::write_ref(repo.git_dir(), name, oid)?;
4943 }
4944 let opts = crate::maintenance::RepackOptions {
4945 pack_build: Some(crate::pack_objects::PackBuildOptions {
4946 delta: false,
4947 window: 0,
4948 max_depth: 0,
4949 ..crate::pack_objects::PackBuildOptions::for_local_copy(None)
4950 }),
4951 ..crate::maintenance::RepackOptions::default()
4952 };
4953 crate::maintenance::repack(&repo, &opts)?;
4954 drop(repo);
4955 Repository::open(dir.path(), &RepositoryOptions::empty())
4956 }
4957
4958 struct ManyTags {
4960 trunk: Vec<ObjectId>,
4961 tags: Vec<ObjectId>,
4962 refs: Vec<(String, ObjectId)>,
4963 }
4964
4965 fn write_many_tags(repo: &Repository, count: usize) -> Result<ManyTags> {
4966 const HOUR: i64 = 3_600;
4967 let trunk = write_chain(repo, count + 10, |i| 1_000_000 + HOUR * i as i64)?;
4968 let mut tags = Vec::with_capacity(count);
4969 let mut refs = vec![("refs/heads/main".to_owned(), trunk[count + 9])];
4970 for i in 0..count {
4971 let tag = write_commit_same_time(
4972 &repo.odb(),
4973 &[trunk[i]],
4974 &format!("release {i}\n\nA longer message body for release {i}."),
4975 1_000_000 + HOUR * i as i64 + 60,
4976 )?;
4977 refs.push((format!("refs/tags/v{i}"), tag));
4978 tags.push(tag);
4979 }
4980 Ok(ManyTags { trunk, tags, refs })
4981 }
4982
4983 #[test]
4984 fn prime_tips_records_the_linkage_a_full_read_records() -> Result<()> {
4985 let dir = tempdir().map_err(Error::Io)?;
4986 let repo = open_test_bare(&dir)?;
4987 let history = write_many_tags(&repo, PRIME_TIPS_MIN + 16)?;
4988 let repo = packed_over_refs(&dir, repo, &history.refs)?;
4989
4990 let mut expected = Vec::new();
4991 clear_commit_linkage(&repo);
4992 let mut full = CommitGraphCache::new(&repo);
4993 for &tag in &history.tags {
4994 expected.push((full.parents_of(tag)?, full.commit_time(tag)?));
4995 }
4996
4997 clear_commit_linkage(&repo);
4998 let mut primed = CommitGraphCache::new(&repo);
4999 primed.prime_tips(&history.tags);
5000 assert_eq!(primed.header_primed.len(), history.tags.len());
5001 for (&tag, want) in history.tags.iter().zip(&expected) {
5002 assert_eq!(primed.peel_tip(tag)?, Some(tag));
5003 let recorded = primed.with_linkage(|link| {
5004 (
5005 link.parents.get(&tag).cloned(),
5006 link.times.get(&tag).copied(),
5007 )
5008 });
5009 assert_eq!(recorded, (Some(want.0.clone()), Some(want.1)), "{tag}");
5010 }
5011
5012 clear_commit_linkage(&repo);
5013 let mut few = CommitGraphCache::new(&repo);
5014 few.prime_tips(&history.tags[..PRIME_TIPS_MIN - 1]);
5015 assert!(
5016 few.header_primed.is_empty(),
5017 "a paint with few tips keeps the plain reads"
5018 );
5019 Ok(())
5020 }
5021
5022 #[test]
5023 fn prime_tips_leaves_loose_commits_and_tags_to_the_full_peel() -> Result<()> {
5024 use crate::objects::{serialize_tag, TagData};
5025
5026 let dir = tempdir().map_err(Error::Io)?;
5027 let repo = open_test_bare(&dir)?;
5028 let history = write_many_tags(&repo, PRIME_TIPS_MIN)?;
5029 let repo = packed_over_refs(&dir, repo, &history.refs)?;
5030 let loose = write_commit_same_time(&repo.odb(), &[history.tags[0]], "loose", 9_999_999)?;
5031 let tag_bytes = serialize_tag(&TagData {
5032 object: history.tags[1],
5033 object_type: "commit".into(),
5034 tag: "annotated".into(),
5035 tagger: Some("T <t@e.com> 1 +0000".into()),
5036 message: "annotated\n".into(),
5037 extra_headers: Vec::new(),
5038 });
5039 let annotated = repo.odb().write(ObjectKind::Tag, &tag_bytes)?;
5040 let mut tips = history.tags.clone();
5041 tips.extend([loose, annotated]);
5042
5043 clear_commit_linkage(&repo);
5044 let mut cache = CommitGraphCache::new(&repo);
5045 cache.prime_tips(&tips);
5046 assert_eq!(cache.header_primed.len(), history.tags.len());
5047 assert!(!cache.header_primed.contains(&loose));
5048 assert!(!cache.header_primed.contains(&annotated));
5049 assert_eq!(cache.peel_tip(loose)?, Some(loose));
5050 assert_eq!(cache.peel_tip(annotated)?, Some(history.tags[1]));
5051 Ok(())
5052 }
5053
5054 #[test]
5055 fn left_behind_walk_over_many_packed_tags_matches_the_exhaustive_walk() -> Result<()> {
5056 let dir = tempdir().map_err(Error::Io)?;
5057 let repo = open_test_bare(&dir)?;
5058 let history = write_many_tags(&repo, 2 * PRIME_TIPS_MIN)?;
5059 let tip = *history.trunk.last().expect("trunk");
5060 let repo = packed_over_refs(&dir, repo, &history.refs)?;
5061 let wip = write_commit_same_time(&repo.odb(), &[tip], "wip", 9_000_000)?;
5062 let mut hide: Vec<ObjectId> = history.refs.iter().map(|(_, oid)| *oid).collect();
5063
5064 for expected in [(1, vec![wip]), (0, Vec::new())] {
5065 clear_commit_linkage(&repo);
5066 let exhaustive = walk_commits_reachable_excluding_ancestors_of(
5067 &repo,
5068 &[wip],
5069 &hide,
5070 ReachableWalkLimit::CountAllRetainNewest(20),
5071 )?;
5072 clear_commit_linkage(&repo);
5073 let bounded = count_commits_left_behind(&repo, &[wip], &hide, 20)?;
5074 assert_eq!(bounded, expected);
5075 assert_eq!(bounded, exhaustive);
5076 let child = write_commit_same_time(&repo.odb(), &[wip], "child", 9_000_100)?;
5078 hide.push(child);
5079 }
5080 Ok(())
5081 }
5082
5083 #[test]
5084 fn rewound_ref_is_not_an_ancestor_without_the_slop_walk() -> Result<()> {
5085 use super::intersect_walk_test_hooks::{
5086 commit_parent_lookups, reset_commit_parent_lookups,
5087 };
5088
5089 const HOUR: i64 = 3_600;
5090 let dir = tempdir().map_err(Error::Io)?;
5091 let repo = open_test_bare(&dir)?;
5092 let trunk = write_chain(&repo, 400, |i| 1_000_000 + HOUR * i as i64)?;
5093 let wip = write_commit_same_time(&repo.odb(), &[trunk[399]], "wip", 9_000_000)?;
5094 let side = write_commit_same_time(&repo.odb(), &[trunk[200]], "side", 9_000_000)?;
5095
5096 for (ancestor, descendant) in [
5097 (wip, trunk[399]),
5098 (wip, trunk[396]),
5099 (trunk[396], wip),
5100 (side, trunk[399]),
5101 (trunk[100], side),
5102 (trunk[399], trunk[399]),
5103 ] {
5104 clear_commit_linkage(&repo);
5105 let expected = is_ancestor(&repo, ancestor, descendant)?;
5106 clear_commit_linkage(&repo);
5107 assert_eq!(
5108 is_ancestor_unless_rewound(&repo, ancestor, descendant)?,
5109 expected,
5110 "{ancestor} -> {descendant}"
5111 );
5112 }
5113
5114 clear_commit_linkage(&repo);
5115 reset_commit_parent_lookups();
5116 assert!(is_ancestor(&repo, wip, trunk[396]).is_ok_and(|a| !a));
5117 let slop_lookups = commit_parent_lookups();
5118 clear_commit_linkage(&repo);
5119 reset_commit_parent_lookups();
5120 assert!(!is_ancestor_unless_rewound(&repo, wip, trunk[396])?);
5121 let lookups = commit_parent_lookups();
5122 assert!(slop_lookups >= 100, "the date-slop walk ({slop_lookups})");
5123 assert!(lookups <= 5, "HEAD~3 is found on the chain ({lookups})");
5125 Ok(())
5126 }
5127
5128 #[test]
5129 fn rewound_check_defers_to_the_full_walk_when_grafts_rewrite_parents() -> Result<()> {
5130 let dir = tempdir().map_err(Error::Io)?;
5131 let repo = open_test_bare(&dir)?;
5132 let base = write_commit_same_time(&repo.odb(), &[], "base", 100)?;
5133 let tip = write_commit_same_time(&repo.odb(), &[base], "tip", 200)?;
5134 fs::create_dir_all(repo.git_dir().join("info"))?;
5136 fs::write(
5137 repo.git_dir().join("info/grafts"),
5138 format!("{base} {tip}\n"),
5139 )?;
5140 let repo = Repository::open(dir.path(), &RepositoryOptions::empty())?;
5141
5142 clear_commit_linkage(&repo);
5143 let expected = is_ancestor(&repo, tip, base)?;
5144 clear_commit_linkage(&repo);
5145 assert_eq!(is_ancestor_unless_rewound(&repo, tip, base)?, expected);
5146 assert!(expected, "the graft makes {tip} an ancestor of {base}");
5147 Ok(())
5148 }
5149}