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 usually mutable - jj prevents rewriting
immutable()(by default, root and the ancestors of trunk, tags, and untracked remote bookmarks).--ignore-immutableoverrides that check except for root. Mutable pushed commits can be rewritten;jj git pushapplies lease-like safety checks. Conflicts don't block operations - they're stored in commits. -
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
Inspect the current change before running a command that moves or records it:
jj status # or jj st
jj log # view commit graph
jj diff --summary # inventory changed paths
jj diff # review the full working-copy diff
Default handoff for completed implementation work
Follow explicit user and repository conventions. When a user asks you to change or build something, treat recording the completed, agent-owned diff and advancing to an empty @ as part of delivery, even if they did not separately ask for a commit. Use this finish-and-advance handoff when the current diff belongs entirely to the task and no other workflow is established:
# Start from the requested base only when the current @ is clean
jj new <base> # for example: jj new main
# Make and test changes, then review them
jj diff --summary
jj diff
# Record all current changes and leave a fresh empty @
jj commit -m "feat: describe the completed change"
Without filesets or -i, jj commit is equivalent to jj describe followed by jj new: the completed change becomes @-, and @ is a fresh empty child. Do not finish this pattern with only jj describe; it updates metadata but leaves @ on the completed change. Use jj describe alone when intentionally continuing to edit that change or changing metadata without advancing.
This completion default includes simple edit-and-show requests. It does not apply to review, diagnosis, or inspection-only work. Do not commit if the user asks to leave changes uncommitted, repository instructions establish another workflow, or @ includes pre-existing or unrelated changes. A partial jj commit <filesets> leaves unselected changes in the new @, so it intentionally does not produce an empty handoff.
If @ is already an empty change at the desired base, reuse it instead of stacking another empty change. Do not run jj new <base> over unexplained dirty work: jj snapshots that work into the old @, which may become a sibling rather than @-.
Common operations
# Name the current change without advancing
jj describe -m "commit message"
# Create a new empty change on top of current
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"
Choose a workflow
The finish-and-advance pattern is a useful default for bounded agent work, not a universal jj rule. Preserve the user's preference and repository conventions.
Finish-and-advance workflow: Start or reuse an empty change → work directly in it → use a full jj commit -m for an owned, bounded implementation diff. This ends on a new empty @.
Squash workflow (index-like): Describe a target change → create an empty child → work in the child → jj squash into the 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>' -o '<destination>'
# Rebase commit and all descendants
jj rebase -s '<source>' -o '<destination>'
# Rebase entire branch (all commits reachable from <branch> but not from <destination>)
jj rebase -b '<branch>' -o '<destination>'
Examples:
# Move current commit onto main
jj rebase -r @ -o main
# Move a feature branch onto latest trunk
jj rebase -s feature-start -o '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 xx & y- Both x and yx | y- Either x or yx ~ 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 a full jj commit without filesets or -i, your working copy (@) is a fresh empty commit on top of the completed change (@-). A partial commit instead leaves the unselected changes in @. When creating bookmarks for PRs after a full commit:
- 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 and tags, including new ones; it does not push unreferenced commits. Plain jj git push selects eligible tracked bookmarks and tags. 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 (a tracked local bookmark syncs on fetch)
jj rebase -o main
# Or rebase a specific branch
jj rebase -b my-feature -o 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
Use message flags instead of opening editors:
jj commit -m "message"to finish-and-advance an owned implementation diffjj describe -m "message"to update metadata without advancingjj new -m "message"to start a named change
Avoid -i (interactive) flags:
- Do NOT use
jj squash -i(interactive selection) - Do NOT use
jj split -i - Do NOT use
jj absorb -i
References
For detailed information on specific operations, see the reference files in references/:
- workflows.md - Finish-and-advance, Squash, and Edit workflows; anonymous branches; multi-parent merges
- cli-options.md - Understanding
-r,-s,-o,-A,-B,--from,--toflag patterns - bookmarks.md - Bookmarks, tags, 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, run, bisect, next/prev
- revsets.md - Common revsets reference with operators, functions, and patterns