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/but → gitbutler-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) |
teardown → setup 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 perfectly — vb_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
- 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.
- 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".
- 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.
- 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)
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/gitbutlerleaves 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 apermanent split-brain: the Git layer still holds the complete workspace
(
refs/heads/gitbutler/workspace,refs/heads/gitbutler/target,gitbutler.project.targetref/targetcommitid), andbut statusrenders itcorrectly — but the rebuilt SQLite state has zero stacks. Stack-dependent
commands then fail with an internal assertion:
but teardownandbut setupdo not repair it; they form a loop.but setupeven prints the correct
Target branch: origin/developwhile still writing nostacks. 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/but→gitbutler-tauri, so CLI and desktop share the binary). Linux.originremote; GitButler targetorigin/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:
TODO: cannot run in non-workspace mode yet. It would need a way to deal with limiting the commit traversal.but status,commit,undo,cleanall work after the wipevb_state.initialized=1with alldefault_target_*NULL used as the corruption signalgitbutler.project.*and is read correctlybut teardownchecking out the internalgitbutler/workspacereffeature)teardown→setupdoes not heal the broken statevb_stacks = 0andBUG: stack has no idReproducer
Self-contained, deterministic, no network (builds its own bare
origin):repro.sh. Run./repro.sh.but setup+but branch new feature+but commitvb_stacks = 1,virtual_branches.toml= 560 B of real branch data,but unapply featuresucceedsrm -rf .git/gitbutler, then anybutcommandvb_stacks = 0,virtual_branches.tomlrewritten as an 11-byte[branches]stubbut statusfeatureand its commit shownbut unapply featureError: BUG: stack has no idbut branch listbut teardownthenbut setupTarget branch: origin/develop,vb_stacksstill 0,unapplystill fails — the looprefs/heads/gitbutler/{target,workspace}+gitbutler.project.*all present — the authoritative data that should have been usedbut.sqlite*, keepvirtual_branches.tomlvb_stacks = 1,unapplysucceedsObserved output (abridged)
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 rebuildsvb_stacksfromvirtual_branches.tomland recovers fully. Dropping the whole directory takes theTOML with it; a fresh empty
[branches]stub is written, and the rebuild neverconsults the Git layer for the stacks — even though the same code path
demonstrably reads
gitbutler.project.targetref/targetcommitidandrefs/heads/gitbutler/targetwell enough forbut statusto render the workspaceand for
but setupto print the right target branch.Defect 2 — no corruption detection, no self-heal.
but setupshort-circuits on "already set up" and trusts the existing state.but teardownchecks out a branch and exits workspace mode but leaves.git/gitbutler/andgitbutler.project.*untouched.gitbutler/workspaceref present and applied, but 0 stacks inSQLite" as corrupt. →
teardown → setupis a loop that never repairs it.but doctor,but repair, orbut setup --force.Defect 3 — an internal invariant violation is surfaced to the user.
BUG: stack has no idis an assertion message, not a diagnostic. It gives theuser nothing to act on.
but branch listdouble-listing the same branch as bothapplied and unapplied is the same split-brain leaking into the UI.
Acceptance criteria — all met on 0.22.1
vb_stacks = 0whilerefs/heads/gitbutler/workspaceexists andgitbutler.project.targetcommitidis set.but teardownfollowed bybut setupdoes not restore the stacks.but unapply <branch>fails withError: BUG: stack has no id.The only working recovery (verified on 0.22.1)
After this the workspace is healthy and new branches/commits work normally. The
pre-existing
featurebranch survives as an ordinary local Git branch — confirmingnothing was ever lost.
Proposed fix
state dir is rebuilt and
refs/heads/gitbutler/workspaceexists with parentsbeyond the target, reconstruct the stacks from those parents (and
gitbutler.project.*) instead of writing an empty[branches]stub. Thetarget is already reconstructed this way — extend the same treatment to stacks.
but setup(anda startup integrity check) detect it and reinitialize from the Git layer rather
than short-circuiting on "already set up".
but setup --force/but doctorthat doesthe manual recovery above, or make
teardownoptionally clear.git/gitbutler+gitbutler.project.*so a latersetupstarts genuinely clean.BUG: stack has no idshould be anactionable error naming the inconsistency and pointing at the repair command.
Fix
but branch listso a branch cannot appear as applied and unapplied at once.Source-investigation pointers
BUG: stack has no id(present in the 0.22.1 binary) — the assertion site..git/gitbutlerinit /virtual_branches.toml↔ SQLite reconciliation path(
vb_state.toml_last_seen_mtime_ns/toml_last_seen_sha256) — where theempty-stub-over-Git-refs choice lives (defect 1).
setup"already set up" short-circuit andteardown's cleanup scope (defect 2).but branch listapplied/unapplied partitioning (defect 3).vb_state.default_target_*are NULL even on a healthy0.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)