gitbutlerapp/gitbutler · error

Fixup commit must not come after a reference step

Error message

Fixup commit must not come after a reference step

What it means

Rebase plans are validated step by step. A SquashIntoPreceding (fixup) step squashes its commit into the immediately preceding step, so the preceding step must be a commit-bearing pick. If the last recorded step is a Reference step, there is no preceding pick to squash into, and validation rejects the plan.

Solutions

  1. Reorder so the fixup follows the pick it should squash into, not a reference.
  2. Insert the reference after the fixup step instead of before it.
  3. Drop the reference step if it is not needed at that position.

Example fix

// before
rebase.reference("my-branch")?;
rebase.squash_into_preceding(commit_id, None)?; // fails
// after
rebase.squash_into_preceding(commit_id, None)?;
rebase.reference("my-branch")?;
Defensive patterns

Strategy: validation

Validate before calling

fn can_squash(steps: &[RebaseStep]) -> bool {
    matches!(steps.last(), Some(RebaseStep::Pick(_)))
}

Type guard

if !matches!(rebase.last_step(), Some(RebaseStep::Pick(_))) { return Err(anyhow!("fixup requires a preceding pick")); }

Prevention

When it happens

Trigger: Calling a step-adding API (e.g. squash_into_preceding) when self.steps.last() is Some(RebaseStep::Reference(..)) — i.e. a reference was registered immediately before the fixup.

Common situations: Building plans programmatically where reference steps and fixups are interleaved; UI-generated plans that place a reference right before a squash; reordering operations that moved a reference after the target commit.

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/1098767c832a7398. Report an issue: GitHub.

Appendix: source

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

    ///
    /// Fixup operations:
    /// - Must not be a reference step immediately before it
    /// - Must not be the first operation
    ///
    /// Reference operations:
    /// - The refname must be a valid reference name
    fn validate_step(&self, step: &RebaseStep) -> Result<()> {
        match step {
            RebaseStep::Pick { commit_id, .. } => {
                self.assure_unique_step_and_existing_non_base(commit_id, "Picked")?;
            }
            RebaseStep::SquashIntoPreceding {
                commit_id,
                new_message: _,
            } => {
                self.assure_unique_step_and_existing_non_base(commit_id, "Fixup")?;
                if matches!(self.steps.last(), Some(RebaseStep::Reference { .. })) {
                    bail!("Fixup commit must not come after a reference step");
                }
                if self.steps.is_empty() {
                    bail!("Fixup must have a commit to work on");
                }
            }
            RebaseStep::Reference(name) => {
                if matches!(name, but_core::Reference::Virtual(name) if name.is_empty()) {
                    return Err(anyhow!(
                        "Reference step must have a non-empty virtual branch name"
                    ));
                }
            }
        }
        Ok(())
    }

    fn assure_unique_step_and_existing_non_base(
        &self,

View on GitHub (pinned to 58e5313667)