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
- 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.
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
- 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.
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
- Squash step must have at least two commits
- line {line_number}: pick does not accept a message clause
- line {line_number}: pick requires exactly one commit
- line {line_number}: merge does not accept a message clause
- line {line_number}: merge requires exactly one commit
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/66fca2fb0a546164.
Report an issue: GitHub.