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
- Flatten the stack to a single head (merge or rebase its extra branches together) before integrating upstream
- Ensure the stack has one bottom commit by splitting off extra bases into separate stacks
- Use an integration strategy other than Merge (e.g. rebase-based updates) for multi-head stacks
- 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
- Keep one branch per stack when merge-based upstream integration is expected
- Split multi-base stacks into separate stacks before integrating
- Prefer rebase-style updates for stacks allowed to diverge
- Check stack shape with the workspace API before requesting integration
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
- Ad-hoc (single-branch) branch moves are not supported…
- Branch ' ' cannot be created: the target commit ( ) already…
- BranchNotFound
- BUG: failed to apply head ref
- Cannot add the target
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)