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
- Discard the stale rebase state and restart the rebase
- Ensure the HEAD ref name recorded in the state matches an expected ref marked editable
- 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
- Only modify rebase state with tix itself
- Keep the same tix version for start and resume of a rebase
- Verify state completeness after crashes before resuming
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
- the root of a repeated rebase must be pending
- the parent of a repeated rebase must not be pending
- the current checkout has a pending rebase; time-travel to…
- has a pending rebase
- the rebase state contains duplicate scope commits
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)