GitoxideLabs/gitoxide · error

the recorded HEAD ref is not editable

Error message

the recorded HEAD ref is not editable

What it means

The rebase state records a HEAD ref name, but validation requires that this name appears among the expected refs flagged as `editable`. If the recorded HEAD ref is missing from expected_refs or is not editable, the state cannot safely move HEAD during the rebase, so it is rejected.

Solutions

  1. Discard the stale rebase state and restart the rebase
  2. Ensure the HEAD ref name recorded in the state matches an expected ref marked editable
  3. Regenerate state with the same tix version that started the rebase
Defensive patterns

Strategy: validation

Validate before calling

if let Some(name) = &state.head_ref {
    assert!(state.expected_refs.iter().any(|r| r.editable && r.name == *name),
        "HEAD ref must be listed as an editable expected ref");
}

Try / catch

if let Err(e) = result {
    if e.to_string().contains("HEAD ref is not editable") {
        // treat state as corrupt: abandon and restart rebase
    }
}

Prevention

When it happens

Trigger: Parsing a rebase state where `state.head_ref` is Some but no entry in `state.expected_refs` has `editable == true` and a matching name. Caused by hand-edited state files, partially written state, or state from a different tix version.

Common situations: Editing the state file to rename HEAD; state files produced by incompatible tix versions; truncated state writes after a crash.

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

Appendix: source

Thrown at gix-tix/src/edit/todo.rs:802

    if state.tips.iter().copied().collect::<HashSet<_>>().len() != state.tips.len() {
        anyhow::bail!("the rebase state contains duplicate tips");
    }
    let mut refs = HashSet::new();
    for reference in &state.expected_refs {
        if !refs.insert(reference.name.as_bstr()) {
            anyhow::bail!("the rebase state contains duplicate refs");
        }
        if !scope.contains(&reference.target) && reference.target != state.base && reference.target != state.onto {
            anyhow::bail!("a captured ref does not logically point into the rebase scope");
        }
    }
    if let Some(name) = &state.head_ref
        && !state
            .expected_refs
            .iter()
            .any(|reference| reference.editable && reference.name == *name)
    {
        anyhow::bail!("the recorded HEAD ref is not editable");
    }
    for tip in &state.tips {
        repo.find_commit(*tip).context("could not find a recorded rebase tip")?;
    }
    for id in &state.scope {
        let commit = repo
            .find_commit(*id)
            .context("could not find a recorded scope commit")?;
        let parent = commit
            .parent_ids()
            .next()
            .map(gix::Id::detach)
            .context("a recorded scope commit has no parent")?;
        if parent != state.base && !scope.contains(&parent) && !continuation_sources.contains(id) {
            anyhow::bail!("a recorded scope commit is disconnected from the rebase base");
        }
    }
    if state.resolved.is_some_and(|id| !scope.contains(&id)) {

View on GitHub (pinned to e73179060b)