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

  1. Deduplicate commit ids across all plan steps before submitting
  2. Ensure every picked id is included in `plan.scope`
  3. 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

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


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()
        && !plan

View on GitHub (pinned to e73179060b)