gitbutlerapp/gitbutler · error

Merge strategy must only be used on stacks with one head…

Error message

Merge strategy must only be used on stacks with one head and one bottom commit

What it means

but-workspace's upstream integration supports a 'merge' bottom-update strategy for integrating upstream changes, but it can only be applied to a stack that resolves to exactly one head commit and one bottom commit. When a stack targeted with the Merge strategy has multiple heads or multiple bottoms, the library refuses rather than attempt an ambiguous merge integration.

Solutions

  1. Flatten the stack to a single head (merge or rebase its extra branches together) before integrating upstream
  2. Ensure the stack has one bottom commit by splitting off extra bases into separate stacks
  3. Use an integration strategy other than Merge (e.g. rebase-based updates) for multi-head stacks
  4. Inspect stack.heads and stack.bottoms via the workspace API to confirm the shape before triggering integration

Example fix

// before: integrating a multi-head stack with a merge update
integrate_upstream(&repo, &workspace, /* hints with Merge */ hints)?;
// after: guard the stack shape first
anyhow::ensure!(stack.heads.len() == 1 && stack.bottoms.len() == 1, "flatten stack before merge-integration");
integrate_upstream(&repo, &workspace, hints)?;
Defensive patterns

Strategy: validation

Validate before calling

if stack.heads.len() != 1 || stack.bottoms.len() != 1 {
    anyhow::bail!("flatten stack to one head/bottom before merge-strategy integration");
}

Type guard

fn is_merge_integratable(stack: &Stack) -> bool {
    stack.heads.len() == 1 && stack.bottoms.len() == 1
}

Try / catch

match integrate_upstream(&repo, &workspace, hints) {
    Err(e) if e.to_string().contains("one head and one bottom") => flatten_stack_then_retry(),
    r => r,
}

Prevention

When it happens

Trigger: Calling integrate_upstream (or integrate_upstream_with_hints) where the stack's relevant updates include BottomUpdateKind::Merge while stack.heads.len() != 1 or stack.bottoms.len() != 1. Note a sibling check first rejects multiple updates for the same stack, so this fires on the single-update-but-multi-head/multi-bottom-stack shape.

Common situations: Running integration (e.g. 'but pull'/workspace integrate) on a stack that has diverged into multiple branches/heads, or whose base was split so it has several bottom commits, while a merge-based update from the remote is pending.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/ff3e18668cf4ea52. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-workspace/src/upstream_integration.rs:300

        review_hints,
    )?;

    // Validate described updates and find commits to rebase
    for stack in &mut stacks {
        let relevant_updates = updates_with_selectors
            .iter()
            .filter(|(s, _)| stack.bottoms.contains(s))
            .collect::<Vec<_>>();

        if relevant_updates
            .iter()
            .any(|(_, kind)| *kind == BottomUpdateKind::Merge)
        {
            if relevant_updates.len() > 1 {
                bail!("Found multiple updates for a stack using the merge strategy");
            }
            if stack.heads.len() != 1 || stack.bottoms.len() != 1 {
                bail!(
                    "Merge strategy must only be used on stacks with one head and one bottom commit"
                );
            }

            stack.to_merge = true
        } else {
            // currently the only other kind is rebase.
            let mut tips = relevant_updates.iter().map(|(s, _)| *s).collect::<Vec<_>>();
            let mut seen = tips.iter().cloned().collect::<HashSet<_>>();

            while let Some(tip) = tips.pop() {
                for c in editor
                    .direct_children(tip)?
                    .iter()
                    .filter_map(|(c, _)| stack.nodes.contains_key(c).then_some(*c))
                {
                    if seen.insert(c) {
                        tips.push(c);

View on GitHub (pinned to 58e5313667)