{"record":{"id":"6dd9fbacbf04327b","repo":"gitbutlerapp/gitbutler","slug":"squash-step-must-have-at-least-two-commits","errorCode":null,"errorMessage":"Squash step must have at least two commits","messagePattern":"Squash step must have at least two commits","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-workspace/src/branch/integrate_branch_upstream/plan.rs","lineNumber":257,"sourceCode":"            bail_precondition!(\n                \"Integration plan is invalid: prepared commit {commit_id} appears more than once\"\n            );\n        }\n        prepared.push(step);\n    }\n\n    Ok(prepared)\n}\n\n/// Precompute the squash payload from the current editor/repository state,\n/// before later integration graph mutations can rewire step-graph ancestry.\nfn prepare_squash_step_for_editor<M: RefMetadata>(\n    editor: &Editor<'_, '_, M>,\n    commit_ids: &[gix::ObjectId],\n    message: Option<&str>,\n) -> Result<gix::ObjectId> {\n    if commit_ids.len() < 2 {\n        bail!(\"Squash step must have at least two commits\");\n    }\n\n    let maybe_selectors = commit_ids\n        .iter()\n        .map(|commit_id| editor.try_select_commit(*commit_id))\n        .collect::<Vec<_>>();\n    let ordered_commit_ids = if maybe_selectors.iter().all(Option::is_some) {\n        let ordered_selectors = editor.order_commit_selectors_by_parentage(\n            maybe_selectors\n                .into_iter()\n                .map(|selector| selector.expect(\"checked all selectors are present\"))\n                .collect::<Vec<_>>(),\n        )?;\n        ordered_selectors\n            .iter()\n            .map(|selector| {\n                editor\n                    .find_selectable_commit(*selector)","sourceCodeStart":239,"sourceCodeEnd":275,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-workspace/src/branch/integrate_branch_upstream/plan.rs#L239-L275","documentation":"Defensive precondition in `prepare_squash_step_for_editor` (plan.rs), hit when `integrate_branch_with_steps` processes an `InteractiveIntegrationStep::Squash` whose `commits` vec has fewer than two entries. The script parser already rejects single-commit squashes (parsing.rs), so this bail catches callers that build `InteractiveIntegration` programmatically and bypass parsing.","triggerScenarios":"Constructing `InteractiveIntegration { steps: vec![InteractiveIntegrationStep::Squash { commits: vec![one_id], message: None }], .. }` and passing it to `integrate_branch_with_steps`. Also reachable by deserializing an integration plan (SDK/IPC) that contains a degenerate squash step.","commonSituations":"SDK/CLI callers composing steps by hand; filtering logic that drops commits from squash groups but keeps the step; a stale plan from a previous session where a group lost members.","solutions":["Validate/normalize steps before calling: replace any squash with <2 commits by a `Pick` of its single commit, or drop empty ones.","Prefer round-tripping through `render_integration_steps_script` + `parse_integration_steps_script` to get the same validation the editor path enforces.","Re-derive the plan with `initial_integration_steps` from the current divergence instead of reusing an old one."],"exampleFix":"// before\nlet steps = vec![InteractiveIntegrationStep::Squash { commits: vec![only_commit], message: None }];\nintegrate_branch_with_steps(&ref, integration, ...)?;\n\n// after\nlet steps = vec![InteractiveIntegrationStep::Pick { commit_id: only_commit }];\nintegrate_branch_with_steps(&ref, integration, ...)?;","handlingStrategy":"validation","validationCode":"// Normalize programmatically built steps before integrate_branch_with_steps:\nfn normalize(steps: Vec<InteractiveIntegrationStep>) -> Vec<InteractiveIntegrationStep> {\n    steps.into_iter()\n        .filter_map(|s| match s {\n            InteractiveIntegrationStep::Squash { commits, message } if commits.len() < 2 => {\n                commits.first().map(|id| InteractiveIntegrationStep::Pick { commit_id: *id })\n            }\n            InteractiveIntegrationStep::Squash { .. } if false => None,\n            other => Some(other),\n        })\n        .collect()\n}","typeGuard":"fn squash_steps_are_valid(steps: &[InteractiveIntegrationStep]) -> bool {\n    steps.iter().all(|s| !matches!(s, InteractiveIntegrationStep::Squash { commits, .. } if commits.len() < 2))\n}","tryCatchPattern":"if let Err(err) = integrate_branch_with_steps(&ref_name, integration, workspace, meta, repo, db) {\n    if err.to_string().contains(\"Squash step must have at least two commits\") {\n        // fix the plan construction site, not here: this only fires for programmatic plans\n    }\n}","preventionTips":["Never construct Squash steps with <2 commits; collapse them to Pick at build time.","Round-trip programmatic plans through render+parse to reuse the parser's validation.","Re-derive plans from the current divergence instead of reusing stale ones."],"tags":["rust","git","but-workspace","integration","squash","precondition"],"backgroundTag":"squash-insufficient-commits","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}