GitoxideLabs/gitoxide · error
a rebase plan contains an invalid or duplicate pick
Error message
a rebase plan contains an invalid or duplicate pick
What it means
The rebase-plan validator requires every pick/squash id to (a) belong to the plan's declared scope and (b) appear at most once across all steps. A duplicate pick or an id outside the scope would produce an inconsistent rewrite, so the plan is rejected.
Solutions
- Deduplicate commit ids across all plan steps before submitting
- Ensure every picked id is included in `plan.scope`
- Regenerate the plan from the current history instead of replaying a stale one
Example fix
// before
steps.push(pick(a)); steps.push(pick(a)); // duplicate
// after
let mut seen = HashSet::new();
if seen.insert(a) { steps.push(pick(a)); } Defensive patterns
Strategy: validation
Validate before calling
fn validate_picks(scope: &HashSet<ObjectId>, picks: &[ObjectId]) -> bool {
let mut seen = HashSet::new();
picks.iter().all(|p| scope.contains(p) && seen.insert(*p))
} Try / catch
match result {
Err(e) if e.to_string().contains("invalid or duplicate pick") => {
eprintln!("deduplicate picks and ensure all are in plan scope");
}
r => r?,
} Prevention
- Deduplicate ids across all steps before submitting a plan
- Regenerate plans after history changes instead of replaying stale ones
- Assert scope coverage in plan builders
When it happens
Trigger: A plan whose steps reference commit ids not in `plan.scope`; the same commit id picked twice (e.g. once as Pick and once inside a squash list); tooling that appends steps without deduplicating ids.
Common situations: Auto-generated plans that repeat a commit because it appears in two ranges; stale plans replayed after history changed so ids fell out of scope; hand-edited todo files with duplicated lines.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- moving the stack would create a commit cycle
- merge commits cannot be picked by the rebase editor
- the checkout ancestry contains a cycle
- a commit is picked more than once
- rebase todo requires at least one -x/--hide revision when…
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/8f1068d4c88a3b58.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/rebase.rs:1652
.to_owned()
.context("could not own the Git committer")?;
repo = repo.with_object_memory();
let scope: HashSet<_> = plan.scope.iter().copied().collect();
let mut picked = HashSet::new();
for step in &plan.steps {
if let PlanCommit::Copy(id) = step.commit
&& graph.parents_of(id).context("a copied commit is incomplete")?.len() != 1
{
anyhow::bail!("copying a commit requires it to have exactly one parent");
}
let ids = match step.commit {
PlanCommit::Pick(id) | PlanCommit::Resolved(id) => Some(id).into_iter().chain(step.squash.iter().copied()),
PlanCommit::Copy(_) | PlanCommit::Empty(_) => None.into_iter().chain(step.squash.iter().copied()),
};
for id in ids {
if !scope.contains(&id) || !picked.insert(id) {
anyhow::bail!("a rebase plan contains an invalid or duplicate pick");
}
if graph.parents_of(id).context("a picked commit is incomplete")?.len() > 1 {
anyhow::bail!("merge commits cannot be picked by the rebase editor");
}
}
}
let checkout_target = if repo.workdir().is_some() {
plan.checkout
.as_ref()
.map(|checkout| checkout.target)
.or(infer_plan_checkout(&repo, graph, &plan)?)
} else {
None
};
if plan.checkout.is_none()
&& checkout_target.is_some()
&& !planView on GitHub (pinned to e73179060b)