gitbutlerapp/gitbutler · error

Picked commit already exists in a previous step

Error message

Picked commit already exists in a previous step

What it means

The same validator rejects a rebase plan in which the same commit appears in more than one step. Since `assure_unique_step_and_existing_non_base` runs for both Pick and Fixup/Squash steps, adding any commit twice makes the second occurrence fail with "Picked commit already exists in a previous step". Rebasing the same commit twice is not meaningful, so the plan is rejected up front.

Solutions

  1. Deduplicate the step list by commit_id before constructing/validating the rebase plan.
  2. When a commit is both picked and fixup'd into another, use one step with `new_message` instead of two steps.
  3. Reset or rebuild the plan from scratch when re-submitting after an edit, rather than appending to an existing steps vector.

Example fix

// before
steps.push(pick(commit_a));
steps.push(fixup(commit_a)); // duplicate -> error
// after
steps.push(pick_with_message(commit_a, "new message"));
Defensive patterns

Strategy: validation

Validate before calling

fn has_unique_commits(steps: &[RebaseStep]) -> bool {
    let mut ids: Vec<_> = steps.iter().filter_map(|s| s.commit_id().map(|c| c.to_string())).collect();
    ids.sort();
    ids.dedup();
    // dedup shrank the vec only if duplicates existed
    ids.len() == steps.len()
}

Type guard

fn is_first_occurrence(step: &RebaseStep, prior: &[RebaseStep]) -> bool {
    !prior.iter().any(|s| s.commit_id() == step.commit_id())
}

Try / catch

match request.validate_step(&step) {
    Err(e) if e.to_string().contains("already exists in a previous step") => {
        // remove the duplicate step, optionally merge message changes into the existing one
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `validate_step` (through plan building) with a `RebaseStep::Pick` or `RebaseStep::SquashIntoPreceding` whose `commit_id` already appears in an earlier step in `self.steps` (`steps.iter().any(|s| s.commit_id() == Some(commit_id))`).

Common situations: UI drag-and-drop that duplicates a commit entry when reordering; merging two step lists that both reference the same commit; appending a fixup for a commit that is also being picked; loading a stale plan and re-adding its commits.

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 gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/6f8b02363ad12f84. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-rebase/src/lib.rs:215

                        "Reference step must have a non-empty virtual branch name"
                    ));
                }
            }
        }
        Ok(())
    }

    fn assure_unique_step_and_existing_non_base(
        &self,
        commit_id: &gix::oid,
        kind: &str,
    ) -> Result<()> {
        self.repo.find_commit(commit_id)?;
        if Some(commit_id) == self.base.as_deref() {
            bail!("{kind} commit cannot be the base commit");
        }
        if self.steps.iter().any(|s| s.commit_id() == Some(commit_id)) {
            bail!("Picked commit already exists in a previous step");
        }
        Ok(())
    }
}

#[instrument(level = "debug", skip(repo))]
fn rebase(
    repo: &gix::Repository,
    base: Option<gix::ObjectId>,
    base_substitute: Option<gix::ObjectId>,
    steps: Vec<RebaseStep>,
    pick_mode: PickMode,
) -> Result<RebaseOutput> {
    let (mut references, mut commit_mapping) = (
        vec![],
        Vec::<(Option<gix::ObjectId>, gix::ObjectId, gix::ObjectId)>::new(),
    );
    let (mut cursor, mut last_seen_commit) = (base, base);

View on GitHub (pinned to 58e5313667)