Skip to content

but pull crashes with BUG(opt-stack-id) mid-base-update, leaving refs/metadata/workspace-commit desynced with no CLI recovery path #14848

Description

@bahalperin

Version: but 0.21.0 (CLI), macOS (Apple Silicon)
Repo shape: large private monorepo, ~9 applied single-branch stacks in the workspace, multiple agents/sessions committing via but concurrently

Summary

Two related incidents in one day, in the same workspace. The second one left the workspace in a state where but status, but unapply, and but oplog restore all fail, with no CLI recovery path — I had to repair it with hand-crafted git plumbing.

  1. A but amend silently dropped an unrelated applied stack from the workspace — no UNAPPLY oplog entry, no message. The stack's files stayed on disk and reappeared as uncommitted changes.
  2. A later but pull crashed partway through integrating one upstream commit. It had already rebased all nine applied branch refs onto the new base and deleted an integrated branch's ref + metadata, but died before rewriting the workspace merge commit — leaving branch refs and virtual_branches.toml in the "new world" while gitbutler/workspace (HEAD) still pointed at the old merge commit with the stale tips.

The main ask here is transactionality of upstream integration#14831 reports a different assertion (no entrypoints in the rebase engine) with the same fact pattern on 0.21.0: integration crashes mid-flight and wedges the workspace. The downstream IdMap wedge is #14497 (partially fixed by #14700, merged after the 0.21.0 release).

Incident 1: amend silently evicts an unrelated stack

  • Workspace had ~10 applied stacks, including one stack with two heads (branch-x + branch-y, both at tip <commit-A>).
  • A but amend targeting a commit on a different stack ran (call it T+0).
  • The resulting workspace commit no longer had <commit-A> as a parent, and virtual_branches.toml (rewritten ~1s later) flipped that stack to in_workspace = false.
  • The oplog records only [AMEND] Amended commitno UNAPPLY entry (every legitimate unapply in my oplog history has one). No warning was printed.
  • The evicted stack's files were left in the worktree (mtimes untouched), so they showed up as uncommitted/untracked changes, duplicating content committed on the now-unapplied branch.

Expected: an amend on stack A should never remove stack B from the workspace; if eviction is ever necessary it should be recorded in the oplog and reported to the user (related: #12355).

Incident 2: but pull crashes and leaves a three-way desync

After re-applying branch-x (clean), I ran but pull about a minute later:

but pull
Fetching newest data from remotes...
   Checking: git@github.com:<org>/<repo>/origin/main

Found 1 upstream commits on origin/main
   <upstream-sha> <commit subject> (#<pr>)
   Checking integration statuses...
Error: BUG(opt-stack-id): should have gotten exactly one stack, got 8

The error is the bail! in crates/but-workspace/src/legacy/stacks.rsstack_details_v3(): it was called with stack_id: None (via get_stack_status() in crates/gitbutler-branch-actions/src/upstream_integration.rs, which passes stack.id unguarded), fell into the "assume single-branch mode" arm, found 8 stacks in head_info(), and bailed. So some stack in the projection had no StackId — presumably because the operation was already partway through mutating state.

State after the crash (verified via reflogs and oplog snapshot trees):

  • All nine applied branch refs had been rebased onto the new base and moved (all reflog entries carry the same timestamp as the pull).
  • The integrated branch (squash-merged upstream as <upstream-sha>) had its ref deleted and its stanza removed from virtual_branches.toml.
  • virtual_branches.toml head CommitIds matched the new rebased tips.
  • But gitbutler/workspace still pointed at the pre-pull merge commit, wh — including a commit whose only remaining ref was branch-y (which has nostack metadata), and the deleted integrated branch's old tip.

The oplog has a single [UPDATE_BASE] Updated workspace base entry for this —ten, then the operation crashed mid-flight with no rollback.

Consequence: no CLI recovery path

With refs/metadata in the new world and HEAD in the old world, every relevant command failed:

  • but status prints a garbled tree (duplicated uncommitted sections, a phantom stack for the metadata-less ref) and ends with:
    Error: Could not find branch CLI id '' in IdMap (this symptom is Stale parent in workspace merge commit wedges CLI ("Could not find branch CLI id '' in IdMap") and shows "Unnamed segment" #14497; #but isn't in 0.21.0)
  • but unapply <metadata-less-branch>Branch '…' does not have an associated stack
  • but unapply <metadata-having-branch> → `Branch '…' not found in any applie metadata layer each see a different branch as applied — deadlock)
  • but oplog restore <any snapshot>
    Error: Specified HEAD <tree-a> didn't match actual HEAD^{tree} <tree-b>
    (the precondition in crates/but-core/src/worktree/checkout/utils.rs::merge_tion_or_keep_snapshot — with a dirty worktree it requires HEAD's tree to match
    the snapshot's recorded source tree, which can't hold once the workspace is de unusable exactly when you need it most**). This resembles App unusable after failure to un-apply stack #11648.

Manual recovery that worked (for anyone else hitting this)

The crashed pull had actually done the rebases correctly, so I completed its f

  • but oplog restore <any snapshot>
    Error: Specified HEAD <tree-a> didn't match actual HEAD^{tree} <tree-b>
    (the precondition in crates/but-core/src/worktree/checkout/utils.rs::merge_tion_or_keep_snapshot — with a dirty worktree it requires HEAD's tree to match the snapshot's recorded source tree, which can't hold once the workspace is desynced, so oplog restore is unusable exactly when you need it most). This resembles App unusable after failure to un-apply stack #11648.

After that, but status was fully healthy again.

Suggested fixes

  1. but pull / upstream integration should be transactional: don't move brancil the new workspace commit is ready; on error, roll back to the
    before-snapshot automatically.
  2. get_stack_status() should handle stack.id == None gracefully (skip/warnetails_v3(None)` bail on multi-stack workspaces.
  3. but oplog restore needs to work from an inconsistent state — that's its p
  4. Amend should never silently evict another stack, and any eviction must prodisible message.
    Repo shape: large private monorepo, ~9 applied single-branch stacks in theessions committing via but concurrently

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfeedback requestedFeedback was requested to help resolve the issue

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions