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
- Deduplicate the step list by commit_id before constructing/validating the rebase plan.
- When a commit is both picked and fixup'd into another, use one step with `new_message` instead of two steps.
- 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
- Deduplicate steps by commit_id when assembling or merging plans.
- Prefer representing reword + pick as a single step with new_message instead of two steps.
- Run validate_step on the full plan before execution.
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
- Cannot reconcile : branch name ' ' occurs more than once
- commit cannot be the base commit
- Aborting due to empty branch name
- Base commit must exist if provided
- Branch name ' ' collides with existing branch
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)