gitbutlerapp/gitbutler · error

Can't squash if previous commit is missing

Error message

Can't squash if previous commit is missing

What it means

A `RebaseStep::SquashIntoPreceding` step squashes its commit into the commit produced by the preceding step, tracked via `cursor`. If `cursor` is `None` when the squash step executes, there is no preceding rewritten commit to squash into, so `rebase` bails out. This is a plan-shape invariant: a squash step must follow a step that produced a commit.

Solutions

  1. Ensure at least one Pick (or a valid base) precedes every SquashIntoPreceding step.
  2. Run `validate_step` on each step when building the plan so this shape error is caught before execution.
  3. Convert a leading squash step into a plain Pick with the desired message instead.

Example fix

// before
steps = [squash_into_preceding(c1), pick(c2)]; // no preceding commit
// after
steps = [pick(c2), squash_into_preceding(c1)];
Defensive patterns

Strategy: validation

Validate before calling

fn squash_steps_have_preceding_pick(steps: &[RebaseStep]) -> bool {
    let mut saw_pick = false;
    for s in steps {
        match s {
            RebaseStep::Pick { .. } => saw_pick = true,
            RebaseStep::SquashIntoPreceding { .. } if !saw_pick => return false,
            _ => {}
        }
    }
    true
}

Type guard

fn can_squash(base: Option<gix::ObjectId>, prior_steps: &[RebaseStep]) -> bool {
    base.is_some() || prior_steps.iter().any(|s| matches!(s, RebaseStep::Pick { .. }))
}

Try / catch

match rebase(repo, base, base_substitute, steps, pick_mode) {
    Err(e) if e.to_string().contains("previous commit is missing") => {
        // insert a Pick step before the squash, or convert the squash into a Pick
    }
    other => other,
}

Prevention

When it happens

Trigger: Executing a rebase whose step list contains `RebaseStep::SquashIntoPreceding` while `cursor` is still `None` — i.e. the squash is the first step, or all preceding steps failed to establish a cursor (crates/but-rebase/src/lib.rs:317).

Common situations: Fixup/squash-first reorderings produced by UI drag-and-drop; plan construction that puts a squash step before any Pick; note that `validate_step` guards `Fixup` steps with "Fixup must have a commit to work on", so this error typically arises from plans not routed through full validation.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/4b58dd3fd3df2d89. Report an issue: GitHub.

Appendix: source

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

                                SignCommit::IfSignCommitsEnabled,
                                None,
                            )?);
                        }
                        None => {
                            // TODO: should this be supported? This would be as easy as forgetting its parents.
                            bail!(
                                "Cannot currently rebase a commit so that it becomes the first commit in the history"
                            )
                        }
                    }
                }
            }
            RebaseStep::SquashIntoPreceding {
                commit_id,
                new_message,
            } => {
                let Some(cursor) = &mut cursor else {
                    bail!("Can't squash if previous commit is missing");
                };
                last_seen_commit = Some(commit_id);
                let base_commit = repo.find_commit(*cursor)?;
                let new_commit = cherry_pick_one(
                    repo,
                    *cursor,
                    commit_id,
                    PickMode::Unconditionally,
                    EmptyCommit::Keep,
                )?;

                // Now, lets pretend the base didn't exist by swapping parent with the parent of the base
                let mut new_commit = repo.find_commit(new_commit)?.decode()?.to_owned()?;
                new_commit.parents = base_commit.parent_ids().map(|id| id.detach()).collect();
                if let Some(new_message) = new_message {
                    new_commit.message =
                        but_core::commit::rewrite_conflict_markers_on_message_change(
                            new_commit.message.as_ref(),

View on GitHub (pinned to 58e5313667)