GitoxideLabs/gitoxide · error

a projected worktree reference contains a symbolic cycle

Error message

a projected worktree reference contains a symbolic cycle

What it means

`resolve_state` resolves the projected post-undo/redo state of a reference by following symbolic chains within the projected state. To guarantee termination it tracks visited full names; if the projected symbolic chain revisits a name, the projected state itself is cyclic — an unresolvable invariant violation — so this error is raised.

Solutions

  1. Inspect the queue commit's `[undo]` config metadata to find the changes forming the cycle.
  2. Break the cycle by pointing one of the refs at an object target before undoing (repair the live repo state so the projection is acyclic).
  3. Recreate the undo queue (discard and re-record) if its metadata is corrupt.

Example fix

// before: batch of changes that makes refs cyclic
changes = [ref_a -> Symbolic(ref_b), ref_b -> Symbolic(ref_a)];
// after: at least one ref must resolve to an object
changes = [ref_a -> Object(commit_id), ref_b -> Symbolic(ref_a)];
Defensive patterns

Strategy: validation

Validate before calling

// simulate the projected symref graph and detect cycles before applying changes
fn projected_cycle(changes: &[RefChange]) -> bool {
    let mut seen = std::collections::HashSet::new();
    for c in changes {
        if let State::Symbolic(t) = &c.after {
            if !seen.insert(t.clone()) { return true; }
        }
    }
    false
}

Prevention

When it happens

Trigger: Undo/redo (`apply_with_worktrees` -> `worktree_transitions` -> `resolve_state`, or nested `resolve_state` recursion) when the applied ref changes would make symbolic refs point at each other in a loop (e.g. change A to symref->B and B to symref->A).

Common situations: Undoing a batch that swapped two symbolic refs; hand-edited or corrupted queue metadata whose recorded changes describe cyclic symref states.

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/55197cedf4f992c3. Report an issue: GitHub.

Appendix: source

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

    if name == b"HEAD" {
        return Ok(fallback.clone());
    }
    let name = FullName::try_from(name).context("a projected worktree reference name is invalid")?;
    state(repo, name.as_ref())
}

fn resolve_state(
    repo: &gix::Repository,
    state: &State,
    changes: &[RefChange],
    include_head: bool,
    seen: &mut HashSet<FullName>,
) -> Result<Option<ObjectId>> {
    match state {
        State::Missing => Ok(None),
        State::Object(id) => Ok(Some(*id)),
        State::Symbolic(name) => {
            ensure!(
                seen.insert(name.clone()),
                "a projected worktree reference contains a symbolic cycle"
            );
            let next = projected_ref_state(repo, name.as_bstr(), &State::Missing, changes, include_head)?;
            resolve_state(repo, &next, changes, include_head, seen)
        }
    }
}

fn tree_id(repo: &gix::Repository, commit: Option<ObjectId>) -> Result<ObjectId> {
    commit.map_or_else(
        || Ok(ObjectId::empty_tree(repo.object_hash())),
        |commit| {
            repo.find_commit(commit)
                .context("a worktree HEAD target is not a commit")?
                .tree_id()
                .context("could not decode a worktree HEAD commit")
                .map(gix::Id::detach)

View on GitHub (pinned to e73179060b)