GitoxideLabs/gitoxide · error
a commit is picked more than once
Error message
a commit is picked more than once
What it means
Thrown when the same commit would be applied more than once by the plan, either by a `pick` line or by a `squash` line referencing it. Duplicate application would create ambiguous or duplicated history, so the parser tracks a `picked` map of commit ID to step index and rejects repeats.
Solutions
- Remove or reword the duplicate line so each commit appears once
- Squash a different commit into the step instead of one already picked
- De-duplicate the todo text before calling the parser
Example fix
// before pick abc123 squash abc123 # duplicate // after pick abc123
Defensive patterns
Strategy: validation
Validate before calling
let mut seen = HashSet::new();
for line in todo.lines() {
if let Some(id) = line.split_whitespace().nth(1) {
if !seen.insert(id.to_owned()) { anyhow::bail!("commit {id} used more than once"); }
}
} Type guard
fn is_unique_pick(id: &str, picked: &HashSet<String>) -> bool {
!picked.contains(id)
} Try / catch
match parse_plan(...) {
Err(e) if e.to_string().contains("picked more than once") => { /* de-duplicate the todo */ }
other => other?,
} Prevention
- De-duplicate commit IDs across pick and squash lines before parsing
- Never squash a commit that is also picked elsewhere
- Track applied commit IDs when generating todos in scripts
When it happens
Trigger: `parse_plan` sees two commands referencing the same commit ID — e.g. a `pick abc123` followed by `squash abc123`, or the same commit picked twice.
Common situations: Copy-pasting todo lines; generated todos that include a commit both as a pick and as a squash source; accidentally re-adding a commit after an edit.
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
- the rebase todo contains more than one @ command
- unsupported rebase todo command
- rebase todo requires at least one -x/--hide revision when…
- the hidden and visible revisions have no editable fork point
- the revisions have multiple editable fork points
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/5bf1e16d23c54438.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/todo.rs:944
if marked {
if std::mem::replace(&mut command_marker, true) {
anyhow::bail!("the rebase todo contains more than one @ command");
}
if !state.checkout_allowed || repo.workdir().is_none() {
anyhow::bail!("the rebase todo cannot select a checkout without a worktree");
}
}
if verb == "squash" {
let index = section_last_step.context("a squash must follow a command in the same fork")?;
let id = resolve_commit(
repo,
value.split_whitespace().next().context("a squash needs a commit ID")?,
)?;
if !scope.contains(&id) {
anyhow::bail!("a squash is outside the editable history");
}
if picked.insert(id, index).is_some() {
anyhow::bail!("a commit is picked more than once");
}
steps[index].squash.push(id);
if marked {
let target = rebase::PlanParent::Step(index);
if checkout_target.is_some_and(|checkout| checkout != target) {
anyhow::bail!("the @ command and @ reference point to different results");
}
checkout_target = Some(target);
}
section_has_commit = true;
continue;
}
let parent = cursor.context("the first todo command must follow a fork heading")?;
let commit = match verb {
"pick" => {
let value = value.split_whitespace().next().context("a pick needs a commit ID")?;
let resolved_id = state.resolved;
let full_null = resolved_id.is_some_and(|id| {View on GitHub (pinned to e73179060b)