{"record":{"id":"b217933908d5a7fa","repo":"GitoxideLabs/gitoxide","slug":"a-stack-always-contains-head","errorCode":null,"errorMessage":"a stack always contains HEAD","messagePattern":"a stack always contains HEAD","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gix-tix/src/edit/rebase.rs","lineNumber":662,"sourceCode":"    graph: &HistoryGraph,\n    base: ObjectId,\n    head: ObjectId,\n    target: ObjectId,\n) -> Result<Plan> {\n    if repo.head_id()?.detach() != head {\n        anyhow::bail!(\"the move source must be the current HEAD\");\n    }\n    if !graph.is_ancestor(base, head) {\n        anyhow::bail!(\"the stack base must be an ancestor of HEAD\");\n    }\n    graph\n        .parents_of(target)\n        .context(\"the move target is not in the loaded history\")?;\n\n    let mut stack = vec![head];\n    let mut stack_parent = HashMap::new();\n    loop {\n        let id = *stack.last().expect(\"a stack always contains HEAD\");\n        let parents = graph.parents_of(id).context(\"a moved stack commit is incomplete\")?;\n        let [parent] = parents.as_slice() else {\n            anyhow::bail!(\"moving a stack requires every commit to have exactly one parent\");\n        };\n        stack_parent.insert(id, *parent);\n        if id == base {\n            break;\n        }\n        stack.push(*parent);\n    }\n    stack.reverse();\n    let stack_set: HashSet<_> = stack.iter().copied().collect();\n    if stack_set.contains(&target) {\n        anyhow::bail!(\"the move target must not be part of the moved stack\");\n    }\n    let base_parent = stack_parent[&base];\n    if base_parent == target {\n        anyhow::bail!(\"the stack is already directly above the move target\");","sourceCodeStart":644,"sourceCodeEnd":680,"githubUrl":"https://github.com/GitoxideLabs/gitoxide/blob/e73179060badf27222d790981fac3f84c1830a7e/gix-tix/src/edit/rebase.rs#L644-L680","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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`."],"exampleFix":"// before\nlet id = *stack.last().expect(\"a stack always contains HEAD\");\n// after\nlet Some(&id) = stack.last() else {\n    anyhow::bail!(\"stack walk drained before reaching base; base {:?} is not an ancestor of head\", base);\n};","handlingStrategy":"validation","validationCode":"// Before starting the stack move, confirm base is an ancestor of head:\nanyhow::ensure!(\n    graph.is_ancestor(base, head),\n    \"base commit must be an ancestor of HEAD to move a stack\"\n);","typeGuard":"fn stack_end(stack: &[gix::Id]) -> Option<&gix::Id> {\n    stack.last()\n}\n// use `let Some(&id) = stack_end(&stack) else { bail!(...) };`","tryCatchPattern":"// Rust panics are not catchable via Result; fail fast with a clear message instead:\nlet Some(&id) = stack.last() else {\n    anyhow::bail!(\"stack walk drained early; base not reachable from HEAD\");\n};","preventionTips":["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."],"tags":["panic","rebase","history-rewrite","invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"e73179060badf27222d790981fac3f84c1830a7e","analyzedAt":"2026-09-08T11:26:50.865Z","contentChangedAt":"2026-09-08T11:26:50.865Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}