Skip to content

rm -rf .git/gitbutler leaves a project with zero stacks and no way to repair it (BUG: stack has no id) #15600

Description

@masecla22

Version

0.22.1

Operating System

Linux

Distribution Method

deb (Linux)

Describe the issue

I encountered this issue while working on a project with GitButler, reaching some sort of weird state (which I don't recall) and attempting to reinitialize git butler by fully removing it from the .git folder.

Diagnosis and fixes were done by AI postmortem:

rm -rf .git/gitbutler leaves a project with zero stacks and no way to repair it (BUG: stack has no id)

Summary

Deleting .git/gitbutler/ on an otherwise-healthy GitButler project puts it in a
permanent split-brain: the Git layer still holds the complete workspace
(refs/heads/gitbutler/workspace, refs/heads/gitbutler/target,
gitbutler.project.targetref / targetcommitid), and but status renders it
correctly — but the rebuilt SQLite state has zero stacks. Stack-dependent
commands then fail with an internal assertion:

Error: BUG: stack has no id

but teardown and but setup do not repair it; they form a loop. but setup
even prints the correct Target branch: origin/develop while still writing no
stacks. Only manual, out-of-band cleanup recovers the project.

Nothing is lost — every object is in Git refs/config/reflog — so this is a
robustness / self-heal defect, not data loss.

Version

  • but 0.22.1 (/usr/bin/butgitbutler-tauri, so CLI and desktop share the binary). Linux.
  • Repo has a normal origin remote; GitButler target origin/develop.

What changed since the original report (0.20.3 / 0.20.4)

This was first hit on 0.20.3. Re-verified on 0.22.1 — partly fixed, partly not:

0.20.x symptom 0.22.1
TODO: cannot run in non-workspace mode yet. It would need a way to deal with limiting the commit traversal. Fixed — the string is no longer present in the binary; but status, commit, undo, clean all work after the wipe
vb_state.initialized=1 with all default_target_* NULL used as the corruption signal Obsolete — those columns are now NULL on a healthy project too, so they are no longer a usable oracle. The target now lives in gitbutler.project.* and is read correctly
but teardown checking out the internal gitbutler/workspace ref Fixed — teardown now checks out the real branch (feature)
teardownsetup does not heal the broken state NOT fixed — still a loop
Stack state not reconstructed from the still-present Git refs NOT fixed — now surfaces as vb_stacks = 0 and BUG: stack has no id

Reproducer

Self-contained, deterministic, no network (builds its own bare origin):
repro.sh. Run ./repro.sh.

step action result
1 but setup + but branch new feature + but commit healthy: vb_stacks = 1, virtual_branches.toml = 560 B of real branch data, but unapply feature succeeds
2 rm -rf .git/gitbutler, then any but command broken: vb_stacks = 0, virtual_branches.toml rewritten as an 11-byte [branches] stub
2a but status still correct — target reconstructed from Git config, feature and its commit shown
2b but unapply feature Error: BUG: stack has no id
2c but branch list lists the same branch twice — under Applied branches and Unapplied Branches
3 but teardown then but setup setup prints Target branch: origin/develop, vb_stacks still 0, unapply still fails — the loop
4 inspect Git layer refs/heads/gitbutler/{target,workspace} + gitbutler.project.* all present — the authoritative data that should have been used
5 contrast: delete only but.sqlite*, keep virtual_branches.toml recovers perfectlyvb_stacks = 1, unapply succeeds

Observed output (abridged)

=== 1. healthy project ===
  vb_stacks           : 1
  virtual_branches.toml: 560 bytes
  but unapply feature -> Unapplied stack with 'feature' from workspace

=== 2. the trigger: user wipes .git/gitbutler ===
  vb_stacks           : 0          (BUG: expected 1)
  virtual_branches.toml: 11 bytes  (rewritten as an empty stub)

=== 2b. stack-dependent commands hit an internal assertion ===
  but unapply feature -> Error: BUG: stack has no id

=== 2c. branch list shows the SAME branch as applied AND unapplied ===
  Applied branches
  active  ✓ *feature ↑1     today      a
  Unapplied Branches
  local   ✓ feature ↑1     today      a

=== 3. teardown -> setup does NOT heal ===
  teardown -> checked out feature
  GitButler project is already set up!
  Target branch: origin/develop
  vb_stacks           : 0          (STILL 0 — no self-heal)
  but unapply feature -> Error: BUG: stack has no id

=== 4. Git layer that SHOULD have been used to rebuild ===
  052a2a1… refs/heads/gitbutler/target
  64e0230… refs/heads/gitbutler/workspace
  gitbutler.project.targetref      refs/remotes/origin/develop
  gitbutler.project.targetcommitid 052a2a1…

=== 5. CONTRAST — delete only but.sqlite*, KEEP the TOML ===
  vb_stacks           : 1
  but unapply feature -> Unapplied stack with 'feature' from workspace

Root cause

Defect 1 — the stack rebuild has exactly one source of truth, and it is the wrong one.
Step 5 isolates it: dropping but.sqlite* alone rebuilds vb_stacks from
virtual_branches.toml and recovers fully. Dropping the whole directory takes the
TOML with it; a fresh empty [branches] stub is written, and the rebuild never
consults the Git layer for the stacks
— even though the same code path
demonstrably reads gitbutler.project.targetref / targetcommitid and
refs/heads/gitbutler/target well enough for but status to render the workspace
and for but setup to print the right target branch.

Defect 2 — no corruption detection, no self-heal.

  • but setup short-circuits on "already set up" and trusts the existing state.
  • but teardown checks out a branch and exits workspace mode but leaves
    .git/gitbutler/ and gitbutler.project.* untouched.
  • Neither treats "gitbutler/workspace ref present and applied, but 0 stacks in
    SQLite" as corrupt. → teardown → setup is a loop that never repairs it.
  • There is no but doctor, but repair, or but setup --force.

Defect 3 — an internal invariant violation is surfaced to the user.
BUG: stack has no id is an assertion message, not a diagnostic. It gives the
user nothing to act on. but branch list double-listing the same branch as both
applied and unapplied is the same split-brain leaking into the UI.

Acceptance criteria — all met on 0.22.1

  • vb_stacks = 0 while refs/heads/gitbutler/workspace exists and
    gitbutler.project.targetcommitid is set.
  • but teardown followed by but setup does not restore the stacks.
  • but unapply <branch> fails with Error: BUG: stack has no id.

The only working recovery (verified on 0.22.1)

git checkout develop
git branch -D gitbutler/workspace gitbutler/target
git config --local --remove-section gitbutler.project
git config --local --unset gitbutler.signcommits    # if set
mv .git/gitbutler .git/gitbutler.broken             # drop the poisoned state dir
yes | but setup                                     # fresh init

After this the workspace is healthy and new branches/commits work normally. The
pre-existing feature branch survives as an ordinary local Git branch — confirming
nothing was ever lost.

Proposed fix

  1. Reconstruct stacks from Git, don't trust an absent/empty TOML. When the
    state dir is rebuilt and refs/heads/gitbutler/workspace exists with parents
    beyond the target, reconstruct the stacks from those parents (and
    gitbutler.project.*) instead of writing an empty [branches] stub. The
    target is already reconstructed this way — extend the same treatment to stacks.
  2. Treat "workspace ref applied + 0 stacks" as corrupt. Have but setup (and
    a startup integrity check) detect it and reinitialize from the Git layer rather
    than short-circuiting on "already set up".
  3. Give the user an escape hatch. but setup --force / but doctor that does
    the manual recovery above, or make teardown optionally clear
    .git/gitbutler + gitbutler.project.* so a later setup starts genuinely clean.
  4. Replace the assertion with a diagnostic. BUG: stack has no id should be an
    actionable error naming the inconsistency and pointing at the repair command.
    Fix but branch list so a branch cannot appear as applied and unapplied at once.

Source-investigation pointers

  • Grep for BUG: stack has no id (present in the 0.22.1 binary) — the assertion site.
  • The .git/gitbutler init / virtual_branches.toml ↔ SQLite reconciliation path
    (vb_state.toml_last_seen_mtime_ns / toml_last_seen_sha256) — where the
    empty-stub-over-Git-refs choice lives (defect 1).
  • The setup "already set up" short-circuit and teardown's cleanup scope (defect 2).
  • but branch list applied/unapplied partitioning (defect 3).
  • Dead columns worth noting: vb_state.default_target_* are NULL even on a healthy
    0.22.1 project. If they're vestigial they should be dropped; if not, they aren't
    being populated.

How to reproduce (Optional)

gitbutler-bug-repro.zip

Expected behavior (Optional)

No response

Relevant log output (Optional)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingos:linuxThe Linux platform

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions