GitoxideLabs/gitoxide · error

a stack always contains HEAD

Error message

a stack always contains HEAD

What it means

This `expect` panic (gix-tix/src/edit/rebase.rs:662) asserts that the commit-walk `stack` is never empty while rewriting history: the loop only pops entries after pushing their parent, and HEAD is pushed before the loop, so `stack.last()` should always succeed. Popping the final element without termination makes the 'a stack always contains HEAD' invariant fail.

Solutions

  1. Verify the `base` commit is an ancestor of `head` before starting the walk; bail with a clear error otherwise.
  2. Restructure the loop to break explicitly when `stack` becomes empty instead of using `expect`.
  3. Re-check that `graph.parents_of(id)` cannot fail for the base sentinel and that `base` is pushed/handled symmetrically with `head`.

Example fix

// before
let id = *stack.last().expect("a stack always contains HEAD");
// after
let Some(&id) = stack.last() else {
    anyhow::bail!("stack walk drained before reaching base; base {:?} is not an ancestor of head", base);
};
Defensive patterns

Strategy: validation

Validate before calling

// Before starting the stack move, confirm base is an ancestor of head:
anyhow::ensure!(
    graph.is_ancestor(base, head),
    "base commit must be an ancestor of HEAD to move a stack"
);

Type guard

fn stack_end(stack: &[gix::Id]) -> Option<&gix::Id> {
    stack.last()
}
// use `let Some(&id) = stack_end(&stack) else { bail!(...) };`

Try / catch

// Rust panics are not catchable via Result; fail fast with a clear message instead:
let Some(&id) = stack.last() else {
    anyhow::bail!("stack walk drained early; base not reachable from HEAD");
};

Prevention

When it happens

Trigger: Calling the stack-movement routine (`run` path that moves a stack of commits onto a target) on history where the walk reaches a commit with no parent handled by the loop-termination condition — e.g. the `base` commit is not actually an ancestor of `head`, or the graph lookup for `base` failed silently, so the pop loop drains the stack past HEAD.

Common situations: Moving a commit stack when the specified base commit does not lie on the first-parent chain from HEAD (detached or rewritten history), or after an external operation (gc, reset) changed the refs the plan was built against.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

Thrown at gix-tix/src/edit/rebase.rs:662

    graph: &HistoryGraph,
    base: ObjectId,
    head: ObjectId,
    target: ObjectId,
) -> Result<Plan> {
    if repo.head_id()?.detach() != head {
        anyhow::bail!("the move source must be the current HEAD");
    }
    if !graph.is_ancestor(base, head) {
        anyhow::bail!("the stack base must be an ancestor of HEAD");
    }
    graph
        .parents_of(target)
        .context("the move target is not in the loaded history")?;

    let mut stack = vec![head];
    let mut stack_parent = HashMap::new();
    loop {
        let id = *stack.last().expect("a stack always contains HEAD");
        let parents = graph.parents_of(id).context("a moved stack commit is incomplete")?;
        let [parent] = parents.as_slice() else {
            anyhow::bail!("moving a stack requires every commit to have exactly one parent");
        };
        stack_parent.insert(id, *parent);
        if id == base {
            break;
        }
        stack.push(*parent);
    }
    stack.reverse();
    let stack_set: HashSet<_> = stack.iter().copied().collect();
    if stack_set.contains(&target) {
        anyhow::bail!("the move target must not be part of the moved stack");
    }
    let base_parent = stack_parent[&base];
    if base_parent == target {
        anyhow::bail!("the stack is already directly above the move target");

View on GitHub (pinned to e73179060b)