GitoxideLabs/gitoxide · error

a detached checked-out root commit cannot be removed

Error message

a detached checked-out root commit cannot be removed

What it means

When a rebase removes the commit currently checked out in a linked worktree, the worktree must be moved to some replacement tree. If the new commit is `None` (commit removed entirely) and the worktree HEAD is detached (no referent name), there is no branch to re-point and no commit to stay on, so removal of that detached checked-out root commit is refused.

Solutions

  1. Attach the worktree's HEAD to a branch (`git switch -c <branch>` or `git switch main`) so the referent-name fallback can move it to the empty tree.
  2. Attach the worktree to a surviving commit outside the rewritten range before running the removal.
  3. Don't remove that root commit — rewrite it instead so `new` is `Some(...)`.
  4. Remove the worktree entirely before the rebase edit.

Example fix

// before: worktree detached on the commit being removed
// bail: "a detached checked-out root commit cannot be removed"
// after: attach the worktree first
run_in(worktree_path, &["git", "switch", "-c", "keep-alive"])?;
edit.rebase_drop(root_id)?; // now succeeds
Defensive patterns

Strategy: validation

Validate before calling

if let Some(wt) = worktree_holding(repo, old_commit_id) {
    let wt_repo = wt.into_repo()?;
    let detached = wt_repo.head()?.referent_name().is_none();
    if detached && removal_planned(old_commit_id) {
        // attach the worktree to a branch before the edit
    }
}

Prevention

When it happens

Trigger: A rebase edit plans `new: None` for a commit `old` that a worktree has checked out; the worktree's `head()?.referent_name()` is `None` (detached HEAD), so neither `repo.find_commit(new)` nor `repo.empty_tree()` fallback applies and the code bails at `gix-tix/src/edit/rebase.rs:2929`.

Common situations: Rewriting history that drops the very root/first commit while a CI or scratch worktree sits detached on exactly that commit; force-cleanup scripts that drop root commits of experimental branches checked out detached in secondary worktrees.

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

Appendix: source

Thrown at gix-tix/src/edit/rebase.rs:2929

                    continue;
                }
                anyhow::bail!(
                    "cannot delete {} because another worktree has it checked out",
                    expected.name.shorten()
                );
            }
            Some(expected) => Some(expected.new),
            None => rewritten.get(&old).copied(),
        };
        let Some(new) = new else { continue };
        if worktree_repo.workdir().is_none() && worktree_repo.is_bare() {
            continue;
        }
        let old_tree = worktree_repo.find_commit(old)?.tree_id()?.detach();
        let new_tree = match new {
            Some(new) => repo.find_commit(new)?.tree_id()?.detach(),
            None if worktree_repo.head()?.referent_name().is_some() => repo.empty_tree().id,
            None => anyhow::bail!("a detached checked-out root commit cannot be removed"),
        };
        if old_tree == new_tree {
            continue;
        }
        let workdir = worktree_repo
            .workdir()
            .filter(|path| path.is_dir())
            .context("an affected worktree is inaccessible")?
            .to_owned();
        out.push(Transition {
            repo: worktree_repo,
            workdir,
            old: old_tree,
            new: new_tree,
        });
    }
    Ok(out)
}

View on GitHub (pinned to e73179060b)