{"record":{"id":"4b58dd3fd3df2d89","repo":"gitbutlerapp/gitbutler","slug":"can-t-squash-if-previous-commit-is-missing","errorCode":null,"errorMessage":"Can't squash if previous commit is missing","messagePattern":"Can't squash if previous commit is missing","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-rebase/src/lib.rs","lineNumber":317,"sourceCode":"                                SignCommit::IfSignCommitsEnabled,\n                                None,\n                            )?);\n                        }\n                        None => {\n                            // TODO: should this be supported? This would be as easy as forgetting its parents.\n                            bail!(\n                                \"Cannot currently rebase a commit so that it becomes the first commit in the history\"\n                            )\n                        }\n                    }\n                }\n            }\n            RebaseStep::SquashIntoPreceding {\n                commit_id,\n                new_message,\n            } => {\n                let Some(cursor) = &mut cursor else {\n                    bail!(\"Can't squash if previous commit is missing\");\n                };\n                last_seen_commit = Some(commit_id);\n                let base_commit = repo.find_commit(*cursor)?;\n                let new_commit = cherry_pick_one(\n                    repo,\n                    *cursor,\n                    commit_id,\n                    PickMode::Unconditionally,\n                    EmptyCommit::Keep,\n                )?;\n\n                // Now, lets pretend the base didn't exist by swapping parent with the parent of the base\n                let mut new_commit = repo.find_commit(new_commit)?.decode()?.to_owned()?;\n                new_commit.parents = base_commit.parent_ids().map(|id| id.detach()).collect();\n                if let Some(new_message) = new_message {\n                    new_commit.message =\n                        but_core::commit::rewrite_conflict_markers_on_message_change(\n                            new_commit.message.as_ref(),","sourceCodeStart":299,"sourceCodeEnd":335,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-rebase/src/lib.rs#L299-L335","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Ensure at least one Pick (or a valid base) precedes every SquashIntoPreceding step.","Run `validate_step` on each step when building the plan so this shape error is caught before execution.","Convert a leading squash step into a plain Pick with the desired message instead."],"exampleFix":"// before\nsteps = [squash_into_preceding(c1), pick(c2)]; // no preceding commit\n// after\nsteps = [pick(c2), squash_into_preceding(c1)];","handlingStrategy":"validation","validationCode":"fn squash_steps_have_preceding_pick(steps: &[RebaseStep]) -> bool {\n    let mut saw_pick = false;\n    for s in steps {\n        match s {\n            RebaseStep::Pick { .. } => saw_pick = true,\n            RebaseStep::SquashIntoPreceding { .. } if !saw_pick => return false,\n            _ => {}\n        }\n    }\n    true\n}","typeGuard":"fn can_squash(base: Option<gix::ObjectId>, prior_steps: &[RebaseStep]) -> bool {\n    base.is_some() || prior_steps.iter().any(|s| matches!(s, RebaseStep::Pick { .. }))\n}","tryCatchPattern":"match rebase(repo, base, base_substitute, steps, pick_mode) {\n    Err(e) if e.to_string().contains(\"previous commit is missing\") => {\n        // insert a Pick step before the squash, or convert the squash into a Pick\n    }\n    other => other,\n}","preventionTips":["Keep every SquashIntoPreceding step after at least one Pick step.","Run validate_step during plan construction to catch this before execution.","Guard UI reorder logic so fixup/squash rows cannot be moved above the first pick."],"tags":["rebase","git-history","plan-validation"],"backgroundTag":"invalid-state-transition","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}