GitoxideLabs/gitoxide · error
HEAD attachment changed while resolving the conflict…
Error message
HEAD attachment changed while resolving the conflict; return to the conflict checkout or exit
What it means
When finalizing an external conflict resolution, the tool re-opens the repository and compares the current HEAD referent name with the one recorded in `expected.reference`. If the user (or another process) attached HEAD to a different branch while the conflict was being resolved, the ensure aborts and asks to return to the conflict checkout or exit.
Solutions
- Return to the conflict-checkout state (checkout the recorded reference `expected.reference`) before retrying the resolution.
- Undo the branch switch: `git checkout <expected branch>` so HEAD attachment matches what was recorded.
- Avoid running `git switch`/`git checkout` while an external conflict resolution is open.
Example fix
// before
anyhow::ensure!(
reference == expected.reference,
"HEAD attachment changed while resolving the conflict; return to the conflict checkout or exit"
);
// after
if reference != expected.reference {
if let Some(name) = &expected.reference {
repository.checkout_reference(name)?;
}
}
anyhow::ensure!(
reference == expected.reference,
"HEAD attachment changed while resolving the conflict; return to the conflict checkout or exit"
); Defensive patterns
Strategy: validation
Validate before calling
let current = repo.head()?.referent_name().map(|n| n.to_owned());
if current != expected.reference {
eprintln!("switch back to {:?} before finalizing the conflict", expected.reference);
return;
} Type guard
fn head_matches(repo: &gix::Repository, expected: &Option<gix::refs::FullName>) -> bool {
repo.head().ok().and_then(|h| h.referent_name().map(|n| n.to_owned())) == *expected
} Try / catch
if let Err(e) = finalize_conflict(&expected) {
if e.to_string().contains("HEAD attachment changed") {
prompt_return_to_conflict_checkout();
} else { return Err(e.into()); }
} Prevention
- Don't run `git switch`/`git checkout` in other terminals while an external conflict resolution is open.
- Pause IDE auto-fetch/checkout integrations during conflict resolution.
- Record and re-verify HEAD referent at each finalization step.
When it happens
Trigger: `head.referent_name()` returns a different branch than `expected.reference` — user ran `git checkout <other-branch>` or `git switch` while the external conflict resolution was in progress (e.g. an editor or merge tool was open).
Common situations: Users switching branches in another terminal or IDE while resolving conflicts in an external tool; scripts running `git switch` concurrently; resuming a session after unrelated git commands ran.
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 conflict checkout did not leave HEAD at
- HEAD moved to an unrelated commit while resolving the…
- Tree conflicted
- File conflicted
- Tree conflicted, refusing to write commit
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/55bf122c50185634.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/lib.rs:4408
Ok(ConflictHead { reference, parents })
}
fn reconcile_external_conflict(
repository_path: &Path,
bare: bool,
pending: &mut Option<PendingConflictResolution>,
) -> Result<ExternalConflictResolution> {
let state = pending
.as_ref()
.context("external conflict reconciliation requires pending state")?;
let Some(expected) = state.head.as_ref() else {
return Ok(ExternalConflictResolution::Current);
};
let repository =
open_repository(repository_path, bare, false).context("could not inspect external conflict resolution")?;
let head = repository.head().context("could not inspect HEAD after the conflict")?;
let reference = head.referent_name().map(ToOwned::to_owned);
anyhow::ensure!(
reference == expected.reference,
"HEAD attachment changed while resolving the conflict; return to the conflict checkout or exit"
);
let replacement = head
.id()
.map(gix::Id::detach)
.context("HEAD became unborn while resolving the conflict")?;
drop(head);
if replacement == state.commit {
return Ok(ExternalConflictResolution::Current);
}
let replacement_commit = repository
.find_commit(replacement)
.context("the replacement HEAD is not a commit")?
.decode()
.context("could not decode the replacement HEAD commit")?
.into_owned()View on GitHub (pinned to e73179060b)