GitoxideLabs/gitoxide · error
HEAD moved to an unrelated commit while resolving the…
Error message
HEAD moved to an unrelated commit while resolving the conflict; return to the conflict checkout or exit
What it means
During external conflict finalization, HEAD's current commit (the replacement) is decoded and its parent list is compared with `expected.parents`. If the parent list differs, HEAD was moved to an unrelated commit while the conflict was open, so finalization is refused.
Solutions
- Reset the branch back to a commit with the expected parents (`git reset --hard <expected-parent>` plus the replacement) before resuming.
- Return to the conflict checkout state and redo the resolution to refresh `expected`.
- Don't amend/commit/rebase on the branch while an external conflict resolution is pending.
Example fix
// before
anyhow::ensure!(
replacement_commit.parents.as_slice() == expected.parents,
"HEAD moved to an unrelated commit while resolving the conflict; return to the conflict checkout or exit"
);
// after
if replacement_commit.parents.as_slice() != expected.parents {
tracing::warn!("HEAD ancestry changed during conflict resolution; refreshing expected state");
return Err(ConflictStale::Retry.required());
} Defensive patterns
Strategy: validation
Validate before calling
let head_commit = repo.head()?.id().context("unborn HEAD")?.find_commit()?.decode()?.into_owned()?;
if head_commit.parents != expected.parents {
eprintln!("HEAD ancestry changed; restart the conflict resolution");
return;
} Type guard
fn parents_unchanged(c: &gix_object::Commit, expected: &[gix::ObjectId]) -> bool {
c.parents.as_slice() == expected
} Try / catch
match finalize() {
Err(e) if e.to_string().contains("unrelated commit") => restart_conflict_resolution(),
other => other,
} Prevention
- Avoid amend/rebase/new commits on the branch while conflicts are pending.
- Snapshot expected.parents at checkout and re-check before every mutation.
- Use a dedicated worktree for conflict resolution to isolate it from other work.
When it happens
Trigger: HEAD's commit at finalize time has parents != those recorded when the conflict checkout happened — e.g. an amend, rebase, or new commit created on the branch during resolution, then HEAD points at a commit whose ancestry diverges.
Common situations: User runs `git commit --amend`, `git pull --rebase`, or makes a new commit in another terminal while the external merge-tool conflict resolution is pending.
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 attachment changed while resolving the conflict…
- 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/4653a7912de1e1db.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/lib.rs:4428
"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()
.context("could not own the replacement HEAD commit")?;
anyhow::ensure!(
replacement_commit.parents.as_slice() == expected.parents,
"HEAD moved to an unrelated commit while resolving the conflict; return to the conflict checkout or exit"
);
let index = repository
.open_index()
.context("could not inspect the conflict index")?;
if index
.entries()
.iter()
.any(|entry| entry.stage() != gix::index::entry::Stage::Unconflicted)
{
return Ok(ExternalConflictResolution::Changed);
}
if edit::create::index_tree(&repository, &index)? != replacement_commit.tree {
return Ok(ExternalConflictResolution::Changed);
}
let name = expectedView on GitHub (pinned to e73179060b)