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. For substantial work that the agent owns from start to finish, committing the completed change and advancing to a new empty @ is part of delivery, even when the user does not explicitly ask for a commit. This default does not apply to small requested edits, simple edit-and-show requests, or inspection-only work. 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 does not apply to small requested edits, simple edit-and-show requests, 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