jj (Jujutsu) Version Control
jj is a Git-compatible VCS with a different mental model. Most LLMs are trained primarily on git, so this skill provides the correct jj approach.
Critical differences from git
- •
Working copy is always a commit - Every file change automatically amends the current working copy commit. There is no staging area, no
git add. - •
Change ID vs Commit ID - Every commit has two identifiers:
- •Change ID (e.g.,
kntqzsqt): Stable across rewrites, use this in commands - •Commit ID (e.g.,
d7439b06): Changes when commit is modified (like git's SHA)
- •Change ID (e.g.,
- •
No staging area - Files are automatically tracked. Use
.gitignore(jj uses git's ignore format) andjj file untrack <path>to untrack. - •
Commits are mutable - You can freely rewrite any commit. Rewriting pushed commits requires force push (
jj git pushhandles this automatically with lease protection). Conflicts don't block operations - they're stored in the commit. - •
Bookmarks, not branches - jj uses "bookmarks" instead of git branches. They map 1:1 to git branches when pushing/fetching.
- •
Colocated repos - When
.jjand.gitcoexist, every jj command auto-syncs with git. Git stays in detached HEAD state. Tools likeghCLI work normally.
Core workflow
# See current state jj status # or jj st jj log # view commit graph jj diff # see working copy changes # Describe current commit (set message) jj describe -m "commit message" # Create new commit on top of current (signals "I'm done editing this commit") jj new jj new -m "message for the new commit" # Insert a commit BEFORE the current one (children auto-rebase) jj new -B @ -m "insert before current" # Squash working copy changes into parent jj squash # Squash into a specific ancestor (not just parent) jj squash --into <change-id> # Edit an existing commit (makes it the working copy) jj edit <change-id> # Auto-distribute working copy changes into the right commits in a stack jj absorb # Create a merge commit (multiple parents) jj new branch1 branch2 -m "merge branches"
Two mental models: Squash vs Edit workflow
Squash workflow (index-like): Describe commit → create empty child → work in child → jj squash into parent. Familiar if you liked git's staging area.
Edit workflow (direct): Work directly in commits → use jj new -B @ to insert commits before → use jj next --edit to navigate. More natural for stack-based development.
See workflows.md for detailed patterns.
Stack workflow with jj absorb
When working on a stack of commits, jj absorb automatically moves each change to the commit where that line was last modified:
# You have a stack and notice bugs in earlier commits # Make fixes in working copy, then: jj absorb # Each fix is moved to the appropriate commit in the stack # Review what happened: jj op show -p
Revsets (selecting commits)
Revsets are expressions for selecting commits. Use change IDs, not commit IDs.
| Revset | Meaning |
|---|---|
@ | Working copy commit |
@- | Parent of working copy |
@-- | Grandparent |
root() | Root commit |
bookmarks() | All bookmarked commits |
trunk() | Main branch (usually main@origin) |
::foo | Ancestors of foo (inclusive) |
foo:: | Descendants of foo (inclusive) |
foo::bar | DAG range (ancestry path) |
foo..bar | Range (like git's) |
foo- | Parents of foo |
foo+ | Children of foo |
foo | bar | Union |
foo & bar | Intersection |
~foo | Complement (not foo) |
Rebase
Use the direct flags, not longwinded approaches:
# Rebase single commit to new destination jj rebase -r '<revision>' -d '<destination>' # Rebase commit and all descendants jj rebase -s '<source>' -d '<destination>' # Rebase entire branch (all commits reachable from <branch> but not from <destination>) jj rebase -b '<branch>' -d '<destination>'
Examples:
# Move current commit onto main jj rebase -r @ -d main # Move a feature branch onto latest trunk jj rebase -s feature-start -d trunk()
Filesets (selecting files)
Filesets are expressions for selecting files. Quote file names containing special characters like (), [], ~, &, |, or whitespace.
| Pattern | Meaning |
|---|---|
"path" | Prefix match (file or directory, default) |
file:"path" | Exact file path only |
glob:"*.rs" | Glob pattern (cwd-relative) |
root:"path" | Workspace-relative prefix |
root-glob:"**/*.rs" | Workspace-relative glob |
Operators:
- •
~x- Everything except x - •
x & y- Both x and y - •
x | y- Either x or y - •
x ~ y- x but not y
Examples:
# Diff excluding a file jj diff '~Cargo.lock' # Files with special characters MUST be quoted jj diff '"src/foo[1].txt"' jj diff '"path with spaces/file.rs"' # Glob patterns jj diff 'glob:"**/*.test.ts"' # Split excluding certain files jj split '~glob:"**/*.generated.*"'
Bookmarks and pushing
Bookmarks are named pointers to commits (like git branches). They auto-move when commits are rewritten, but do not auto-move to new commits after jj new/jj commit (unlike git branches).
Understanding @ vs @- for bookmarks
After jj new or jj commit, your working copy (@) becomes an empty commit sitting on top of your actual changes (@-). When creating bookmarks for PRs:
- •Create bookmarks on
@-(the commit with your changes):jj bookmark create feat/foo -r @- - •Not on
@(the empty working copy)
If you accidentally create a bookmark on @ or try to push @ directly, you'll get errors like "No commits between main and @" because the working copy is empty.
# Create bookmark (typically on @- after jj commit leaves you on empty commit) jj bookmark create feat/foo -r @- # List bookmarks jj bookmark list # or jj b l # Move bookmark to different commit jj bookmark move feat/foo --to <revision> # Delete bookmark jj bookmark delete feat/foo # Push specific bookmark (safest for automation) jj git push --bookmark feat/foo # Push and auto-create bookmark from change ID jj git push -c @-
Shorthand: jj b = jj bookmark, subcommands have single-letter shortcuts (jj b c = jj bookmark create).
Note: jj git push --all pushes all bookmarks, not all commits. Use jj git push -c <change> to auto-create and push a bookmark.
Fetching and updating (no git pull)
There is no jj git pull. Instead, fetch and rebase separately:
# Fetch latest from remote jj git fetch # Update your work onto latest main (local bookmark syncs with remote on fetch) jj rebase -d main # Or rebase a specific branch jj rebase -b my-feature -d main # If starting fresh with no local changes, just create new commit on main jj new main
Undo and operation log
jj tracks all operations and allows easy undo:
jj undo # Undo last operation jj op log # View operation history jj op restore <op> # Restore to specific operation
Important flags for non-interactive use
Always use message flags instead of opening editors:
- •
jj describe -m "message"(notjj describewhich opens editor) - •
jj new -m "message" - •
jj commit -m "message"
Avoid -i (interactive) flags:
- •Do NOT use
jj squash -i(interactive selection) - •Do NOT use
jj split -i
References
For detailed information on specific operations, see the reference files in references/:
- •workflows.md - Squash vs Edit workflows, anonymous branches, multi-parent merges, simultaneous branch editing
- •cli-options.md - Understanding
-r,-s,-d,-A,-B,--from,--toflag patterns - •bookmarks.md - Bookmarks, remote operations, GitHub/GitLab workflows
- •multiple-remotes.md - Fork workflows, upstream integration, tracking configuration
- •troubleshooting.md - Debugging with
jj evolog, divergent changes, conflicted bookmarks, recovery patterns - •git-mapping.md - Git to jj command mapping table
- •advanced-commands.md - Power commands: absorb, revert, duplicate, bisect, next/prev
- •revsets.md - Complete revsets reference with all operators, functions, and patterns