Skip to main content

grit_lib/
merge_base.rs

1//! Merge-base and reachability primitives.
2//!
3//! This module implements the subset needed by `grit merge-base`:
4//! default merge-base selection, `--all`, `--octopus`, `--independent`,
5//! and `--is-ancestor`.
6
7use 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
27/// Resolve commit-ish command arguments to commit object IDs.
28///
29/// # Parameters
30///
31/// - `repo` - repository used for revision lookup and object reads.
32/// - `specs` - revision arguments such as `HEAD`, ref names, or object IDs.
33///
34/// # Errors
35///
36/// Returns [`Error::ObjectNotFound`] when a revision does not resolve and
37/// [`Error::CorruptObject`] when the resolved object is not a commit.
38pub 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
48/// Compute merge bases for one commit vs one or more others.
49///
50/// Semantics match Git's default mode: for `<a> <b>...`, this computes merge
51/// bases between `a` and a hypothetical merge of all remaining commits.
52///
53/// # Parameters
54///
55/// - `repo` - repository used to walk commit parents.
56/// - `first` - first commit argument.
57/// - `others` - remaining commit arguments.
58///
59/// # Errors
60///
61/// Returns parse and object read errors from commit traversal.
62pub 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
118/// Merge base of `HEAD` and one other commit, matching `git diff --merge-base <commit>`.
119///
120/// Returns an error when there is no merge base or more than one.
121pub 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
135/// Merge base of two commits, matching `git diff --merge-base <a> <b>` / `diff-tree --merge-base`.
136///
137/// Returns an error when there is no merge base or more than one.
138pub 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/// Failure modes for [`merge_base_for_diff_index`] and [`merge_base_for_diff_two_commits`].
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub enum MergeBaseForDiffError {
155    /// No common ancestor between the commits.
156    None,
157    /// More than one minimal merge base (criss-cross history).
158    Multiple,
159    /// Resolution or object read error; message is suitable for stderr.
160    Other(String),
161}
162
163/// Compute merge bases common to all supplied commits (`--octopus` mode).
164///
165/// # Parameters
166///
167/// - `repo` - repository used to walk commit parents.
168/// - `commits` - commits to intersect.
169///
170/// # Errors
171///
172/// Returns parse and object read errors from commit traversal.
173pub 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
189/// All merge bases common to every supplied commit (intersection of ancestor sets,
190/// reduced to minimal bases). Matches `git merge-base` with two or more tips.
191///
192/// This is the same intersection-and-reduction as [`merge_bases_octopus`]; the name
193/// documents the `git merge-base A B C ...` calling convention.
194///
195/// For two commits the bases come newest committer date first, and bases with equal
196/// dates in the order Git reports them (the order its history walk reaches them).
197pub 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        // A tip that is an ancestor of the other is the only merge base: every other common
207        // ancestor is one of its own ancestors.
208        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
220/// Every merge base of `one` and a hypothetical merge of all `others`, newest committer first.
221///
222/// The hypothetical merge's history is the union of the histories of `others`, so this is
223/// also the merge-base set of `one` and an in-memory commit whose parents are `others`
224/// (what a recursive merge needs after merging several merge bases into a virtual one).
225/// Unlike [`merge_bases_first_vs_rest`] the result is not truncated to one base.
226pub(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    // The walk yields bases newest first and, among equal dates, in discovery order (Git's
240    // order); dropping redundant ones keeps that order, which a set would lose.
241    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
250/// Check whether `ancestor` is reachable from `descendant`.
251///
252/// # Errors
253///
254/// Returns parse and object read errors from commit traversal.
255pub 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
263/// [`is_ancestor`] that first looks for `descendant` on `ancestor`'s first-parent chain, the
264/// shape of a ref moved back with `HEAD~n`.
265///
266/// Finding it proves `descendant` is a strict ancestor of `ancestor`, so the answer is `false`
267/// (history has no cycles) without [`is_ancestor`]'s date-slop walk, which would otherwise read
268/// a hundred commits below `ancestor`'s date. The chain walk stops at the first commit dated
269/// before `descendant`, so a ref that moved anywhere else costs only the few commits newer
270/// than `descendant`; [`is_ancestor`] then decides. Grafts and replace refs can make ancestry
271/// cyclic, so with either present the chain is not consulted.
272///
273/// # Errors
274///
275/// Propagates commit read and parse errors.
276pub(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
293/// Whether `target` lies on `from`'s first-parent chain, walking only commits dated at or
294/// after `target`.
295fn 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
314/// Whether the acknowledged haves of a fetch negotiation cover every wanted
315/// commit, so upload-pack may answer `ready` and send the pack.
316///
317/// Each have commit and its immediate parents count as held by the client.
318/// A want is covered when walking its parents reaches a held commit. Commits
319/// older (by committer date) than the oldest have are not walked through,
320/// which keeps the check to the few commits between the wants and the haves
321/// instead of a history scan. Parents and dates come from the commit-graph
322/// when the repository has one.
323///
324/// # Parameters
325///
326/// - `repo` - repository being served.
327/// - `wants` - wanted tips, already peeled through tags; only commits.
328/// - `have_commits` - acknowledged haves that are commits (tags are not peeled).
329///
330/// # Returns
331///
332/// `false` when `have_commits` is empty or some want cannot reach a held
333/// commit within the date cutoff; `true` otherwise.
334///
335/// # Errors
336///
337/// Returns object read and parse errors for commits on the walk.
338pub(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    // Commits that reach a held commit; grows with each covered want's path.
348    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    // Commits whose walk finished without reaching a held commit.
356    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
390/// Reachability test using only the given object database (no commit-graph,
391/// grafts or shallow boundaries).
392///
393/// Works on any [`Odb`](crate::odb::Odb), such as a receive quarantine whose
394/// alternates include the repository's own objects, so it can check commits
395/// that are not yet visible through [`Repository::odb`].
396///
397/// History is walked from both commits at once, newest committer date first.
398/// The walk ends once every line of history from `descendant` has either
399/// reached `ancestor` or joined `ancestor`'s own history, so a rejected
400/// fast-forward costs about the distance to the fork point. Committer dates
401/// only order the walk; the answer stays exact when they are skewed.
402///
403/// # Parameters
404///
405/// - `odb` - object database holding both commits and the history between them.
406/// - `ancestor` - candidate ancestor commit.
407/// - `descendant` - commit whose history is searched.
408///
409/// # Returns
410///
411/// `true` when `ancestor` equals or is reachable from `descendant`; `false`
412/// otherwise, including when either object is not a commit. Parents missing
413/// from `odb` end their line of history.
414///
415/// # Errors
416///
417/// Returns read errors for `ancestor` or `descendant`, and parse errors for
418/// commits on the walk.
419pub 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
427/// [`is_ancestor_odb_walk`] that also uses `graph`, the commit-graph of the
428/// repository whose objects `odb` contains (directly or as an alternate).
429///
430/// Commits found in `graph` take their parents from it without an object
431/// read. When `ancestor` has a corrected commit date (generation v2), any
432/// graph commit with a generation at or below it cannot reach `ancestor`, so
433/// its history is skipped. Generations strictly increase from parent to
434/// child, which makes this cutoff exact; commits outside the graph (such as
435/// freshly pushed ones) are always expanded.
436pub(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    // Paint everything reachable from `ancestor` with FROM_ANCESTOR and from
463    // `descendant` with FROM_DESCENDANT. `ancestor` is reachable exactly when it
464    // gets FROM_DESCENDANT. Only a commit painted FROM_DESCENDANT alone can
465    // still pass that paint on to `ancestor`: a commit with both paints is an
466    // ancestor of `ancestor`, so nothing it reaches is `ancestor` itself. The
467    // walk therefore ends once no queued commit is painted FROM_DESCENDANT alone,
468    // whatever order committer dates put the commits in.
469    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    // Expand by generation, then committer date. Commits outside the graph (such as the
476    // ones being pushed) rank above every graph commit, so a descendant dated before the
477    // ancestor's history reaches it without first draining that history. Without a graph
478    // every commit ranks the same and this is the plain date order.
479    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
539/// Committer timestamp and parents of `oid`, or `None` when it is not a commit.
540///
541/// Reads them from `graph` when it lists `oid`, otherwise from `odb`.
542fn 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
561/// Returns the ref path under `logs/` used for fork-point reflog scanning for `merge-base --fork-point`
562/// and `rebase --fork-point`, matching Git's resolution order.
563///
564/// # Parameters
565///
566/// - `spec` - upstream argument as given on the command line (`main`, `refs/heads/main`, `HEAD`, …).
567pub 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
588/// Picks the fork-point candidate that is not strictly dominated by another candidate in the list.
589fn 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
614/// Computes the fork-point commit between `upstream_tip` and `head`, using the upstream ref's reflog.
615///
616/// This matches `git merge-base --fork-point` / the merge base `git rebase --fork-point` uses for
617/// selecting commits to replay.
618///
619/// # Parameters
620///
621/// - `upstream_spec` - upstream revision string (used to locate the reflog; e.g. `main`,
622///   `refs/heads/main`, or `topic@{{upstream}}`).
623/// - `upstream_tip` - resolved commit of the upstream branch tip.
624/// - `head` - commit to rebase (usually `HEAD`).
625///
626/// # Errors
627///
628/// Propagates object read, reflog, and graph walk errors.
629pub 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
675/// Returns every commit reachable from `tip` by walking parent links (including `tip`).
676///
677/// # Errors
678///
679/// Returns [`Error::CorruptObject`] if an encountered object is not a commit.
680pub 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
685/// Count symmetric-diff commits between two tips, matching `git rev-list --left-right A...B`.
686///
687/// Returns `(ahead, behind)` where `ahead` counts commits reachable from `local` but not from
688/// `other`, and `behind` the converse. Shared history is excluded from both counts.
689///
690/// # Errors
691///
692/// Propagates errors from commit graph walks.
693pub 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
702/// Symmetric ahead/behind between `head` and `target`, plus bounded newest OIDs on each side.
703///
704/// Matches `git rev-list --left-right --count HEAD...TARGET` for the counts. Both sides are
705/// counted in one two-sided walk that stops at the common frontier, so the cost scales with
706/// the commits since the histories diverged rather than with total history. The returned
707/// OID lists contain up to `retain_newest_ahead` / `retain_newest_behind` commits reachable
708/// from `head` / `target` but not the other tip (newest first, `git rev-list` order).
709///
710/// # Errors
711///
712/// Propagates object read and parse errors from commit traversal.
713pub 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        // Reuse the paint so the ordered walk only filters, never re-walks history.
730        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/// Commits reachable from `tips` that are not ancestors of any `hide` tip, in committer-date order
755/// (newest first). Matches `git rev-list tips ^hide…` default ordering for the common case without
756/// materializing the full ancestor closure of the hide tips.
757///
758/// When `limit` is [`Some`], traversal stops once that many commits have been collected (after
759/// applying the hide filter). Use this for bounded ahead-of-target lists and status shortlogs.
760///
761/// Shallow boundaries (`.git/shallow`) stop parent traversal without following
762/// absent parent objects. Other missing commits propagate [`Error::ObjectNotFound`].
763///
764/// # Errors
765///
766/// Propagates object read and parse errors from commit traversal.
767/// Committer-date filters applied during bounded reachable walks (Git `--since` / `--until`).
768#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
769pub struct ReachableWalkCommitterCutoffs {
770    /// Exclude commits with committer date strictly before this Unix timestamp (`--since`).
771    pub since: Option<i64>,
772    /// Exclude commits with committer date strictly after this Unix timestamp (`--until`).
773    pub until: Option<i64>,
774}
775
776/// How [`walk_commits_reachable_excluding_ancestors_of`] bounds its output.
777#[derive(Debug, Clone, Copy)]
778pub enum ReachableWalkLimit {
779    /// Emit at most `n` commits and stop (rev-list `--max-count`).
780    StopAfter(usize),
781    /// Walk the full emitted set; return the total count and only the newest `n` OIDs.
782    CountAllRetainNewest(usize),
783    /// Walk and collect every emitted commit.
784    Unlimited,
785}
786
787/// Max-heap key for Git default rev-list date ordering (newest committer time first).
788#[derive(Clone, Copy, Debug, Eq, PartialEq)]
789struct WalkDateKey {
790    oid: ObjectId,
791    time: i64,
792    /// Insertion-order tiebreaker for equal dates (earlier insertion pops first).
793    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/// Resumable date-order walk state for [`walk_commits_reachable_excluding_ancestors_of`].
815///
816/// Ordering matches Git's default `rev-list`: pop the greatest committer timestamp (ties go
817/// to the earlier queued commit) and queue each parent when it is first discovered. Nothing
818/// waits for a commit's other children, so under clock skew a parent can print before one of
819/// its children, exactly as in Git.
820#[derive(Clone, Debug)]
821pub struct ReachableWalkState {
822    heap: BinaryHeap<WalkDateKey>,
823    done: OidHashSet,
824    queued: OidHashSet,
825    /// Tips seeded on the heap up front (may emit before all children are shown).
826    seeded: OidHashSet,
827    seq: u64,
828    hide: Vec<ObjectId>,
829    /// Which pending commits reach a hide tip; computed on first use so restored
830    /// snapshots rebuild it from their heap.
831    paint: Option<SidePaint>,
832    /// Read parents and dates from the on-disk commit-graph when one is present.
833    use_commit_graph: bool,
834    /// When the hide paint may stop (see [`HidePaintStop`]).
835    hide_paint_stop: HidePaintStop,
836}
837
838/// When [`SidePaint::compute_hiding`] may stop painting.
839#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
840enum HidePaintStop {
841    /// Paint until every queued commit is reachable from both sides. Exact under clock skew,
842    /// but a hide tip that forked near the root keeps the walk going down to that fork.
843    #[default]
844    Converged,
845    /// Stop once no queued commit is reachable from the tips alone, except that commits
846    /// without a commit-graph generation keep the [`Self::Converged`] rule. Generations grow
847    /// strictly from parent to child and the graph holds every ancestor of its commits, so a
848    /// queued commit with a generation can never reach a commit already popped: the stop is
849    /// exact. Committer dates promise nothing under clock skew, so without a commit-graph this
850    /// is the converging walk.
851    TipsResolved,
852    /// [`Self::TipsResolved`] for commits without a generation too: a queued commit keeps the
853    /// walk going only while the tips alone reach it, and once none does the walk ends by
854    /// committer date (see [`PaintWalk::unranked_hide_may_reach_left`]). Like Git's slop-based
855    /// `rev-list`, the result can be wrong under clock skew larger than
856    /// [`UNRANKED_SKEW_ALLOWANCE`]. Only the orphaned-commit report, which is advisory, uses it,
857    /// and only in repositories without a commit-graph.
858    TipsResolvedDateSlop,
859}
860
861/// Serializable snapshot of [`ReachableWalkState`] for resuming a bounded walk.
862#[derive(Clone, Debug, PartialEq, Eq)]
863pub struct ReachableWalkSnapshot {
864    /// Pending heap entries (order does not matter).
865    pub heap: Vec<(i64, u64, ObjectId)>,
866    /// Commits already popped from the heap (emitted or skipped as duplicate/hidden).
867    pub done: Vec<ObjectId>,
868    /// Every OID ever queued on the heap.
869    pub queued: Vec<ObjectId>,
870    /// Tip OIDs seeded on the heap at walk start.
871    pub seeded: Vec<ObjectId>,
872    /// `seq` field.
873    pub seq: u64,
874    /// `hide` field.
875    pub hide: Vec<ObjectId>,
876}
877
878impl ReachableWalkState {
879    /// Initialize a walk from `tips`, excluding ancestors of `hide`.
880    ///
881    /// # Errors
882    ///
883    /// Propagates commit read errors while seeding the heap.
884    pub fn new(repo: &Repository, tips: &[ObjectId], hide: &[ObjectId]) -> Result<Self> {
885        Self::with_commit_graph(repo, tips, hide, true)
886    }
887
888    /// Like [`Self::new`]; `use_commit_graph: false` parses every commit object instead of
889    /// reading the on-disk commit-graph ([`crate::rev_list::RevListOptions::use_commit_graph`]).
890    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    /// Start (or resume from `resume`) a walk and emit up to `limit` commits, also returning
929    /// the raw object bytes of every emitted commit whose object the walk had to read.
930    ///
931    /// Commits whose parents and dates came from the commit-graph (or an earlier walk's
932    /// linkage cache, such as commits read while painting the hidden side) are absent from
933    /// the map; callers read those themselves.
934    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        // Under the pack-read context the walk reads every commit through one single-pack
943        // reader instead of installing the context and opening a reader per object.
944        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        // Sized for the page up front so the id table never exists twice while it grows.
958        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    /// Restore walk state from a [`ReachableWalkSnapshot`].
978    #[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    /// Capture the current walk state for later resumption.
1005    #[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    /// Whether the heap still holds commits that might be emitted.
1022    #[must_use]
1023    pub fn has_pending(&self) -> bool {
1024        !self.heap.is_empty()
1025    }
1026
1027    /// Whether the walk queue may still produce commits that pass `cutoffs`.
1028    ///
1029    /// When `--since` excludes every queued commit (common after hitting `-n` while
1030    /// older commits remain on the heap), this returns `false` even though
1031    /// [`Self::has_pending`] is still true.
1032    #[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    /// Paint pending commits against the hide tips. Must run before the heap is
1047    /// popped: every commit still to be emitted is an ancestor of a heap entry.
1048    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        // Tag tips were painted under their peeled commit.
1077        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        // Unpainted commits sit below the frontier where every path is reachable
1084        // from a hide tip.
1085        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                // Ancestors of hidden commits are hidden too; keeping them off the
1108                // heap stops the walk at the hide frontier instead of the root.
1109                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    /// Emit up to `limit` commits (newest first), stopping early when `limit` is reached.
1161    ///
1162    /// # Errors
1163    ///
1164    /// Propagates object read and ancestry errors during traversal.
1165    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
1192/// Walk commits reachable from `tips` that are not ancestors of any `hide` tip.
1193///
1194/// Returns `(emitted_count, collected_oids)` where `collected_oids` are newest-first.
1195/// With [`ReachableWalkLimit::CountAllRetainNewest`], `emitted_count` is the full
1196/// ahead count while `collected_oids` holds at most the requested number of newest tips.
1197pub 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
1212/// Like [`walk_commits_reachable_excluding_ancestors_of`] with committer-date cutoffs on bounded walks.
1213pub 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
1248/// Count the commits reachable from `tips` that no `hide` tip reaches, keeping the newest
1249/// `retain_newest` of them: the commits a ref move leaves behind.
1250///
1251/// Same result as [`walk_commits_reachable_excluding_ancestors_of`] with
1252/// [`ReachableWalkLimit::CountAllRetainNewest`] when every commit involved has a commit-graph
1253/// generation: the hide paint stops as soon as `tips` are resolved, so a hidden ref that forked
1254/// from `tips` near the root costs nothing, which matters because callers hide every ref in the
1255/// repository. Without a commit-graph the paint ends by committer date instead
1256/// ([`HidePaintStop::TipsResolvedDateSlop`]), so the count behaves like Git's slop-based
1257/// `rev-list` and can be wrong under heavy clock skew. Grafts and shallow boundaries rewrite
1258/// parents behind the graph's back, so they keep the converging paint.
1259///
1260/// # Errors
1261///
1262/// Propagates commit read and parse errors from the walk.
1263pub(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
1291/// Count commits reachable from `head` but not from `target`, and return up to `retain_newest`
1292/// of those commit OIDs (newest first) without materializing the full ahead list.
1293///
1294/// # Errors
1295///
1296/// Propagates object read and parse errors from commit traversal.
1297pub 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/// Filters applied when counting reachable commits (parent count and committer dates).
1315#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1316pub struct ReachableCountFilter {
1317    /// Minimum parent count for a commit to be included.
1318    pub min_parents: Option<usize>,
1319    /// Maximum parent count for a commit to be included.
1320    pub max_parents: Option<usize>,
1321    /// Follow only the first parent link when walking.
1322    pub first_parent_only: bool,
1323    /// Exclude commits with committer date strictly before this Unix timestamp.
1324    pub since: Option<i64>,
1325    /// Exclude commits with committer date strictly after this Unix timestamp.
1326    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
1361/// Count commits reachable from `tips` that are not ancestors of any `hide` tip.
1362///
1363/// One two-sided paint marks what `hide` reaches and stops at the common frontier, so the
1364/// cost follows the size of the range rather than the depth of history. Parent and date
1365/// filters only decide which painted commits are counted: like Git `rev-list`, the walk still
1366/// goes through commits they reject. Cheap shortcuts (a first-parent chain down to `hide`,
1367/// stored pack bitmaps, a whole-history commit-graph count) run first when commit-graph
1368/// generations cannot bound the paint.
1369///
1370/// # Errors
1371///
1372/// Propagates object read and ancestry errors during traversal.
1373pub 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    // Generations resolve the included side without visiting older hidden ancestry.
1389    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    // Every commit the tips reach without `hide` is painted left-only, so the filtered walk
1408    // tests membership in the paint instead of asking about ancestry per commit. Taking each
1409    // entry out of the map doubles as the visited set; unpainted commits are common history.
1410    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
1440/// The first commit on each first-parent chain from `tips` that a `hide` tip reaches through
1441/// any parent: the cut points of a `--first-parent` range walk.
1442///
1443/// Git follows only first parents from the commits it lists, while excluded tips still hide
1444/// everything they reach. Every ancestor of a hidden commit is hidden too, so a first-parent
1445/// walk from `tips` that stops at the returned commits lists exactly the range. The paint ends
1446/// once the tips' first-parent chains meet hidden history when commit-graph generations bound
1447/// it; otherwise it converges like the range count does.
1448///
1449/// # Parameters
1450///
1451/// - `tips` - commits to list from, following first parents only.
1452/// - `hide` - excluded tips; their ancestry through every parent is hidden.
1453/// - `use_commit_graph` - read parents and generations from the on-disk commit-graph.
1454///
1455/// # Errors
1456///
1457/// Propagates commit read and parse errors for commits not reached from `hide`.
1458pub(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    // Grafts and replacements rewrite parents behind the graph's back, so its generations
1466    // cannot order the paint.
1467    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        // Unpainted chain commits lie below a commit both sides reach, so they are hidden.
1480        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
1491/// Shortcuts for [`count_commits_reachable_excluding_ancestors_of`] that avoid painting, or
1492/// `None` when the paint has to run.
1493fn 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    // The graph count follows first parents from `hide` too, so it only serves first-parent
1510    // walks that hide nothing.
1511    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    // Walking a merge-free chain down to `hide` costs one commit per counted commit, so it
1526    // runs before the bitmap probe, which pays for opening and checking the bitmap file.
1527    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
1538/// Newest-first commits from `tip` applying committer-date cutoffs, stopping after `limit`
1539/// matches, but only when every visited commit has at most one parent (linear history).
1540/// Returns `None` when a merge is encountered so callers can fall back to a full-graph
1541/// bounded walk.
1542pub 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
1576/// Count commits on `tip`'s first-parent chain above `hide`.
1577///
1578/// The caller must already know that `hide` lies on `tip`'s first-parent chain
1579/// ([`FirstParentRelation::SecondIsAncestor`]). Returns `None` when a merge is met and the
1580/// full graph must be walked instead.
1581fn 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
1608/// `commits_reachable_excluding_ancestors_of` function.
1609pub 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
1624/// Like [`commits_reachable_excluding_ancestors_of`] with committer-date cutoffs on bounded walks.
1625pub 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
1642/// [`commits_reachable_excluding_ancestors_of_cutoffs`] that can skip the on-disk commit-graph
1643/// and parse every commit object instead.
1644pub(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
1659/// Return commits that are not reachable from any other input commit.
1660///
1661/// The output order follows input order, dropping any commit reachable from
1662/// another supplied commit.
1663///
1664/// # Errors
1665///
1666/// Returns parse and object read errors from commit traversal.
1667pub 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
1688/// Select the best base ref for a target tip using Git's first-parent branch-base heuristic.
1689///
1690/// The returned index points into `bases`. The algorithm walks the first-parent histories of
1691/// `tip` and the candidate bases, picking the base whose first-parent path collides with the
1692/// tip's first-parent path at the newest branch point. Ties keep the earliest candidate index.
1693///
1694/// # Parameters
1695///
1696/// - `repo` - repository used to read commit parents.
1697/// - `tip` - target commit whose branch base is being queried.
1698/// - `bases` - candidate base commits in caller-visible order.
1699///
1700/// # Errors
1701///
1702/// Returns object read or parse errors for malformed commit history.
1703pub 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        /// Parent reads including cache hits: repeated walks over cached commits show here.
1815        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    /// Queue insertion counter: among equal rank and date, the earlier queued commit pops
1836    /// first, so merge bases with equal dates come out in Git's discovery order.
1837    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    /// Commits queued so far; stamps [`RankedCommit::queued_at`].
1863    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
1890/// Classify `first` and `second` along first-parent chains, using commit-graph generations
1891/// to bound the walk when both commits are ranked and [`first_parent_chain_relation`]
1892/// otherwise.
1893fn 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/// How two commits relate along their first-parent chains.
1941#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1942enum FirstParentRelation {
1943    /// Both inputs are the same commit.
1944    Same,
1945    /// The first commit lies on the second commit's first-parent chain.
1946    FirstIsAncestor,
1947    /// The second commit lies on the first commit's first-parent chain.
1948    SecondIsAncestor,
1949    /// Neither commit lies on the other's first-parent chain (they may still be
1950    /// related through second parents).
1951    Unrelated,
1952}
1953
1954/// Classify `first` and `second` by walking both first-parent chains together.
1955///
1956/// Stops as soon as one chain reaches the other tip, or the chains merge at a commit that
1957/// is neither tip: once merged they share every later commit, so the tip that is not yet
1958/// on the other chain can never appear on it. That argument holds for any interleaving of
1959/// the two walks, so committer dates only pick which chain to advance (newer first) and
1960/// never affect the answer. With sane dates the cost is about the distance to the answer
1961/// instead of a walk to the root of the losing direction. A root commit or a
1962/// missing or unreadable parent ends that chain; the caller's full walk reports real errors.
1963fn 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        // Advance the newer chain first so a descendant reaches its ancestor without also
1979        // walking the ancestor's chain; equal or unreadable dates step both.
1980        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
2018/// Commit-graph generation when present (`None` if the commit is outside the graph).
2019fn 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
2036/// Heap ordering key: commit-graph generation when present, else [`UNRANKED_COMMIT`]
2037/// with [`CommitGraphCache::commit_time`] as the tie-break (see [`PaintKey`]).
2038fn 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
2056/// Commit is reachable from the left tips of a [`SidePaint`].
2057const PAINT_LEFT: u8 = 1;
2058/// Commit is reachable from the right tips of a [`SidePaint`].
2059const PAINT_RIGHT: u8 = 2;
2060const PAINT_BOTH: u8 = PAINT_LEFT | PAINT_RIGHT;
2061
2062/// Two-sided reachability paint (Git's `ahead_behind` walk).
2063///
2064/// One walk from both sides at once, newest generation (then committer date)
2065/// first, marking each commit with the sides that reach it. The walk stops once
2066/// every queued commit is reachable from both sides: everything below that
2067/// frontier is common history. Commits never painted are therefore reachable
2068/// from both sides (or from neither).
2069///
2070/// With commit-graph generations the result is exact; for commits outside the
2071/// graph, ordering falls back to committer dates like Git does.
2072#[derive(Clone, Debug, Default)]
2073struct SidePaint {
2074    sides: OidHashMap<u8>,
2075}
2076
2077impl SidePaint {
2078    /// Paint ancestors of `left` and `right` down to their common frontier.
2079    ///
2080    /// # Errors
2081    ///
2082    /// Propagates commit read and parse errors from the walk.
2083    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    /// Paint `tips` against the `hide` tips of a range walk. Like Git's walk over excluded
2092    /// commits, a missing object reached from `hide` is skipped rather than an error: it is
2093    /// hidden whatever lies behind it.
2094    ///
2095    /// # Errors
2096    ///
2097    /// Propagates commit read and parse errors for commits not reached from `hide`.
2098    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    /// [`Self::compute_hiding`] with [`HidePaintStop::TipsResolved`] for a `--first-parent`
2117    /// walk: the left side follows only first parents, while `hide` still hides everything it
2118    /// reaches through any parent, as in Git. Only commits on the tips' first-parent chains are
2119    /// painted left, so the walk ends where those chains meet hidden history.
2120    ///
2121    /// # Errors
2122    ///
2123    /// Propagates commit read and parse errors for commits not reached from `hide`.
2124    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    /// Number of painted commits reachable from `side` only.
2188    fn count_only(&self, side: u8) -> usize {
2189        self.sides.values().filter(|&&s| s == side).count()
2190    }
2191
2192    /// The same paint with left and right exchanged, for walking from the right tips.
2193    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    /// Whether `side` reaches `oid`, or [`None`] when the walk never painted it.
2210    fn reached_by(&self, oid: ObjectId, side: u8) -> Option<bool> {
2211        self.sides.get(&oid).map(|s| s & side != 0)
2212    }
2213}
2214
2215/// Queue state for [`SidePaint::compute`].
2216#[derive(Default)]
2217struct PaintWalk {
2218    sides: OidHashMap<u8>,
2219    /// Queued commits, each flagged whether it keeps the walk going.
2220    queued: OidHashMap<bool>,
2221    popped: OidHashSet,
2222    heap: BinaryHeap<PaintKey>,
2223    seq: u64,
2224    /// Queued commits that still keep the walk going (see [`Self::mark`]).
2225    open: usize,
2226    /// Leave unreadable commits reached from the right side unqueued instead of failing.
2227    skip_missing_right: bool,
2228    /// Commits with a commit-graph generation keep the walk going only while the left side
2229    /// alone reaches them ([`HidePaintStop::TipsResolved`]).
2230    open_while_left_only: bool,
2231    /// Commits without a generation follow the same rule, and the walk ends by committer date
2232    /// ([`HidePaintStop::TipsResolvedDateSlop`]).
2233    date_slop: bool,
2234    /// Oldest committer date among commits without a generation popped while the left side
2235    /// alone reached them.
2236    left_floor: Option<i64>,
2237    /// Pass the left paint to first parents only ([`SidePaint::compute_hiding_first_parent`]).
2238    left_first_parent_only: bool,
2239}
2240
2241/// Commits a [`HidePaintStop::TipsResolvedDateSlop`] paint pops past its date guard, so a hide
2242/// commit dated slightly too old still repaints the ancestor it reaches. Git's `rev-list` walks
2243/// the same number of commits past its "everything left is uninteresting" point.
2244const PAINT_SLOP: usize = 5;
2245
2246/// How much older than a popped left-only commit a queued hide commit without a generation may
2247/// be dated and still keep a [`HidePaintStop::TipsResolvedDateSlop`] paint going: the clock
2248/// skew it tolerates exactly.
2249const UNRANKED_SKEW_ALLOWANCE: time::Duration = time::Duration::DAY;
2250
2251impl PaintWalk {
2252    /// Date-slop paint with nothing left-only queued: whether a queued commit without a
2253    /// generation may still reach a commit popped as left-only, so the walk goes on.
2254    ///
2255    /// A commit with a generation cannot: the graph holds every ancestor of its commits, and
2256    /// those all sort below commits outside it. Otherwise the walk continues while the next
2257    /// queued commit is dated within [`UNRANKED_SKEW_ALLOWANCE`] of the oldest left-only commit
2258    /// popped, then for [`PAINT_SLOP`] more commits.
2259    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    /// Add `side` to `oid`, queueing it when its paint grows.
2281    ///
2282    /// A queued commit keeps the walk going while it is not reachable from both
2283    /// sides, or when it was already popped: a commit popped before the other
2284    /// side reached it (equal or skewed dates) has to pass the new side on to
2285    /// the ancestors it already painted.
2286    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            // Hidden either way, and unpainted ancestors count as hidden too.
2304            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/// [`PaintWalk`] heap order: highest generation, then newest committer date,
2335/// then first queued (Git's priority queue is stable for ties).
2336#[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
2344/// Newest committer timestamp first, matching `git merge-base` stdout ordering.
2345fn 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            // Queue holds only stale commits (the popped one included): no new merge-base
2404            // candidates can appear, and walking on would follow the stale tail to the root.
2405            // `reduce_to_best` removes redundant bases afterward (Git paint_down_to_common).
2406            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
2419/// Walk two histories toward their intersection; optionally collect every minimal base.
2420fn 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
2528/// Fewest tips on one side of a paint for [`CommitGraphCache::prime_tips`] to read them up
2529/// front; merge-base and range walks start from one or two tips and keep the plain reads.
2530const 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    /// Parent overrides from `.git/info/grafts` (same as Git and [`crate::rev_list::CommitGraph`]).
2538    graft_parents: OidHashMap<Vec<ObjectId>>,
2539    /// Commit-graph file, when present: supplies parents + committer time without
2540    /// decompressing commit objects. Commits absent from it (or octopus merges)
2541    /// fall back to [`Self::parents_of`]'s object read.
2542    graph: Option<crate::commit_graph_file::CommitGraphChain>,
2543    /// Monotonic pack cursor for single-pack repos (deep-history benchmarks).
2544    single_pack: Option<crate::pack::SingleLocalPackReader>,
2545    /// Set when [`Self::single_pack`] could not be opened under a matching pack-read context.
2546    single_pack_unavailable: bool,
2547    /// When set, the commit objects [`Self::commit_time_for_heap`] reads from the object
2548    /// database, so a caller that renders the walked commits does not read them a second time.
2549    retained: Option<WalkedCommits>,
2550    /// True only while [`Self::commit_time_for_heap`] runs, so commits read for painting
2551    /// or ancestry checks are not retained.
2552    retain_reads: bool,
2553    /// Payload of the commit [`Self::parents_of`] parses, reused across reads. Linkage and
2554    /// [`Self::retained`] keep what the walk needs, so a per-commit allocation (and the
2555    /// object database's session copy of every walked commit) would only be thrown away.
2556    read_buf: Vec<u8>,
2557    /// Plain commits whose linkage [`Self::prime_tips`] recorded from their header blocks.
2558    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    /// `use_commit_graph: false` leaves [`Self::graph`] empty so every lookup parses the commit.
2567    fn with_commit_graph(repo: &'r Repository, use_commit_graph: bool) -> Self {
2568        let cfg = repo.config().unwrap_or_default();
2569        // Stop ancestry traversal only at genuinely-missing promisor objects, not
2570        // at every member of a promisor pack. The clone base commit lives in a
2571        // promisor pack but is fully present locally; treating it as a stop point
2572        // truncates ancestry and breaks fast-forward detection on a push from a
2573        // partial clone (t5616 "after fetching descendants of non-promisor
2574        // commits, gc works"). Missing parents are already handled by
2575        // `parents_of` returning no parents on `ObjectNotFound`, so we only need
2576        // to record the OIDs the partial clone knows are absent.
2577        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    /// Read `oid` into `buf` and return its kind. Delta bases stay cached, as for
2608    /// [`crate::odb::Odb::read`].
2609    ///
2610    /// The single local pack answers first; an object it does not hold (one in an alternate,
2611    /// say) is read through the object database, so a walk never mistakes it for missing.
2612    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    /// Parent rewrites can invalidate commit-graph linkage and create ancestry cycles.
2624    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    /// Open the reader over the single local pack, once per pack-read context.
2656    ///
2657    /// Loose objects may exist beside the pack: [`Self::read_object_into`] reads whatever the
2658    /// pack lacks through the object database.
2659    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            // Several packs, or none: the listing will not change during this walk.
2673            _ => 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        // Fast path: parents + committer time from the commit-graph file (no
2735        // object decompression). Misses (not in graph / octopus) fall through.
2736        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        // A plain commit without a replacement is its own peel target, so one read serves both
2755        // the peel and the parse; anything else (tags, replaced objects) peels first.
2756        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    /// [`Self::parents_of`] for a commit the commit-graph does not cover: read and parse its
2763    /// object through `buf`, then record its linkage.
2764    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    /// Record `oid`'s committer time and its parents, read from `commit_oid` (the commit
2796    /// `oid` peels to), after shallow, promisor and graft rewrites. Returns those parents.
2797    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    /// Whether `oid` names itself: no replace ref, promisor stop or shallow boundary rewrites it.
2822    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    /// Record the linkage of every plain packed commit among `tips` from its header block
2835    /// ([`crate::odb::Odb::read_packed_commit_header_block_into`]), reading them in parallel.
2836    ///
2837    /// Hide sets hold every ref in the repository, so a walk can start from hundreds of
2838    /// scattered tips whose dates it needs before taking a single step. Their reads are
2839    /// independent, and each one only needs the headers. Commits the commit-graph lists need
2840    /// no read. Tips this cannot prime (tags, loose or deltified commits, unparsable headers,
2841    /// read errors) are left to [`Self::peel_tip`] and the regular object reads, which report
2842    /// any error. Sides with fewer than [`PRIME_TIPS_MIN`] tips are left alone too.
2843    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    /// Peel a walk tip to its commit, like [`try_peel_to_commit_for_merge_base`], without a read
2879    /// for tips [`Self::prime_tips`] recorded or the commit-graph lists.
2880    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    /// Committer timestamp (unix seconds) for `oid`, peeling tags to their commit.
2889    /// Populated as a side effect of [`Self::parents_of`]; missing/unreadable
2890    /// objects report `0` (treated as oldest, so they prune away).
2891    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    /// [`Self::commit_time`] for a commit being queued for emission, retaining its parsed
2902    /// object when retention is enabled.
2903    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    /// Whether `ancestor` is an ancestor of (or equal to) `descendant`.
2911    ///
2912    /// Walks parents from `descendant` newest-first (a date-ordered heap),
2913    /// returning as soon as `ancestor` is reached instead of materialising the
2914    /// full ancestor closure. Once the frontier drops below `ancestor`'s commit
2915    /// date we keep going for a small slop window (tolerating non-monotonic
2916    /// committer dates / clock skew, like Git's `paint_down_to_common`) and then
2917    /// stop — bounding the work near the merge base rather than walking all of
2918    /// history.
2919    fn is_ancestor(&mut self, ancestor: ObjectId, descendant: ObjectId) -> Result<bool> {
2920        use std::collections::BinaryHeap;
2921
2922        // Compare against commit OIDs (the form the walk yields), peeling tags.
2923        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        // Tolerate up to this many commits below the cutoff before concluding the
2932        // ancestor is unreachable; absorbs realistic clock skew without walking
2933        // the whole graph.
2934        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    /// Mark branch tips that are ancestors of (merged into) any commit in `bases`.
2966    ///
2967    /// Uses commit-graph generations to stop the walk below the minimum generation among
2968    /// `tips`, matching Git's `tips_reachable_from_bases` / `--merged` batch filter.
2969    /// `bases` and `tips` must already be commit ids; they are not peeled again.
2970    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/// Dense bitset of ref-tip indices (for shared `--contains` walks).
3024#[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    /// Union `other` into `self`. Returns whether any new bit was set.
3045    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
3077/// Generation/date-ordered queue for [`RefContainsBatch::run`].
3078///
3079/// Each commit is popped at most once; tip bitsets merge at enqueue time like [`PaintWalk`].
3080struct 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            // A new tip reached this commit after an earlier pop (skewed dates / merge order).
3129            // Re-queue like [`PaintWalk::mark`] repaint so all active tips walk past here.
3130            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/// Per-tip results from one shared backward walk for `--contains` / `--no-contains`.
3149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3150pub(crate) struct RefContainsTipState {
3151    /// Any `--contains` target appears in this tip's history (OR semantics).
3152    pub contains_match: bool,
3153    /// Any `--no-contains` target appears in this tip's history.
3154    pub no_contains_match: bool,
3155}
3156
3157/// Query-scoped containment for a bounded list: each visited commit remembers whether
3158/// it reaches any target. Both successful paths and exhausted histories are shared by
3159/// subsequent tips, without a bitset sized to the entire ref inventory.
3160pub(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    /// Returns `None` for rewritten parents: ancestry need not be acyclic, so
3177    /// callers must use the batch walk instead of inferring negative ancestors.
3178    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            // Strict ancestors of the sole target cannot contain it. Seed that boundary
3188            // without reading its ancestors; expensive probes can extend it on demand.
3189            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                        // Dates choose exploration order only. Every negative result follows
3238                        // from ancestry of the target, regardless of clock skew.
3239                        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    /// Evaluate one peeled tip, caching positive and negative ancestry. Committer
3256    /// dates never exclude ancestry; only valid commit-graph generations may do so.
3257    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            // A nearby positive probe should not walk the target's older history. Once
3271            // a probe is expensive, advance both histories so an old side branch can
3272            // meet a proven negative boundary without walking to the repository root.
3273            if visited >= 1024 {
3274                if let Some(cut) = self.advance_negative_boundary(&active)? {
3275                    // Stop the whole active path below a newly proven negative ancestor,
3276                    // even when the DFS has already descended far beyond that commit.
3277                    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
3325/// One memoized shared-history walk over all candidate ref tips.
3326pub(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    /// Start a batch session on `repo`.
3335    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    /// Register a commit tip (already peeled; not re-read); returns its index
3345    /// (deduplicated by commit oid).
3346    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    /// Walk shared history from all registered tips and fill [`Self::tip_state`].
3361    ///
3362    /// # Errors
3363    ///
3364    /// Propagates object read failures during parent walks.
3365    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    /// State for a tip index from [`Self::register_tip`].
3447    #[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
3476/// Branch tips that are merged into any of `bases` (ancestors of a base commit).
3477///
3478/// Both `bases` and `tips` must be commit ids (peel refs and tags first).
3479///
3480/// # Errors
3481///
3482/// Propagates object read failures during the ancestry walk.
3483pub(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        // Same dates: the walk visits `left`'s parents in parent order first.
3718        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    /// Two short branches forking from the middle of a long chain: once only stale commits
3777    /// are queued the walk stops instead of following the shared tail to the root.
3778    #[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    /// Linear chain of `len` commits; `time(i)` dates commit `i` (0 is the root).
3836    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    /// Classify a pair with a cold cache and return the result with the parent lookups spent.
3855    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        // Every descendant is dated before its parent: the date order is wrong at each step,
3914        // which may cost extra walking but must not change the answer.
3915        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        // A second-parent ancestor is not on the first-parent chain.
3947        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    /// Trunk of `len` merges, each bringing in one side commit. Most side commits fork from the
4052    /// previous trunk commit; every seventh forks 30 merges back, so it sits off the first-parent
4053    /// chain of the trunk commit it is merged into. Side commit `i` is dated `10 * i`, merge `i`
4054    /// `10 * i + 5`. Returns the trunk (index 0 is the root).
4055    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        // Side branch committed with a clock far ahead of its descendants.
4485        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    /// 2035-01-01 and 2000-01-01, for histories whose dates run backwards.
4525    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        // Pushed commits outside the graph, dated before the ancestor.
4677        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    /// Point `refs` at their commits, write a reachable commit-graph and reopen the repository so
4753    /// walks load it.
4754    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    /// A ref reaches `skewed`, a commit only the old tip seems to reach, through a child dated
4768    /// before it; ten newer commits on another ref come first in date order.
4769    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        // One day of hourly commits behind the tips, then the slop.
4893        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        // `shared` is dated days after the hide chain that reaches it, so the walk pops it as
4907        // left-only before that chain arrives, beyond the skew allowance.
4908        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    /// Point `refs` at their commits, pack everything they reach with whole objects (no
4935    /// deltas), and reopen the repository so reads go through the pack.
4936    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    /// A trunk with one old tagged side commit per tag, every tag older than the trunk tip.
4959    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            // A ref on a descendant of `wip` keeps it reachable.
5077            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        // The target, then `wip` and the three commits down the chain to it.
5124        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        // The graft makes `base` a child of `tip`: each reaches the other.
5135        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}