gitbutlerapp/gitbutler · error · anyhow::Error

Integration steps cannot be empty

Error message

Integration steps cannot be empty

What it means

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.

Source

Thrown at crates/but-workspace/src/branch/integrate_branch_upstream/mod.rs:123

    pub divergence: IntegrationDivergenceDisplay,
}

/// Integrate the upstream changes in the order of the provided steps.
///
/// `ref_name` - The full reference name of the local branch we're integrating the upstream changes into.
///
/// `steps` - The vector of steps in the application order (parent to child) that describe the actions to perform
///   for the integration of the changes.
pub fn integrate_branch_with_steps<'ws, 'meta, M: RefMetadata>(
    ref_name: &gix::refs::FullNameRef,
    integration: InteractiveIntegration,
    workspace: &'ws mut but_graph::Workspace,
    meta: &'meta mut M,
    repo: &gix::Repository,
    db: &'meta mut but_db::DbHandle,
) -> Result<SuccessfulRebase<'ws, 'meta, M>> {
    if integration.steps.is_empty() {
        bail!("Integration steps cannot be empty")
    }
    // The editor maps every segment in the graph, including the remote
    // reference of the branch we're integrating.
    let mut editor = Editor::create(workspace, meta, repo, db)?;
    // Step 1: We prepare the steps before building.
    // At this point, we construct the commits for the squash steps in memory.
    let prepared_steps = prepare_integration_steps_for_editor(&editor, &integration.steps)?;

    let delimiter_child = editor.select_reference(ref_name)?;
    let delimiter_parent = match integration.first_local_not_integrated {
        Some(commit_id) => {
            let selector = find_local_commit_until_merge_base(
                ref_name,
                commit_id,
                integration.merge_base,
                &editor,
            )?;
            let Some(selector) = selector else {

View on GitHub (pinned to caf1f223d3)

Solutions

  1. 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.
  2. If building steps programmatically, seed from `initial_integration_steps` (the same proposal the product uses) and mutate from there rather than constructing from scratch.
  3. 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.

Example fix

// before
let steps = parse_integration_steps_script(&script, &divergence)?;
let outcome = integrate_branch_with_steps(&ref_name, InteractiveIntegration { steps, merge_base, first_local_not_integrated }, workspace, meta, repo, db)?;

// after
let steps = parse_integration_steps_script(&script, &divergence)?;
if steps.is_empty() {
    return Ok(None); // user emptied the script: nothing to integrate
}
let outcome = integrate_branch_with_steps(&ref_name, InteractiveIntegration { steps, merge_base, first_local_not_integrated }, workspace, meta, repo, db)?;
Defensive patterns

Strategy: validation

Validate before calling

// Before calling integrate_branch_with_steps:
if integration.steps.is_empty() {
    anyhow::bail!("refusing to integrate: plan has no steps (user likely emptied the script)");
}
// or treat as cancel:
// if integration.steps.is_empty() { return Ok(None); }

Try / catch

let result = integrate_branch_with_steps(&ref_name, integration, workspace, meta, repo, db);
if let Err(err) = &result {
    if err.to_string().contains("Integration steps cannot be empty") {
        // map to a user-facing 'nothing to integrate' message or no-op
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/66fca2fb0a546164. Report an issue: GitHub.