Jujutsu (jj) Version Control
Jujutsu (jj) is a Git-compatible distributed version control system with a fundamentally better mental model. It treats the working copy as a commit, distinguishes changes from revisions, and provides first-class conflict handling.
Core Mental Model
Key Paradigm Shifts from Git
| Git Concept | jj Concept | Implication |
|-------------|------------|-------------|
| Staging area/index | None - working copy IS a commit | No jj add needed; use jj split for selective commits |
| Detached HEAD | Anonymous branches (default) | Work freely; create bookmarks only when sharing |
| Branches auto-advance | Bookmarks are static pointers | Must jj bookmark set before jj git push |
| Conflicts block work | Conflicts are first-class objects | Commit through conflicts, resolve later |
| Commit hashes only | Change IDs + commit hashes | Stable identifiers even as commits evolve |
The @ Symbol
@ always refers to the current working copy commit. Most commands operate on @ by default.
Essential Commands
Daily Workflow
# View status and log
jj status # Current state (alias: jj st)
jj log # Commit graph with smart defaults
jj diff # Changes in current working copy
jj diff -r <revset> # Changes in specific revision
# Working with changes
jj describe -m "message" # Set/update commit message (any revision with -r)
jj new # Create new empty change (signals "done with this")
jj commit -m "message" # Shorthand: describe + new
jj edit <id> # Move working copy to different change
History Manipulation
# Squash and move changes
jj squash # Move current changes into parent
jj squash -i # Interactive: select what to squash
jj move --from <id1> --to <id2> # Move changes between any commits
# Split commits
jj split # Break current commit into multiple (interactive)
jj split -r <id> # Split specific commit
# Rebase (always succeeds - conflicts become objects)
jj rebase -s <source> -d <dest> # Rebase commit and descendants
jj rebase -b @ -d main # Rebase current branch onto main
# Insert commits anywhere
jj new -A <id> # Insert after (--insert-after)
jj new -B <id> # Insert before (--insert-before)
# Remove commits
jj abandon # Discard commit, rebase children onto parent
Git Interoperability
# Setup (in existing Git repo)
jj git init --colocate # Creates .jj alongside .git; both work
# Remote operations
jj git fetch # Fetch from remotes
jj git push # Push tracked bookmarks
jj git push --allow-new # Push newly created bookmarks
# IMPORTANT: No jj git pull - explicitly fetch then rebase
jj git fetch && jj rebase -b @ -d main
Bookmark Management (Required for Pushing)
jj bookmark create <name> # Create bookmark at @ (or -r <id>)
jj bookmark set <name> # Move existing bookmark to @
jj bookmark list # Show all bookmarks
jj bookmark track <name>@<remote> # Start tracking remote bookmark
jj bookmark delete <name> # Delete locally and on push
Critical: Bookmarks don't auto-advance. Before pushing:
jj bookmark set feature-x # Move bookmark to current @
jj git push # Push the bookmark
Undo and Recovery
jj op log # All operations (more comprehensive than git reflog)
jj undo # Undo last operation
jj op restore --operation <id> # Restore to any previous state
jj evolog # Evolution of current change over time
Revset Quick Reference
Revsets are a functional language for selecting commits.
Basic Operators
| Operator | Meaning | Example |
|----------|---------|---------|
| @ | Working copy | jj log -r @ |
| @- | Parent of @ | jj diff -r @- |
| @-- | Grandparent | jj log -r @-- |
| ::x | Ancestors of x | jj log -r '::@' |
| x:: | Descendants of x | jj log -r 'main::' |
| x..y | Range (y not reachable from x) | jj log -r 'main..@' |
| \| | Union | jj log -r 'a \| b' |
| & | Intersection | jj log -r 'mine() & main..' |
| ~ | Difference | jj log -r 'all() ~ trunk()' |
Key Functions
| Function | Returns |
|----------|---------|
| trunk() | Main branch (auto-detects main/master) |
| bookmarks() | All bookmarked commits |
| remote_bookmarks() | Remote bookmarks |
| mine() | Commits by current user |
| heads(revset) | Commits with no children |
| roots(revset) | Commits with no parents in set |
| ancestors(revset) | All ancestors |
| descendants(revset) | All descendants |
Practical Revset Examples
# Work not yet pushed
jj log -r 'bookmarks() & ~remote_bookmarks()'
# My commits since branching from main
jj log -r 'mine() & main..@'
# Rebase all local branches onto updated main
jj rebase -s 'all:roots(trunk..@)' -d trunk
# Commits with conflicts
jj log -r 'conflict()'
# Empty commits (cleanup candidates)
jj log -r 'empty() & mine()'
Common Workflows
Starting New Feature
jj new -r main -m "feat: add feature X" # Branch from main with message
# ... make changes ...
jj new # Done with this, start next
Iterative Development (Squash Workflow)
# Work in @, make small changes
jj describe -m "WIP"
# ... edit code ...
jj squash # Move changes to parent
# Repeat until done
jj describe -m "feat: final message"
Rebasing onto Updated Main
jj git fetch
jj rebase -b @ -d main # Rebase current branch onto main
# If conflicts, resolve with jj resolve or edit directly
jj bookmark set feature-x
jj git push
Creating Pull Requests
# Ensure bookmark exists and is current
jj bookmark create pr-feature -r @ # Or: jj bookmark set pr-feature
jj git push --allow-new # --allow-new for new bookmarks
# Create PR via gh or web interface
Working with Conflicts
# Conflicts are committed, not blocking
jj rebase -s @ -d main # May create conflicts
jj log # Shows conflict markers in graph
# Continue working if needed
jj resolve # Interactive resolution when ready
# Or edit conflict markers directly and jj describe
Configuration Tips
Essential Config (~/.jjconfig.toml)
[user]
name = "Your Name"
email = "your@email.com"
[ui]
default-command = "log"
diff-editor = ":builtin" # Built-in TUI for split/squash -i
[revset-aliases]
'wip' = 'mine() & mutable() & ~empty()'
'stack' = 'trunk()..@'
Useful Aliases
[aliases]
# Move nearest ancestor bookmark to current commit
tug = ['bookmark', 'move', '--from', 'heads(::@- & bookmarks())', '--to', '@']
Common Pitfalls
Bookmark not advancing: Unlike Git branches, jj bookmarks don't auto-advance.
# Wrong assumption: bookmark follows after jj new
jj new
jj git push # ERROR: bookmark still at old commit
# Correct: explicitly set before push
jj bookmark set <name>
jj git push
Force push is normal: jj rewrites history freely. Expect force pushes.
No jj git pull: Intentional design. Always:
jj git fetch
jj rebase -b @ -d main
Progressive Context
- For advanced revsets and patterns: see
references/revsets.md - For stacked PR workflows: see
references/stacked-prs.md - For common workflow examples: see
examples/workflows.md
Scan to join WeChat group