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

  1. Return to the conflict-checkout state (checkout the recorded reference `expected.reference`) before retrying the resolution.
  2. Undo the branch switch: `git checkout <expected branch>` so HEAD attachment matches what was recorded.
  3. 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

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


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)