GitoxideLabs/gitoxide · error

the undo queue first-parent chain contains a cycle

Error message

the undo queue first-parent chain contains a cycle

What it means

`is_queue_commit` follows the first-parent chain of the undo queue tip (or cursor) commit to decide whether a given object id is part of the queue. A `HashSet` of visited ids enforces termination; if a commit id repeats, the first-parent chain is cyclic, which valid commit graphs cannot produce, so this error is raised.

Solutions

  1. Verify the queue commit graph with `git log --first-parent <queue-tip>` and `git fsck`.
  2. Rebuild or discard the undo queue ref if its history is corrupt.
  3. If constructing queue commits in code, ensure each new commit's parent is a previously unvisited (strictly older) commit.

Example fix

// before: linking a commit back into the queue, creating a loop
let new = commit(parent_id /* an ancestor that loops */);
// after: parent must be the previous tip, advancing the chain
let new = commit(queue_tip_id);
Defensive patterns

Strategy: validation

Validate before calling

// confirm the queue tip's first-parent history is linear before planning undo
let out = std::process::Command::new("git")
    .args(["rev-list", "--first-parent", tip_id.to_string().as_str()])
    .output()?;
// duplicate ids in output => cyclic/corrupt chain, do not call undo APIs

Prevention

When it happens

Trigger: Undo planning via `is_queue_commit` when the undo-queue tip commit's first-parent ancestry loops back on itself, e.g. commit A's first-parent chain reaches A again via corrupted or forged commit objects.

Common situations: Object store corruption or hand-crafted commit objects; a buggy external tool rewrote history into a loop; tests injecting deliberately cyclic commits.

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/9230f1aa6b683612. Report an issue: GitHub.

Appendix: source

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

        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");
        let Ok(stored) = parse_commit(repo, id) else {
            return Ok(false);
        };
        if id == needle {
            return Ok(true);
        }
        let Some(parent) = stored.parent else {
            return Ok(false);
        };
        id = parent;
    }
}

pub(crate) fn review_blocks_undo(repo: &gix::Repository) -> Result<bool> {
    let references = repo.references().context("could not open review references")?;
    for reference in references
        .prefixed(crate::history::REVIEW_PREFIX.as_bstr())
        .context("could not iterate review references")?

View on GitHub (pinned to e73179060b)