GitoxideLabs/gitoxide · error

HEAD no longer directly references its replacement commit

Error message

HEAD no longer directly references its replacement commit

What it means

Final check during external conflict finalization: the recorded undo state of HEAD's referent must be `State::Object(replacement)`, i.e. HEAD directly references the replacement commit. If undo state reports anything else (symbolic elsewhere, unborn, different object), HEAD no longer directly references the replacement and finalization aborts.

Solutions

  1. Re-point the reference at the replacement commit (`git update-ref <name> <replacement>`) so the direct object reference is restored.
  2. Re-run the conflict checkout to rebuild consistent undo state, then finalize again.
  3. Check for concurrent git processes mutating refs during finalization.

Example fix

// before
anyhow::ensure!(
    edit::undo::state(&repository, name.as_ref())? == edit::undo::State::Object(replacement),
    "HEAD no longer directly references its replacement commit"
);
// after
if edit::undo::state(&repository, name.as_ref())? != edit::undo::State::Object(replacement) {
    repository.reference(name.as_bstr(), replacement, gix::refs::transaction::PreviousValue::Any)?;
}
Defensive patterns

Strategy: validation

Validate before calling

let state = edit::undo::state(&repo, name.as_bstr())?;
if state != edit::undo::State::Object(replacement) {
    eprintln!("HEAD does not directly reference the replacement; restoring...");
}

Type guard

fn head_directly_at(repo: &gix::Repository, name: &gix::refs::FullNameRef, replacement: gix::ObjectId) -> bool {
    matches!(edit::undo::state(repo, name).ok().as_ref(), Some(edit::undo::State::Object(id)) if *id == replacement)
}

Try / catch

if edit::undo::state(&repo, name.as_bstr())? != edit::undo::State::Object(replacement) {
    restore_reference(name, replacement)?; // then retry finalize
}

Prevention

When it happens

Trigger: `edit::undo::state(&repository, name)` != `State::Object(replacement)` — HEAD was detached, re-pointed, or its undo bookkeeping was overwritten between the parent checks and finalization.

Common situations: Concurrent `git reset`/`git checkout` during resolution; stale undo state after a crash; running the finalize step twice where the first pass mutated undo bookkeeping.

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/7855f77846b062bb. Report an issue: GitHub.

Appendix: source

Thrown at gix-tix/src/lib.rs:4450

    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
        .reference
        .clone()
        .unwrap_or_else(|| "HEAD".try_into().expect("valid reference name"));
    anyhow::ensure!(
        edit::undo::state(&repository, name.as_ref())? == edit::undo::State::Object(replacement),
        "HEAD no longer directly references its replacement commit"
    );
    let finalized = if edit::rebase::is_pending(&replacement_commit) {
        drop(index);
        let graph = edit::loaded_graph(&repository).context("could not load history to finalize the external amend")?;
        let outcome = edit::head::amend_index_reporting(repository, &graph)
            .context("could not finalize the externally amended pending commit")?
            .context("the externally amended pending commit was not finalized")?;
        let selected = outcome
            .selected
            .context("finalizing the externally amended conflict did not select its result")?;
        Some((selected, outcome.ref_changes))
    } else {
        None
    };
    let accepted = state.commit;
    let mut state = pending

View on GitHub (pinned to e73179060b)