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

  1. Reset the branch back to a commit with the expected parents (`git reset --hard <expected-parent>` plus the replacement) before resuming.
  2. Return to the conflict checkout state and redo the resolution to refresh `expected`.
  3. 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

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


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 = expected

View on GitHub (pinned to e73179060b)