GitoxideLabs/gitoxide · error

a symbolic reference chain contains a cycle

Error message

a symbolic reference chain contains a cycle

What it means

`ref_chain_reaches_queue` walks a symbolic reference chain (ref -> symref target -> next target) looking for an undo-queue ref. To guarantee termination it records each visited name in a `HashSet`; re-visiting a name means the symref chain is cyclic, which is never valid, so it raises this error instead of looping forever.

Solutions

  1. Find the cycle with `git for-each-ref --format='%(refname) %(symref)'` and inspect the offending symrefs.
  2. Break the cycle by repointing or deleting one of the symbolic refs (e.g. `git symbolic-ref refs/foo refs/main` or `git update-ref -d refs/foo`).
  3. Never create symref cycles programmatically; validate targets before `create_reference(..., Target::Symbolic(...))`.

Example fix

// before: creating a cycle
repo.reference("refs/a").create(gix::refs::Target::Symbolic("refs/b".into()), ...);
repo.reference("refs/b").create(gix::refs::Target::Symbolic("refs/a".into()), ...);
// after: point the symref at a real (non-symref) target
repo.reference("refs/b").create(gix::refs::Target::Object(commit_id), ...);
Defensive patterns

Strategy: validation

Validate before calling

// verify the symref chain from a name terminates before operations
fn symref_chain_is_acyclic(repo: &gix::Repository, name: &str) -> anyhow::Result<bool> {
    let mut seen = std::collections::HashSet::new();
    let mut cur = name.to_owned();
    while let Some(r) = repo.try_find_reference(&cur)? {
        let Some(next) = r.target().try_name() else { return Ok(true) };
        if !seen.insert(next.to_owned()) { return Ok(false); }
        cur = next.to_string();
    }
    Ok(true)
}

Prevention

When it happens

Trigger: Any lookup that walks symbolic refs from a name while the repository contains a symref cycle, e.g. ref A -> ref B -> ref A. Triggered from `ref_chain_reaches_queue` during undo planning.

Common situations: Hand-edited or corrupted refs directory; a tool or script created `refs/foo` as a symref of `refs/bar` and vice versa; repository tampering or interrupted ref updates.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/0943ecc9da45054a. Report an issue: GitHub.

Appendix: source

Thrown at gix-tix/src/edit/undo.rs:123

                anyhow::Error::new(err).context("could not atomically move references and the undo cursor"),
            ));
        }
        Ok(())
    }
}

pub(crate) fn is_queue_ref(name: &BStr) -> bool {
    name.as_bytes() == TIP_REF.as_bytes() || name.as_bytes() == CURSOR_REF.as_bytes()
}

pub(crate) fn ref_chain_reaches_queue(repo: &gix::Repository, name: &FullNameRef) -> Result<bool> {
    let mut name = name.to_owned();
    let mut seen = HashSet::new();
    loop {
        if is_queue_ref(name.as_bstr()) {
            return Ok(true);
        }
        ensure!(seen.insert(name.clone()), "a symbolic reference chain contains a cycle");
        let Some(reference) = repo.try_find_reference(name.as_ref())? else {
            return Ok(false);
        };
        let target = reference.target();
        let Some(next) = target.try_name() else {
            return Ok(false);
        };
        name = next.to_owned();
    }
}

pub(crate) fn is_queue_commit(repo: &gix::Repository, needle: ObjectId) -> Result<bool> {
    let Some(mut id) = read_queue_ref(repo, TIP_REF)?.or(read_queue_ref(repo, CURSOR_REF)?) else {
        return Ok(false);
    };
    let mut seen = HashSet::new();
    loop {
        ensure!(seen.insert(id), "the undo queue first-parent chain contains a cycle");

View on GitHub (pinned to e73179060b)