Use Jujutsu (jj) for version control. Covers workflow, commits, bookmarks, pushing to GitHub, absorb, squash, and stacked PRs...
In jj, the working copy IS a commit. This is fundamentally different from Git where you stage changes then commit. In jj, you're always working inside a commit.
This means:
jj statusBefore any operation, check your state:
jj status
# 1. Create a new commit BEFORE starting work
jj new
# 2. Make your changes (edit files)
# 3. Describe what you did
jj describe -m "Add user authentication"
# 4. Create a new commit for the next task
jj new
# Repeat
Why jj new before work? If you work directly in a described commit, those changes get mixed in. Then you have to split them later. Starting fresh means you can always squash back if needed.
jj status # Check current state - ALWAYS run first
jj log # View commit history
jj diff # See changes in current commit
jj describe -m "" # Set/update commit message
jj new # Create new empty commit
jj squash # Move current changes into parent commit
jj absorb # Auto-distribute changes to ancestor commits
jj uses "bookmarks" instead of Git branches. Key differences:
jj bookmark list # List all bookmarks
jj bookmark set foo -r @ # Create/move bookmark to current commit
jj bookmark delete foo # Delete a bookmark
Use --change (-c) to auto-create a bookmark and push:
# Push current commit, auto-generates bookmark name
jj git push -c @
# Push parent of working copy (common when @ is empty)
jj git push -c @-
This creates a bookmark with an auto-generated name like push-abcdefgh and pushes it.
Once a bookmark exists and tracks the remote, just push:
jj git push
jj knows which bookmarks have changed and pushes them. Force push is automatic when history rewrites.
# Get the bookmark name from jj log or the push output
jj log -r @ --no-graph
# Create PR with gh CLI (must specify --head since gh uses git)
gh pr create --head push-abcdefgh
When you need to update an existing PR:
# 1. Create new commit on top of the PR commit
jj new
# 2. Make your changes
# 3. Fold changes back into the PR commit
jj squash # Moves all changes to parent
# 4. Push the update
jj git push
Never work directly in the PR commit. Always jj new first, then squash back.
jj squash - When You Know Where It GoesMoves changes from current commit into parent:
jj squash # All changes to parent
jj squash file.rs # Only specific files
jj squash --into <commit> # Into a specific commit (not just parent)
jj absorb - Auto-Distribute to AncestorsAnalyzes each changed line and moves it to the ancestor commit that last modified that line:
jj absorb
Best for stacked PRs: When you have commits A → B → C and fix things that belong in different commits, jj absorb figures out where each change should go.
Try jj absorb first. If it can't figure out where something goes (new lines, ambiguous context), use jj squash manually.
When working on multiple dependent PRs:
# Create stack: each commit = one PR
jj new main
# work on feature A
jj describe -m "Feature A"
jj git push -c @
jj new
# work on feature B (depends on A)
jj describe -m "Feature B"
jj git push -c @
# Continue stacking...
When a PR in the middle of the stack merges:
# 1. Update the next PR's base branch on GitHub
gh pr edit <PR_NUMBER> --base main
# 2. Fetch the new main
jj git fetch
# 3. Rebase the remaining stack onto main
jj rebase -r <first-remaining-commit> -d main
# 4. Push all updated branches
jj git push --all
The * mark in jj log indicates bookmarks that need pushing.
Need to modify a commit that has descendants?
# Switch working copy to that commit
jj edit <commit-id>
# Make changes (they go directly into that commit)
# Return to where you were
jj edit @
# Descendants are automatically rebased
jj git push --all
When a commit has changes that should be separate:
# Interactive split
jj split
# Non-interactive: specify files for first commit
JJ_EDITOR=true jj split -m "First commit message" file1.rs file2.rs
# Remaining files stay in second commit
jj describe -m "Second commit message"
Made a mistake? jj tracks all operations:
jj undo # Undo last operation
jj op log # View operation history
jj op restore <id> # Restore to specific operation
Wrong:
jj describe -m "Feature A"
# ... keep making changes in same commit ...
# Now Feature A commit has unrelated stuff mixed in
Right:
jj describe -m "Feature A"
jj new # Start fresh for next work
# ... make changes ...
jj squash # If changes belong in Feature A
--allow-new on First Push# First time pushing a bookmark to a new remote
jj git push --bookmark main --allow-new
After the bookmark exists on the remote, you don't need this flag.
Wrong:
jj new
# ... changes for existing PR ...
jj git push -c @ # Creates NEW bookmark/PR!
Right:
jj new
# ... changes for existing PR ...
jj squash # Fold into existing PR commit
jj git push # Updates existing bookmark