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
- Remove duplicate `tip <oid>` lines so every tip is unique.
- Regenerate the state file by restarting the operation.
- 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
- Deduplicate tips when constructing the state, e.g. via a HashSet at write time.
- On retry after a crash, re-derive tips from refs rather than re-appending.
- Add a round-trip test that writes and re-parses generated state.
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
- the rebase state contains duplicate scope commits
- the rebase state contains duplicate refs
- the root of a repeated rebase must be pending
- the parent of a repeated rebase must not be pending
- the current checkout has a pending rebase; time-travel to…
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)