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

  1. Re-run the rebase so the referenced step is executed and a real commit is written, then create the continuation.
  2. Update the continuation to fork from the nearest ancestor step that has a written commit (Pick/Copy/Resolved).
  3. Regenerate the plan so empty steps are not used as fork parents.
  4. 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

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


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)