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

  1. Remove or reword the duplicate line so each commit appears once
  2. Squash a different commit into the step instead of one already picked
  3. 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

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


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)