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
- Find the cycle with `git for-each-ref --format='%(refname) %(symref)'` and inspect the offending symrefs.
- 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`).
- 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
- Never create a symbolic ref whose target chain can loop back to itself.
- Audit hand-written symrefs with `git for-each-ref --format='%(refname) %(symref)'`.
- Use library APIs (which validate targets) instead of raw file edits for symrefs.
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
- a projected worktree reference contains a symbolic cycle
- Could not follow all splits after
- could not inspect a reference before editing
- could not read review reference
- the undo queue first-parent chain contains a cycle
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)