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
- Verify the `base` commit is an ancestor of `head` before starting the walk; bail with a clear error otherwise.
- Restructure the loop to break explicitly when `stack` becomes empty instead of using `expect`.
- 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
- Always verify base-vs-head ancestry before rewriting a commit stack.
- Re-read refs immediately before the operation to avoid stale-graph mismatches.
- Prefer explicit bail!/error returns over expect() for walk-termination edge cases.
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
- continued refs have a target
- descendant merge commits cannot be rebased
- rebase worker panicked
- parser must have set some object value
- successful iteration has outcome
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)