{"record":{"id":"66fca2fb0a546164","repo":"gitbutlerapp/gitbutler","slug":"integration-steps-cannot-be-empty","errorCode":null,"errorMessage":"Integration steps cannot be empty","messagePattern":"Integration steps cannot be empty","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-workspace/src/branch/integrate_branch_upstream/mod.rs","lineNumber":123,"sourceCode":"    pub divergence: IntegrationDivergenceDisplay,\n}\n\n/// Integrate the upstream changes in the order of the provided steps.\n///\n/// `ref_name` - The full reference name of the local branch we're integrating the upstream changes into.\n///\n/// `steps` - The vector of steps in the application order (parent to child) that describe the actions to perform\n///   for the integration of the changes.\npub fn integrate_branch_with_steps<'ws, 'meta, M: RefMetadata>(\n    ref_name: &gix::refs::FullNameRef,\n    integration: InteractiveIntegration,\n    workspace: &'ws mut but_graph::Workspace,\n    meta: &'meta mut M,\n    repo: &gix::Repository,\n    db: &'meta mut but_db::DbHandle,\n) -> Result<SuccessfulRebase<'ws, 'meta, M>> {\n    if integration.steps.is_empty() {\n        bail!(\"Integration steps cannot be empty\")\n    }\n    // The editor maps every segment in the graph, including the remote\n    // reference of the branch we're integrating.\n    let mut editor = Editor::create(workspace, meta, repo, db)?;\n    // Step 1: We prepare the steps before building.\n    // At this point, we construct the commits for the squash steps in memory.\n    let prepared_steps = prepare_integration_steps_for_editor(&editor, &integration.steps)?;\n\n    let delimiter_child = editor.select_reference(ref_name)?;\n    let delimiter_parent = match integration.first_local_not_integrated {\n        Some(commit_id) => {\n            let selector = find_local_commit_until_merge_base(\n                ref_name,\n                commit_id,\n                integration.merge_base,\n                &editor,\n            )?;\n            let Some(selector) = selector else {","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-workspace/src/branch/integrate_branch_upstream/mod.rs#L105-L141","documentation":"Thrown by `integrate_branch_with_steps` in but-workspace when the `InteractiveIntegration.steps` vector is empty. The function drives an upstream integration (picks/merges/squashes) through a but_rebase graph Editor, and an empty plan leaves it nothing to apply, so it refuses up front instead of performing a no-op rebase. It mirrors the shape of the initial proposal produced by the `InitialBranchIntegration` API, which always contains at least one step.","triggerScenarios":"Calling `but_workspace::branch::integrate_branch_upstream::integrate_branch_with_steps(ref_name, InteractiveIntegration { steps: vec![], merge_base, first_local_not_integrated }, ...)` with an empty steps vec — typically when a UI/SDK round-trips a user-edited integration script that came back empty or all-comment, and the caller forwards the parsed (empty) result without checking.","commonSituations":"A user deletes every `pick`/`merge`/`squash` line in the interactive integration editor and confirms; the parsed script yields zero steps and the caller passes them straight through. Also happens when a caller builds `InteractiveIntegration` manually and forgets to populate steps, or filters out all steps (e.g. resolving commits) before applying.","solutions":["Before calling, treat an empty parsed result as 'abort integration' in the UI/SDK layer instead of forwarding it: if `parse_integration_steps_script(...)` returns an empty Vec, return Ok(None)/cancel.","If building steps programmatically, seed from `initial_integration_steps` (the same proposal the product uses) and mutate from there rather than constructing from scratch.","If an empty plan genuinely means 'integrate nothing / already up to date', detect the no-divergence case via the divergence display and skip the integrate call entirely."],"exampleFix":"// before\nlet steps = parse_integration_steps_script(&script, &divergence)?;\nlet outcome = integrate_branch_with_steps(&ref_name, InteractiveIntegration { steps, merge_base, first_local_not_integrated }, workspace, meta, repo, db)?;\n\n// after\nlet steps = parse_integration_steps_script(&script, &divergence)?;\nif steps.is_empty() {\n    return Ok(None); // user emptied the script: nothing to integrate\n}\nlet outcome = integrate_branch_with_steps(&ref_name, InteractiveIntegration { steps, merge_base, first_local_not_integrated }, workspace, meta, repo, db)?;","handlingStrategy":"validation","validationCode":"// Before calling integrate_branch_with_steps:\nif integration.steps.is_empty() {\n    anyhow::bail!(\"refusing to integrate: plan has no steps (user likely emptied the script)\");\n}\n// or treat as cancel:\n// if integration.steps.is_empty() { return Ok(None); }","typeGuard":null,"tryCatchPattern":"let result = integrate_branch_with_steps(&ref_name, integration, workspace, meta, repo, db);\nif let Err(err) = &result {\n    if err.to_string().contains(\"Integration steps cannot be empty\") {\n        // map to a user-facing 'nothing to integrate' message or no-op\n    }\n}","preventionTips":["Never forward a parsed script result without checking `!steps.is_empty()`; empty means the user aborted.","Build plans from `initial_integration_steps` so they are never born empty.","Keep the divergence check upstream: no divergence means skip the integrate call entirely."],"tags":["rust","git","but-workspace","integration","precondition","empty-input"],"backgroundTag":"empty-plan-validation","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}