GitoxideLabs/gitoxide · error

the rebase state contains duplicate tips

Error message

the rebase state contains duplicate tips

What it means

`validate_state` additionally checks the `tips` list for duplicates by collecting the OIDs into a temporary `HashSet` and comparing lengths. Repeated tip OIDs make the multi-tip rebase state ambiguous (the same tip would be processed twice), so the state is rejected.

Solutions

  1. Remove duplicate `tip <oid>` lines so every tip is unique.
  2. Regenerate the state file by restarting the operation.
  3. Deduplicate tips in any tooling that constructs the state file.

Example fix

// before (state file)
tip 0123abc...
tip 0123abc...
// after
tip 0123abc...
Defensive patterns

Strategy: validation

Validate before calling

fn tip_lines_are_unique(text: &str) -> bool {
    let ids: Vec<&str> = text.lines()
        .filter_map(|l| l.strip_prefix("tip "))
        .collect();
    ids.iter().collect::<std::collections::HashSet<_>>().len() == ids.len()
}

Try / catch

match todo::parse(state_text) {
    Ok(state) => apply(state),
    Err(e) if e.to_string().contains("duplicate tips") => {
        eprintln!("deduplicate tip lines before parsing");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: `parse_state` (via validate_state, gix-tix/src/edit/todo.rs:785) parses a state file where the same OID appears on two or more `tip <oid>` lines.

Common situations: Hand-edited tip lists; automation appending the same tip on retry; state files merged from concurrent worktree 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/349053f5a4657912. Report an issue: GitHub.

Appendix: source

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

        resolved,
        continuation_sources,
    };
    validate_state(repo, &state)?;
    Ok(Some(state))
}

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");
    }

View on GitHub (pinned to e73179060b)