| name | jj |
| description | Use Jujutsu (jj) for version control. Covers workflow, commits, bookmarks, pushing to GitHub, absorb, squash, and stacked PRs. Use when working with jj, creating commits, pushing changes, or managing version control. |
Jujutsu (jj) Version Control
The Key Mental Model
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:
- Changes you make are immediately part of the current commit
- You should start new work in a fresh commit (so you can squash it later)
- There's no staging area - your working directory is the commit
Core Workflow
Always Start with jj status
Before any operation, check your state:
jj status
- "Working copy is clean" = you're in an empty commit, ready to work
- Shows file changes = current commit has work in it
- Shows description = current commit is already described
The Basic Pattern
jj new
jj describe -m "Add user authentication"
jj new
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.
Common Commands
jj status
jj log
jj diff
jj describe -m ""
jj new
jj squash
jj absorb
Bookmarks (Not Branches)
jj uses "bookmarks" instead of Git branches. Key differences:
- Bookmarks are just labels pointing to commits
- They don't automatically advance when you commit
- You can have commits without any bookmark
jj bookmark list
jj bookmark set foo -r @
jj bookmark delete foo
Pushing to GitHub
First Push (New PR)
Use --change (-c) to auto-create a bookmark and push:
jj git push -c @
jj git push -c @-
This creates a bookmark with an auto-generated name like push-abcdefgh and pushes it.
Subsequent Pushes (Same PR)
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.
Creating the PR
jj log -r @ --no-graph
gh pr create --head push-abcdefgh
Making More Changes to a PR
When you need to update an existing PR:
jj new
jj squash
jj git push
Never work directly in the PR commit. Always jj new first, then squash back.
Squash vs Absorb
jj squash - When You Know Where It Goes
Moves changes from current commit into parent:
jj squash
jj squash file.rs
jj squash --into <commit>
jj absorb - Auto-Distribute to Ancestors
Analyzes 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.
Stacked PRs Workflow
When working on multiple dependent PRs:
jj new main
jj describe -m "Feature A"
jj git push -c @
jj new
jj describe -m "Feature B"
jj git push -c @
After a PR Merges
When a PR in the middle of the stack merges:
gh pr edit <PR_NUMBER> --base main
jj git fetch
jj rebase -r <first-remaining-commit> -d main
jj git push --all
The * mark in jj log indicates bookmarks that need pushing.
Editing Earlier Commits
Need to modify a commit that has descendants?
jj edit <commit-id>
jj edit @
jj git push --all
Splitting Commits
When a commit has changes that should be separate:
jj split
JJ_EDITOR=true jj split -m "First commit message" file1.rs file2.rs
jj describe -m "Second commit message"
Undo
Made a mistake? jj tracks all operations:
jj undo
jj op log
jj op restore <id>
Common Mistakes
Working in a Described Commit
Wrong:
jj describe -m "Feature A"
Right:
jj describe -m "Feature A"
jj new
jj squash
Forgetting --allow-new on First Push
jj git push --bookmark main --allow-new
After the bookmark exists on the remote, you don't need this flag.
Pushing a New Commit as a New PR
Wrong:
jj new
jj git push -c @
Right:
jj new
jj squash
jj git push