crates/but/skill/references/examples.md
Real-world examples of common workflows.
Note on CLI IDs: Examples below use full branch names for branch-targeting mutations. Illustrative IDs like nn and a1 keep other commands readable; in practice, always read actual IDs from but status -fv because they are generated for the current workspace snapshot. Commit IDs are short change-ID prefixes that stay stable across history edits (e.g., kyn; commits without a change ID fall back to a sha prefix), and file/hunk/stack IDs are auto-generated (e.g., r, r:c, h0). All IDs are unique across entity types within one snapshot.
Scenario: Need to work on two independent features: a new API endpoint and UI styling updates.
# 1. Check current state
but status -fv
# 2. Create two independent (parallel) branches
but branch new api-endpoint
but branch new ui-styling
# 3. Make changes to multiple files
# (edit api/users.js and components/Button.svelte)
# 4. Check what's uncommitted
but status -fv
# 5. Commit specific files by passing their CLI IDs (recommended for agents)
# Use full branch names plus file IDs from but status -fv output
# Multiple IDs are space-separated positional arguments.
but commit -b api-endpoint -m "Add user details endpoint" <api-file-id>
but commit -b ui-styling -m "Update button hover styles" <ui-file-id>
# Follow-up fix that belongs in a commit you just made? Amend it in.
# Add --status-after only when the next step needs resulting workspace IDs or details.
# but amend -t <api-commit-id> <api-fix-file-id> <api-fix-hunk-id>
# 6. Create pull requests (auto-pushes the branches; -m sets the PR title so no editor opens)
but pr new api-endpoint -m "Add user details endpoint"
but pr new ui-styling -m "Update button hover styles"
Why parallel branches? The API endpoint and UI styling are independent - neither depends on the other. They can be reviewed and merged separately.
Scenario: Need to add authentication, then build a user profile page that requires auth.
# 1. Check current state and update
but pull
but status -fv
# 2. Create base branch for authentication
but branch new add-authentication
# 3. Implement auth and commit
# (edit auth/login.js, auth/middleware.js)
but status -fv
but commit -b add-authentication -m "Add JWT authentication" <file-ids>
# 4. Create stacked branch anchored on authentication
but branch new user-profile -a add-authentication
# 5. Implement profile page (depends on auth)
# (edit pages/profile.js)
but status -fv
but commit -b user-profile -m "Add user profile page" <file-ids>
# 6. Create stacked pull requests from the top branch once (auto-pushes its ancestors)
but pr new user-profile -t
Result: Two PRs where user-profile targets add-authentication, with GitButler stack information in the PR descriptions.
Scenario: Made a small typo fix that should be part of an existing commit, not a new commit.
# 1. Check current commits and uncommitted changes
but status -fv
# Output shows:
# Branch: feature-x (bu)
# Commits:
# nn: Implement feature logic
# mm: Add feature tests
# Uncommitted:
# a1: fix-typo.js
# 2. Decide which commit the fix belongs to
# (the typo is in code introduced by nn, so it belongs in nn)
# 3. Amend the file into that commit
but amend -t nn a1 # Amend just this file + get updated status
Why amend? Keeps history clean. Small fixes belong in the commits they fix, not as separate "fix typo" commits. You know what you changed and why — pick the target commit yourself.
Situation: Made 5 small WIP commits, want to combine into one logical commit.
# Before (newest first):
# rr: More tweaks
# pp: Fix another thing
# nn: Fix tests
# mm: Adjust logic
# kk: Initial implementation
# Squash all commits in the branch into one
but squash feature -m "Implement feature"
# Or squash specific commits into a target commit
but squash rr pp nn mm -t kk -m "Implement feature"
Situation: A file was committed in the wrong commit, need to move it.
# 1. See which files are in which commits
but status -fv
# Output shows:
# nn: Add API layer
# nn:a1 api.js
# nn:a2 utils.js
# mm: Add config
# mm:c1 config.js
# 2. Move utils.js from nn to mm
but squash nn:a2 -t mm -u # Committed file nn:a2 (utils.js) → commit mm, keep mm's message
Situation: Committed to wrong branch, need to move commit.
# 1. Check current state
but status -fv
# Output:
# Branch: feature-a (bu)
# nn: This should be in feature-b!
# mm: Correct commit
# 2. Create or identify target branch
but branch new feature-b # Creates branch bv
# 3. Move the commit
but move nn -b feature-b # Move nn to top of feature-b
Scenario: Two independent branches exist, but one now depends on the other. Stack them.
# 1. Check current state — two independent branches in separate stacks
but status -fv
# Output:
# Stack 1: feature/backend (bu) — 2 commits
# Stack 2: feature/frontend (bv) — 1 commit
# 2. Frontend now depends on backend API — stack frontend on backend
# IMPORTANT: Use full branch names; short IDs belong to the previous snapshot
but move feature/frontend --above feature/backend
# Result: Both branches are now in the same stack:
# Stack 1: feature/backend → feature/frontend (stacked)
# 3. Continue working — commits go to the right branch
but status -fv
but commit -b feature/backend -m "Add caching layer" <id>
but commit -b feature/frontend -m "Add dialog component" <id>
Key point: branch stack moves use full branch names like feature/frontend. Commit reordering still uses commit IDs.
Scenario: After but pull, conflicts appear in a commit.
# 1. Pull updates
but pull
# Output:
# Summary
# ────────
# feature-x - conflicted
# nn Add validation
# 2. Enter resolution mode using the commit ID from the pull output
but resolve nn
# Output:
# Checking out conflicted commit nn
# Conflicted files remaining:
# ✗ api/users.js
# 12│<<<<<<< New base: ...
# ...conflict regions with line numbers...
# 3. Edit each conflicted file to resolve
# IMPORTANT: You MUST edit the files — do NOT just run `but resolve finish`
# NEVER use `git add`, `git checkout --theirs/--ours`, or any git write command — just edit the files directly with the Edit tool, then `but resolve finish`
# (edit to remove every marker — <<<<<<< ||||||| ======= >>>>>>> — and keep correct content;
# with several conflicted files, `but resolve status` re-lists what remains)
# 4. Finalize
but resolve finish
# Output:
# ✓ Conflict resolution finalized successfully!
# No conflict markers remain in the resolved files.
# Workspace restored; uncommitted changes intact: ...
# No follow-up status or marker scan needed — finish already reports both.
Scenario: Building a complete feature from start to finish.
# 1. Update to latest
but pull
# 2. Create branch for feature
but branch new user-dashboard
# 3. Make initial changes
# (create dashboard.js, add routes)
# 4. Check status and gather file IDs
but status -fv
# 5. First commit
but commit -b user-dashboard -m "Add dashboard route and basic layout" <file-ids>
# 6. Continue iterating
# (add widgets, styling)
but commit -b user-dashboard -m "Add dashboard widgets" <file-ids>
but commit -b user-dashboard -m "Style dashboard components" <file-ids>
# 7. Make small fix
# (fix typo in widget)
but amend -t <commit-id> a1 # Amend fix into the commit it belongs to
# 8. Clean up if needed
but squash user-dashboard -m "Add user dashboard" # Combine all commits (optional)
# 9. Create pull request (auto-pushes the branch)
but pr new user-dashboard -m "Add user dashboard"
# Output:
# Created PR #123: https://github.com/org/repo/pull/123
# 10. After PR is merged, update
but pull
Scenario: Have 3 branches, but two are causing conflicts. Temporarily unapply them.
# 1. Check active branches
but status -fv
# Output:
# Applied branches:
# bu: feature-a
# bv: feature-b
# bw: feature-c
# 2. Conflicts between feature-b and feature-c
# Unapply them temporarily
but unapply feature-b
but unapply feature-c
# 3. Focus on feature-a
# (make changes, commit)
but commit -b feature-a -m "Complete feature-a" <file-ids>
# 4. Create PR for feature-a (auto-pushes)
but pr new feature-a -m "Complete feature-a"
# 5. Reapply other branches
but apply feature-b
but apply feature-c
# 6. Deal with their conflicts now
but resolve ...
Scenario: Made several commits, realized you need to reword messages and reorder.
# 1. Current state
but status -fv
# Output (newest first):
# Branch: feature-x (bu)
# rr: final commit
# pp: WIP
# nn: Fix stuff
# mm: Another fix
# kk: Initial
# 2. Reword commit messages — commit refs are change-ID based and stay
# valid across rewords and other history edits
but reword pp -m "Add validation logic"
but reword nn -m "Fix edge case in parser"
but reword mm -m "Update error messages"
# 3. Move rr to be earlier
but move rr --below nn # Place rr directly below nn
# 4. Squash similar commits
but squash mm -t nn -u # Combine error handling commits; -u keeps nn's message, drops mm's
# Output (newest first):
# Branch: feature-x (bu)
# pp: Add validation logic
# nn: Fix edge case in parser
# rr: final commit
# kk: Initial
# 5. Push clean history
but push feature-x
Typical day working with GitButler:
# Morning: Start day
but pull # Get latest from team
# Start new task
but branch new fix-auth-bug # Create branch for today's work
# Work and commit iteratively
# (make changes)
but status -fv # Check changes
but commit -b fix-auth-bug -m "Identify auth bug source" <file-ids>
# (make more changes)
but commit -b fix-auth-bug -m "Fix token expiration handling" <file-ids>
# (small fix to existing code)
but amend -t <commit-id> a1 # Amend fix into the commit it belongs to
# Mid-day: Start urgent fix on different branch
but branch new hotfix-login # Parallel branch for urgent work
# (make fix)
but commit -b hotfix-login -m "Fix login redirect loop" <file-ids>
but pr new hotfix-login -m "Fix login redirect loop" # Push and create PR immediately
# Back to original work
# (continue working on fix-auth-bug)
but commit -b fix-auth-bug -m "Add tests for token handling" <file-ids>
# End of day: Clean up and create PR
but squash fix-auth-bug -m "Fix auth bug" # Combine into clean history
but pr new fix-auth-bug -m "Fix auth bug" # Push and create PR
# After PR review: Make requested changes
# (make changes based on feedback)
but amend -t <commit-id> <file-id> # Amend each fix into the commit it belongs to
but push fix-auth-bug # Push updated history
Scenario: Made changes you didn't mean to, need to undo.
# Made a mistake
but squash feature -m "..." # Oops! Didn't mean to squash
# Undo it
but undo # Reverts the squash
# View operation history
but oplog
# Output (snapshot refs are git SHAs, not CLI IDs):
# 9c1f2ab 2026-01-02 [SQUASH] Squashed commits
# f8a3733 2026-01-02 [COMMIT] Created commit
# 4b70e19 2026-01-02 [AMEND] Amended commit
# 1d5c806 2026-01-02 [BRANCH] Created branch
# Restore to before the squash, using the SHA from the output
but oplog restore f8a3733
# Changed a file but want to discard
but status -fv
# Output:
# Uncommitted:
# a1: bad-changes.js
# Discard it
but discard a1
but status -fv # File-centric view for quick overview
but push my-feature --dry-run # See what would be pushed
File/hunk IDs copied from the original output generally remain usable across
commits. Chain but commit calls to split a dirty diff into several commits in
one go:
but diff # read the file/hunk IDs once
but commit -b my-branch -m "Add parser" qs:5 qs:2 \
&& but commit -b my-branch -m "Add tests" uo:d
The commits stack in the order you write them, so Add parser ends up below (older
than) Add tests. Chain these commit commands when each references uncommitted IDs
and a full branch name. If an ID stops resolving, re-read the diff and continue.
Mutation output is concise by default. Add --status-after only when the next
step needs workspace IDs or details that the mutation result does not provide.
History edits — amend, squash, move, uncommit, reword — may also run in
sequence off one status read when every commit ref involved is a change-ID ref;
those stay stable across the edits. Run them one at a time when a ref is sha-based
or #N-suffixed, or when the next command needs freshly issued IDs, and add
--status-after to get them.
eval "$(but completions zsh)" # Add to ~/.zshrc
eval "$(but completions bash)" # Add to ~/.bashrc
but show bu # Show all commits in branch
git log bu # Traditional git log (read-only, still works)