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: 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 amend <file-or-hunk-id> <commit-id> # Amend file or hunk into commit
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. A dependent change can only be committed to the stack that contains the commits it depends on.
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 | Operation | Example |
|---|---|---|---|
| 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 |
| Commit | zz | Undo commit | but rub c2 zz |
zz is a special target meaning "unassigned" (no branch).
These commands are wrappers around but rub:
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:
Example workflow:
but commit empty --before c5
but reword <empty-commit-id> -m "TODO: Add error handling"
# Later, amend the error handling changes into the placeholder
but amend <file-id> <empty-commit-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 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 the workspace merge commit, not your branchgit 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.