GitoxideLabs/gitoxide · error
a continuation fork cannot target an unwritten empty commit
Error message
a continuation fork cannot target an unwritten empty commit
What it means
When building a continuation todo, a fork's parent step must already have a written commit (Pick/Copy/Resolved carrying an object id). If the referenced plan step is still `PlanCommit::Empty` — meaning no commit has been written for it yet — the continuation cannot anchor onto it and `prepare_continuation` bails.
Solutions
- Re-run the rebase so the referenced step is executed and a real commit is written, then create the continuation.
- Update the continuation to fork from the nearest ancestor step that has a written commit (Pick/Copy/Resolved).
- Regenerate the plan so empty steps are not used as fork parents.
- If the empty commit is genuinely unneeded, drop it from the plan and re-derive the continuation.
Example fix
// before // plan step 2 is PlanCommit::Empty(_), fork targets it prepare_continuation(repo, &plan, forks_at_step_2)?; // bails // after // fork from the last executed step instead prepare_continuation(repo, &plan, forks_at_step_1)?; // step 1 is PlanCommit::Resolved(id)
Defensive patterns
Strategy: validation
Validate before calling
// only fork from steps that already have a written commit
let ok = |p: rebase::PlanParent| match p {
rebase::PlanParent::Existing(_) => true,
rebase::PlanParent::Step(i) => !matches!(plan.steps[i].commit, rebase::PlanCommit::Empty(_)),
}; Try / catch
match prepare_continuation(repo, &plan, forks) {
Ok(todo) => /* proceed */,
Err(e) if e.to_string().contains("unwritten empty commit") => {
// retarget fork at nearest executed step, then retry
}
Err(e) => return Err(e),
} Prevention
- Only create continuations after the referenced steps executed
- Don't hand-edit or reorder plan steps that forks depend on
- Skip/drop empty commits in the plan rather than continuing onto them
- Validate plan steps before persisting continuation state
When it happens
Trigger: Calling `prepare_continuation` where a continuation fork references a plan step (via `PlanParent::Step`) whose commit is `rebase::PlanCommit::Empty(_)` — i.e. the plan lists an empty placeholder step that hasn't been executed/written during the interrupted rebase.
Common situations: Resuming an interrupted rebase at a point before the referenced step materialized; a plan edited by hand or by tooling that reordered steps so a fork points at a not-yet-executed empty step; an empty commit being skipped in an upstream rebase while a continuation still references it.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- rebase todo requires at least one -x/--hide revision when…
- the hidden and visible revisions have no editable fork point
- the revisions have multiple editable fork points
- aborted without changes: conflict while applying ; pass…
- an edit unexpectedly produced a merge conflict
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/6d81658b9d0316a3.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/todo.rs:298
};
let mut document = b"<!-- Rebase help follows. Saving unchanged continues the materialized rebase; empty this file or remove the tix-rebase-state-v2 comment to cancel. -->\n# Continue materialized rebase\n\n".to_vec();
let mut body = Vec::new();
let mut enrichments = crate::enrich::open(repo)?;
let mut tree_enrichments = crate::enrich::open_tree(repo)?;
for (index, step) in plan.steps.iter().enumerate() {
let continues = matches!(step.parent, rebase::PlanParent::Step(parent) if parent + 1 == index);
if !continues {
if index > 0 {
body.push(b'\n');
}
let parent = match step.parent {
rebase::PlanParent::Existing(id) => id,
rebase::PlanParent::Step(parent) => match plan.steps[parent].commit {
rebase::PlanCommit::Pick(id) | rebase::PlanCommit::Copy(id) | rebase::PlanCommit::Resolved(id) => {
id
}
rebase::PlanCommit::Empty(_) => {
anyhow::bail!("a continuation fork cannot target an unwritten empty commit")
}
},
};
let base_title = (parent == plan.base)
.then(|| anchor_title(repo, parent))
.transpose()?
.unwrap_or_default();
write_fork_heading(
&mut body,
repo,
parent,
(parent == plan.base).then_some(("base", base_title.as_str())),
show_change_ids,
)?;
if matches!(step.parent, rebase::PlanParent::Existing(_)) {
write_plan_refs_at(&mut body, &plan.expected_refs, step.parent)?;
}
}View on GitHub (pinned to e73179060b)