crates/but/skill/references/concepts.md
Deep dive into GitButler's conceptual model and philosophy.
main ──┬── feature-a (checkout here, work, commit, checkout back)
└── feature-b (checkout here, work, commit, checkout back)
git checkoutworkspace (gitbutler/workspace)
├─ feature-a (applied, merged into workspace)
├─ feature-b (applied, merged into workspace)
└─ feature-c (unapplied, not in workspace)
No git checkout: You don't switch between branches. All applied branches exist simultaneously in your workspace.
The gitbutler/workspace branch: A merge commit containing all applied stacks. Don't interact with it directly - use but commands.
Applied vs Unapplied: Control which branches are active:
but apply/but unapply to controlEvery object gets a short, human-readable CLI ID shown in but status. IDs are generated per-session and are unique across all entity types (no two objects share an ID) — always read them from but status.
Commits: 1, kyn, mpq#0 (short change-ID prefix when the commit has one, sha prefix otherwise;
a #N suffix disambiguates commits sharing a change ID)
Branches: fe, bu, ui (unique 2–3 char substring of the branch name, e.g. "fe" from "feature-x";
falls back to auto-generated ID if no unique substring exists)
Files: g, qs, uo (derived from the file path, long enough to be unique)
Hunks: g:5, uo:d (<file-id>:<hunk-id>; the hunk part is derived from the hunk's content)
Committed files: kyn:n (<commit-id>:<file-id>, shown under each commit in `but status -fv`)
Stacks: m0, n0 (auto-generated, 2–3 chars)
Why? Git commit SHAs are long (40 chars). CLI IDs are short, variable-length, and unique within your current workspace context. Commits, files, and hunks may use a single character when that is unambiguous.
Reading status output: the first token on each line is that line's ID. Verbose commit lines append an informational (sha …) after the timestamp — it changes on every amend; do not pass it to commands.
Stability: File/hunk IDs copied from the current output generally remain usable across ordinary commits, so you can reference several in a row, including across chained but commit calls. If an ID stops resolving, re-read the diff and continue. Commit IDs are change-ID prefixes when the commit has a change ID and sha prefixes otherwise. Change-ID refs survive history edits (amend, squash, move, uncommit, reword); sha refs and #N-suffixed refs do not — a stale sha can silently resolve to the wrong commit. History edits may run in sequence off one status read when every ref involved is a change-ID ref; otherwise run them one at a time with --status-after to get the next ref.
Usage: Pass these IDs as arguments to commands:
but commit -b <branch-id> -m "message" <file-or-hunk-id> # Commit selected changes to a branch
but amend -t <commit-id> <file-or-hunk-id> <file-or-hunk-id> # Amend file(s) or hunk(s) into commit
but squash <commit-id> -t <commit-id> -m "message" # Squash commits
IDs are positional and space-separated. but help cli-ids documents every ID kind in detail.
Create with but branch new <name>:
main ──┬── api-endpoint (independent)
└── ui-update (independent)
Use when:
Example: Adding a new API endpoint and updating button styles are independent.
To stack an existing branch on top of another: but move <child-branch-name> --above <parent-branch-name>.
To create a new stacked branch from scratch: but branch new <name> -a <anchor> — only use this when the child branch doesn't exist yet.
main ── authentication ── user-profile ── settings-page
(base) (stacked) (stacked)
Use when:
Example: User profile page needs authentication to be implemented first.
Stacking two existing branches: If both branches already exist and you need to make one depend on the other, use top-level move:
but move feature/frontend --above feature/backend
# Now frontend is stacked on top of backend — both in the same stack
To tear off a branch from a stack:
but move feature/frontend --unstack
Dependency tracking: GitButler automatically tracks which changes depend on which commits. A dependent change can only be committed to the stack that contains the commits it depends on.
History editing is expressed as sources and a target. Sources are positional CLI IDs; the target
is a flag. zz is a special ID meaning "the uncommitted area".
but squash carries most of the model — what it does depends on the kinds you combine:
| Sources | Target (-t) | Operation | Example |
|---|---|---|---|
| Commit(s) | Commit | Squash commits together | but squash mm -t nn -m "…" |
| Branch | Commit | Squash a branch into a commit | but squash bu -t nn -m "…" |
| Commit(s) | Branch | Squash into the branch's newest | but squash mm -t bu -m "…" |
| Branch | (none) | Squash the branch into one commit | but squash bu -m "…" |
| Uncommitted file | Commit | Amend the change into a commit | but squash a1 -t nn |
zz | Commit | Amend everything into a commit | but squash zz -t nn |
| Commit | zz | Uncommit the commit | but squash mm -t zz |
| Committed file | Commit | Move the file to another commit | but squash nn:a -t mm |
Message flags: the rows whose sources are commits or branches compose a NEW message, so without
-m they open an editor and block — always pass one. The remaining rows reuse the target's message
and need no flag, and -t zz rejects message flags outright.
The two amend rows overlap with but amend — prefer but amend -t nn a1, which does only that and
takes the same IDs. Reach for squash when the sources are commits, branches, or committed files,
which amend does not accept.
The other editing commands are narrower entry points on the same model:
but amend -t <commit> <changes> — amend uncommitted files/hunks into a known commitbut uncommit <commits-or-committed-files> — move committed work back to uncommitted; committed
files in one call must come from one commitbut move <sources> --above|--below|--branch|--unstack — relocate commits, committed files, or a
branch; this is the command with position controlbut discard <changes> — drop work instead of relocating itGitButler tracks dependencies between changes automatically.
Commit C1: Added function foo()
Commit C2: Added function bar()
Uncommitted: Call to foo() in new code
The uncommitted change depends on C1 (because it calls foo()).
Implications:
Prevents you from creating broken states:
You can create empty commits:
but commit --empty --below nn -m "TODO: Add error handling"
but commit --empty --above nn -m "TODO: Add error handling"
Use cases:
Example workflow:
but commit --empty --below rr -m "TODO: Add error handling"
# Later, amend the error handling changes into the placeholder
but amend -t <empty-commit-id> <file-id>
Every operation in GitButler is recorded in the oplog (operation log).
but oplog # View history
but undo # Undo last operation
but redo # Redo last undone operation
but oplog list --since <snapshot-id>
but oplog list --snapshot
but oplog snapshot -m "known good"
but oplog restore <snapshot-id> # Restore to specific point
Think of it as "git reflog" but for all GitButler operations, not just branch movements.
Safety net: Made a mistake? but undo it. Experimented and want to go back? but oplog restore to earlier snapshot.
Branches can be in two states:
gitbutler/workspacebut apply <branch-name> # Make branch active
but unapply <id> # Make branch inactive
Use cases:
When but pull causes conflicts, affected commits are marked as conflicted.
but pull summary lists each conflicted commit's ID, oldest first (but status also shows them)but resolve <commit-id> — it prints the conflict regions with line numbers. With several conflicted commits, resolve the oldest first: finishing a lower commit rebases the ones above itbut resolve status re-lists what remains when several files are conflicted)but resolve finish or but resolve cancel — finish reports leftover markers and the surviving uncommitted changes, so no follow-up check is neededbut status shows you're in resolution modeGit commands that don't modify state are safe to use:
Safe (read-only):
git log - View historygit diff - See changes (but prefer but diff — it supports CLI IDs)git show - View commitsgit blame - See line historygit reflog - View reference logDon't use in a GitButler workspace:
git status - Misleading: shows merged workspace state, not individual stacks; missing CLI IDs that agents needgit commit - Commits to the workspace merge commit, not your branchgit checkout - Breaks workspace modelgit rebase - Conflicts with GitButler's managementgit merge - Use but land insteadRule of thumb: If it reads, it's fine. If it writes, use but instead.