GitoxideLabs/gitoxide · error

the rebase state contains duplicate refs

Error message

the rebase state contains duplicate refs

What it means

`validate_state` inserts every `expected_refs[].name` into a `HashSet` of ref names; if insertion fails, two captured refs share the same fully-qualified name. Since each expected ref entry describes a distinct reference update, duplicate names are ambiguous and validation aborts.

Solutions

  1. Merge or remove duplicate `ref` lines so each fully-qualified ref name appears exactly once.
  2. Regenerate the state by restarting the rebase/edit operation.
  3. Ensure automation keys ref entries by name and updates in place rather than appending.

Example fix

// before (state file)
ref aaa fast-forward "refs/heads/wip"
ref bbb fast-forward "refs/heads/wip"
// after
ref bbb fast-forward "refs/heads/wip"
Defensive patterns

Strategy: validation

Validate before calling

fn ref_lines_are_unique(text: &str) -> bool {
    let names: Vec<&str> = text.lines()
        .filter(|l| l.starts_with("ref "))
        .map(|l| l.rsplit_once(' ').map(|(_, n)| n).unwrap_or(""))
        .collect();
    names.iter().collect::<std::collections::HashSet<_>>().len() == names.len()
}

Try / catch

match todo::parse(state_text) {
    Ok(state) => apply(state),
    Err(e) if e.to_string().contains("duplicate refs") => {
        eprintln!("merge duplicate ref entries; each ref name may appear once");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: `parse_state` (via validate_state, gix-tix/src/edit/todo.rs:790) parses a state file where two `ref` lines (or an edit-ref/expected-ref combination) produce entries with identical `FullName`s.

Common situations: Hand-edited ref lists; scripts writing one `ref` line per change instead of updating the existing entry; state files merged from divergent sessions.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at gix-tix/src/edit/todo.rs:790

}

fn validate_state(repo: &gix::Repository, state: &State) -> Result<()> {
    repo.find_commit(state.base)
        .context("could not find the recorded rebase base")?;
    repo.find_commit(state.onto)
        .context("could not find the recorded rebase target")?;
    let scope: HashSet<_> = state.scope.iter().copied().collect();
    let continuation_sources: HashSet<_> = state.continuation_sources.iter().copied().collect();
    if scope.len() != state.scope.len() {
        anyhow::bail!("the rebase state contains duplicate scope commits");
    }
    if state.tips.iter().copied().collect::<HashSet<_>>().len() != state.tips.len() {
        anyhow::bail!("the rebase state contains duplicate tips");
    }
    let mut refs = HashSet::new();
    for reference in &state.expected_refs {
        if !refs.insert(reference.name.as_bstr()) {
            anyhow::bail!("the rebase state contains duplicate refs");
        }
        if !scope.contains(&reference.target) && reference.target != state.base && reference.target != state.onto {
            anyhow::bail!("a captured ref does not logically point into the rebase scope");
        }
    }
    if let Some(name) = &state.head_ref
        && !state
            .expected_refs
            .iter()
            .any(|reference| reference.editable && reference.name == *name)
    {
        anyhow::bail!("the recorded HEAD ref is not editable");
    }
    for tip in &state.tips {
        repo.find_commit(*tip).context("could not find a recorded rebase tip")?;
    }
    for id in &state.scope {
        let commit = repo

View on GitHub (pinned to e73179060b)