GitoxideLabs/gitoxide · error

the review base must be an ancestor of the reviewed commit

Error message

the review base must be an ancestor of the reviewed commit

What it means

`review::start` validates that the review base is a strict ancestor of the reviewed commit: if `tip == base` or `graph.is_ancestor(base, tip)` is false, a review would have an empty or invalid diff, so it bails. This guarantees every review compares a base strictly below the tip in history.

Solutions

  1. Pass a base that is a proper ancestor of the tip (e.g. the merge-base of the branch and main).
  2. Swap the arguments if base and tip were reversed.
  3. Pick a different, earlier commit as the review base.
  4. If tip == base is intended, there is nothing to review — skip the review creation.

Example fix

// before: base may equal or exceed tip
review::start(repo, tip_id, tip_id)?; // bails
// after: derive a valid ancestor base
let base = repo.merge_base(base_candidate, tip_id)?;
assert!(base != tip_id);
review::start(repo, base, tip_id)?;
Defensive patterns

Strategy: validation

Validate before calling

let graph = repo.rewrite_graph()?;
if tip_id == base_id || !graph.is_ancestor(base_id, tip_id) {
    // pick a proper ancestor (e.g. merge-base) before starting the review
}

Type guard

fn is_valid_review_pair(graph: &Graph, base: ObjectId, tip: ObjectId) -> bool {
    base != tip && graph.is_ancestor(base, tip)
}

Prevention

When it happens

Trigger: Calling `review::start` (via `starts_review_with_base_index_and_tip_worktree` and similar flows) with `base` equal to `tip`, or with a `base` that is not an ancestor of `tip` (diverged branches, swapped arguments, base on an unrelated line of history).

Common situations: Passing base/tip in the wrong order; reviewing a squashed/rebased commit against a base that was rewritten; accidentally selecting the same commit for both ends; reviewing across diverged branches.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/cdfdd4cf7380fc77. Report an issue: GitHub.

Appendix: source

Thrown at gix-tix/src/edit/review.rs:119

}

#[tracing::instrument(skip_all, fields(%tip, %base))]
pub(crate) fn start(
    repository_path: &Path,
    bare: bool,
    graph: &history::HistoryGraph,
    tip: ObjectId,
    base: ObjectId,
) -> Result<Started> {
    let repo = open_repository(repository_path, bare, false).context("could not open repository to start review")?;
    let workdir = repo.workdir().context("review requires a worktree")?.to_owned();
    let head = repo.head().context("could not read HEAD before review")?;
    let restore = (
        head.referent_name().map(ToOwned::to_owned),
        head.id().map(gix::Id::detach),
    );
    if tip == base || !graph.is_ancestor(base, tip) {
        anyhow::bail!("the review base must be an ancestor of the reviewed commit");
    }
    for (label, id) in [("reviewed commit", tip), ("review base", base)] {
        let commit = repo
            .find_commit(id)
            .with_context(|| format!("could not find {label}"))?
            .decode()?
            .into_owned()?;
        if super::rebase::is_pending(&commit) {
            anyhow::bail!("{label} has a pending rebase");
        }
    }
    ensure_clean(&workdir)?;

    let departure_pin = match restore.1 {
        Some(id) => {
            let target = restore.0.clone().map_or(Target::Object(id), Target::Symbolic);
            Some((
                super::time_travel::create_pin(&repo, target, id, "tix review departure")?,

View on GitHub (pinned to e73179060b)