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.
Multiple staging areas: Each branch is like having its own git add staging area. You stage files to specific branches.
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: 1b, 8f, c2 (short hex prefixes of the SHA, long enough to be unique)
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: g0, h0, i0 (auto-generated, 2–3 chars)
Hunks: j0, k1, l2 (auto-generated, 2–3 chars)
Stacks: m0, n0 (auto-generated, 2–3 chars)
Why? Git commit SHAs are long (40 chars). CLI IDs are short (2-3 chars) and unique within your current workspace context.
Usage: Pass these IDs as arguments to commands:
but commit <branch-id> -m "message" # Commit to branch
but stage <file-id> <branch-id> # Stage file to branch
but rub <commit-id> <commit-id> # Squash commits
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> <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 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 zz
Dependency tracking: GitButler automatically tracks which changes depend on which commits. You can't stage dependent changes to the wrong branch.
Traditional git has ONE staging area:
git add file1.js # Stage to THE staging area
git add file2.js # Stage to THE staging area
git commit # Commit from THE staging area
GitButler has MULTIPLE staging areas (one per branch):
but stage file1.js api-branch # Stage to api-branch's staging area
but stage file2.js ui-branch # Stage to ui-branch's staging area
but commit api-branch -m "..." # Commit from api-branch's staging area
but commit ui-branch -m "..." # Commit from ui-branch's staging area
Unstaged changes: Files not staged to any branch yet. Use but status to see them, then but stage to assign them.
Auto-assignment: If only one branch is applied, changes may auto-assign to it.
but rub Philosophybut rub is the core primitive operation: "rub two things together" to perform an action.
The operation performed depends on what you combine:
SOURCE ↓ / TARGET → │ zz (unassigned) │ Commit │ Branch │ Stack
─────────────────────┼─────────────────┼────────────┼─────────────┼────────────
File/Hunk │ Unstage │ Amend │ Stage │ Stage
Commit │ Undo │ Squash │ Move │ -
Branch (all changes) │ Unstage all │ Amend all │ Reassign │ Reassign
Stack (all changes) │ Unstage all │ - │ Reassign │ Reassign
Unassigned (zz) │ - │ Amend all │ Stage all │ Stage all
File-in-Commit │ Uncommit │ Move │ Uncommit & assign │ -
zz is a special target meaning "unassigned" (no branch).
Common examples:
| Source | Target | Operation | Example |
|---|---|---|---|
| File | Branch | Stage file to branch | but rub a1 bu |
| File | Commit | Amend file into commit | but rub a1 c3 |
| Commit | Commit | Squash commits | but rub c2 c3 |
| Commit | Branch | Move commit to branch | but rub c2 bu |
| File | zz | Unstage file | but rub a1 zz |
| Commit | zz | Undo commit | but rub c2 zz |
zz | Branch | Stage all unassigned | but rub zz bu |
These commands are wrappers around but rub:
but stage <file> <branch> = but rub <file> <branch>but amend <file> <commit> = but rub <file> <commit>but squash = Multiple but rub <commit> <commit> operationsbut move = commit move/reorder with position control, plus branch stack/tear-off (<branch> <target-branch> and <branch> zz)Why this design? One powerful primitive is easier to understand and maintain than many specialized commands. Once you understand but rub, you understand the editing model.
GitButler 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:
but absorb will automatically amend it into C1 (or a commit after C1)Prevents you from creating broken states:
You can create empty commits:
but commit empty --before c3
but commit empty --after c3
Use cases:
but mark <empty-commit-id> so future changes auto-amend into itExample workflow:
but commit empty -m "TODO: Add error handling" --before c5
but mark <empty-commit-id>
# Now work on error handling, changes auto-amend into the placeholder
Set a "mark" on a branch or commit to automatically organize new changes.
but mark <branch-id>
New unstaged changes automatically stage to this branch. Useful when focused on one feature.
but mark <commit-id>
New changes automatically amend into this commit. Useful for iterative refinement.
but mark <id> --delete # Remove specific mark
but unmark # Remove all marks
Example workflow:
but branch new refactor
but mark <refactor-branch-id>
# Make lots of changes - they all auto-stage to refactor branch
but unmark
Every operation in GitButler is recorded in the oplog (operation log).
but oplog # View history
but undo # Undo last operation
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 <id> # Make branch active
but unapply <id> # Make branch inactive
Use cases:
When but pull causes conflicts, affected commits are marked as conflicted.
but status shows conflicted commitsbut resolve <commit-id>but resolve status shows remaining conflictsbut resolve finish or but resolve cancelbut 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 wrong place (bypasses branch assignment)git checkout - Breaks workspace modelgit rebase - Conflicts with GitButler's managementgit merge - Use but merge insteadRule of thumb: If it reads, it's fine. If it writes, use but instead.