GitoxideLabs/gitoxide · error

undo/redo cannot delete a worktree HEAD

Error message

undo/redo cannot delete a worktree HEAD

What it means

During undo/redo, `worktree_transitions` projects what each affected worktree's HEAD will look like after applying the ref changes. If the projection yields `State::Missing` for a worktree HEAD, applying the changes would leave that worktree without a valid HEAD, which is unsupported, so the operation aborts with this error before touching anything.

Solutions

  1. Point the affected worktree's HEAD at a branch that survives the undo/redo (e.g. `git -C <worktree> switch <other-branch>`).
  2. Detach the worktree HEAD (`git switch --detach <commit>`) if the underlying branch must remain un-undoable.
  3. Remove the worktree (`git worktree remove`) if it is no longer needed.
  4. Re-order operations so the worktree HEAD is first re-created before undoing past its deletion.

Example fix

// before: undo deletes the branch worktree B has checked out -> HEAD would go Missing
repo.undo()?;
// after: re-point worktree B's HEAD first
// git -C ../wt-b switch main
repo.undo()?;
Defensive patterns

Strategy: validation

Validate before calling

// before undo, ensure no worktree HEAD resolves solely through refs the change deletes
for wt in affected_worktrees {
    let head = wt.head_ref_name();
    let dies = changes.iter().any(|c| &c.name == head && c.after == State::Missing);
    if dies { anyhow::bail!("worktree {wt:?} HEAD would be deleted; re-point it first"); }
}

Prevention

When it happens

Trigger: Running undo or redo (`apply_with_worktrees` -> `worktree_transitions`) when the recorded changes delete or move the ref(s) that a worktree's HEAD (symbolic or direct) resolves through, such that the projected HEAD state becomes Missing.

Common situations: Undoing a change that deleted a branch which is another worktree's HEAD; deleting branches checked out in linked worktrees then trying to undo back across that deletion.

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/57e880e1993dc7ba. Report an issue: GitHub.

Appendix: source

Thrown at gix-tix/src/edit/undo.rs:426

    }

    let mut out = Vec::new();
    let mut seen = HashSet::new();
    for worktree_repo in repos {
        if !seen.insert(worktree_repo.git_dir().to_owned()) {
            continue;
        }
        let head = worktree_repo.head().context("could not inspect a worktree HEAD")?;
        let old_id = head.id().map(gix::Id::detach);
        let raw_head = worktree_repo
            .find_reference("HEAD")
            .context("could not read a worktree HEAD reference")?;
        let raw_head = state_from_target_ref(raw_head.target());
        let current = gix::path::realpath(worktree_repo.git_dir())
            .context("could not resolve an affected worktree Git directory")?
            == current_git_dir;
        let projected_head = projected_ref_state(&worktree_repo, b"HEAD".as_bstr(), &raw_head, changes, current)?;
        ensure!(
            projected_head != State::Missing,
            "undo/redo cannot delete a worktree HEAD"
        );
        let new_id = resolve_state(&worktree_repo, &projected_head, changes, current, &mut HashSet::new())?;
        let old = tree_id(&worktree_repo, old_id).context("could not inspect the current worktree tree")?;
        let new = tree_id(&worktree_repo, new_id).context("could not inspect the destination worktree tree")?;
        if old == new || (worktree_repo.workdir().is_none() && worktree_repo.is_bare()) {
            continue;
        }
        let workdir = worktree_repo
            .workdir()
            .filter(|path| path.is_dir())
            .context("an affected worktree is inaccessible")?
            .to_owned();
        out.push(WorktreeTransition {
            repo: worktree_repo,
            workdir,
            old,

View on GitHub (pinned to e73179060b)