| name | jj |
| description | Jujutsu (jj) version control system - a Git-compatible VCS with novel features. Use when working with jj repositories, managing stacked/dependent commits, needing automatic rebasing with first-class conflict handling, using revsets to select commits, or wanting enhanced Git workflows. Triggers on mentions of 'jj', 'jujutsu', change IDs, operation log, or jj-specific commands. |
Jujutsu (jj) Version Control System
Overview
Jujutsu is a powerful Git-compatible version control system that combines ideas from Git, Mercurial, Darcs, and adds novel features. It uses Git repositories as a storage backend, making it fully interoperable with existing Git tooling.
Key differentiators from Git:
- Working copy is automatically committed (no staging area)
- Conflicts can be committed and resolved later
- Automatic rebasing of descendants when commits change
- Operation log enables easy undo of any operation
- Revsets provide powerful commit selection
- Change IDs stay stable across rewrites (unlike commit hashes)
When to Use This Skill
- User mentions "jj", "jujutsu", or "jujutsu vcs"
- Working with stacked/dependent commits
- Questions about change IDs vs commit IDs
- Revset queries for selecting commits
- Conflict resolution workflows in jj
- Git interoperability with jj
- Operation log, undo, or redo operations
- History rewriting (squash, split, rebase, diffedit)
- Bookmark management (jj's equivalent of branches)
Key Concepts
Working Copy as a Commit
In jj, the working copy is always a commit. Changes are automatically snapshotted:
jj status
jj diff
When Snapshots Are Triggered
The working copy is snapshotted into @ when running most jj commands (new, status, diff, log, describe). Force a snapshot with jj util snapshot or just run any jj command.
Change ID vs Commit ID
- Change ID: Stable identifier that persists across rewrites (e.g.,
kntqzsqt)
- Commit ID: Hash that changes when commit is rewritten (e.g.,
5d39e19d)
Always prefer change IDs when referring to commits in commands.
Versioned access (0.37+): Use xyz/n suffix to access hidden/divergent versions:
xyz/0 - latest version of change xyz
xyz/1 - previous version (useful for jj restore --from xyz/1 --to xyz)
- Shown automatically in
jj log for divergent changes
No Staging Area
Instead of staging, use these patterns:
jj split - Split working copy into multiple commits
jj squash -i - Interactively move changes to parent
- Direct editing with
jj diffedit
First-Class Conflicts
Conflicts are recorded in commits, not blocking operations:
jj rebase -s X -d Y
jj log
jj new <conflicted>
jj squash
Operation Log
Every operation is recorded and can be undone:
jj op log
jj undo
jj redo
jj op revert <op-id>
jj op restore <op-id>
Essential Commands
| Command | Description | Git Equivalent |
|---|
jj git clone <url> | Clone a Git repository | git clone |
jj git init | Initialize new repo | git init |
jj status / jj st | Show working copy status | git status |
jj log | Show commit history | git log --graph |
jj diff | Show changes | git diff |
jj new | Create new empty commit | - |
jj describe / jj desc | Edit commit message | git commit --amend (msg only) |
jj edit <rev> | Edit existing commit | git checkout + amend |
jj squash | Move changes to parent | git commit --amend |
jj split | Split commit in two | git add -p + multiple commits |
jj rebase | Move commits | git rebase |
jj bookmark / jj b | Manage bookmarks | git branch |
jj git fetch | Fetch from remote | git fetch |
jj git push | Push to remote | git push |
jj undo | Undo last operation | git reflog + reset |
jj file annotate | Show line origins | git blame |
jj file search | Search file contents | git grep |
jj commit | Finalize WC commit + start new | git commit |
jj absorb | Auto-squash into right commits | git commit --fixup + autosquash |
jj evolog | History of a single change | git reflog (per-commit) |
jj next / jj prev | Navigate commit graph | git checkout HEAD~ |
jj interdiff | Compare diffs of two changes | - |
jj fix | Run formatters on commits | - |
jj arrange | TUI to reorder/abandon commits | git rebase -i (reorder) |
jj bookmark advance | Move bookmark forward | fast-forward branch |
Common Workflows
Starting a New Change
jj new
jj describe -m "message"
jj new -m "Start feature X"
Editing a Previous Commit
jj edit <change-id>
jj new
jj squash
jj squash -i
jj absorb
Rebasing Commits
jj rebase -d main
jj rebase -s <rev> -d <destination>
jj rebase -r <rev> -d <destination>
jj rebase -r X -A Y
jj rebase -r X -B Y
jj rebase -s <rev> -d <dest> --simplify-parents
Reordering Commits with jj arrange
jj arrange <revset>
The TUI shows selected commits with their immediate parents/children. Use swap up/down to reorder along graph edges.
Working with Bookmarks (Branches)
jj bookmark list
jj bookmark create <name>
jj bookmark set <name>
jj bookmark delete <name>
jj bookmark track <name> --remote <remote>
jj bookmark advance
Gotchas: Use --allow-backwards to move a bookmark to an ancestor. The * suffix in log means diverged from remote (push to sync). Use bookmark set (not create) if the bookmark may already exist on a remote.
Searching File Contents
jj file search <pattern>
jj file search --pattern "glob:*.rs"
jj file search --pattern "substring:foo"
jj file search -r <rev> <pattern>
Pushing Changes
jj git push --bookmark <name>
jj git push --change <change-id>
jj git push --all
jj git push --bookmark <name> --option key=value
Reorder a pushed/stacked branch: jj rebase --ignore-immutable -s <change> -d <dest> (descendants auto-rebase), then jj git push --bookmark <name> — jj git push is force-with-lease-safe by default (no flag). See references/github-workflow.md.
Resolving Conflicts
jj log
jj new <conflicted>
jj squash
jj resolve
jj resolve --list
Binary & Merge Conflict Resolution
Binary files cannot have conflict markers - resolve by choosing a version:
jj restore --from main path/to/binary.wasm
Multi-parent merge conflicts:
jj new <conflicted-merge>
jj squash
Creating multi-parent merges:
jj new branch-a branch-b branch-c -m "integration: merge features"
Undoing Mistakes
jj undo
jj redo
jj op log
jj op revert <op-id>
jj op restore <op-id>
jj --at-op=<op-id> log
jj --no-integrate-operation log -r 'trunk()..@'
Revsets Quick Reference
| Expression | Description |
|---|
@ | Working copy commit |
@- | Parent of working copy |
x- | Parents of x |
x+ | Children of x |
::x | Ancestors of x (inclusive) |
x:: | Descendants of x (inclusive) |
x..y | Ancestors of y not in ancestors of x |
x::y | Commits between x and y (DAG path) |
bookmarks() | All bookmark targets |
trunk() | Main branch (main/master) |
mine() | Commits by current user |
conflicts() | Commits with conflicts |
divergent() | Divergent changes |
description(text) | Commits with matching description |
diff_lines(text) | Commits with matching diff content |
remote_tags() | Remote tag targets |
Examples:
jj log -r '@::'
jj log -r 'trunk()..@'
jj log -r 'mine() & ::@'
jj rebase -s 'roots(trunk()..@)' -d trunk()
Git Interoperability
Colocated Repositories
By default, jj git clone and jj git init create colocated repos where both jj and git commands work:
jj git clone <url>
jj git clone --no-colocate <url>
Converting Existing Git Repo
cd existing-git-repo
jj git init --colocate
In colocated repos, Git changes are auto-imported. If git and jj disagree, use jj git import / jj git export. Best practice: primarily use jj commands.
Colocated repos: Git sees @ as uncommitted
Git's HEAD tracks @'s parent (@-), so @'s own changes look uncommitted to git. git checkout/git switch (and git-based tooling — gh, git-chain, hooks) can abort — but only when the target branch touches the same files as the @-vs-parent diff (error: Your local changes to the following files would be overwritten by checkout … Please commit your changes or stash them before you switch branches. Aborting); with no file overlap the checkout succeeds and carries the change over. Fix — park @ so the tree is fully committed first:
jj new <bookmark>
git checkout <bookmark>
Configuration
Edit config with jj config edit --user (or --repo for per-repo config, stored outside the repo since 0.38):
[user]
name = "Your Name"
email = "your@email.com"
[ui]
default-command = "log"
diff-editor = ":builtin"
[revset-aliases]
'wip' = 'description(exact:"") & mine()'
For automation/LLMs: Use -m flags instead of relying on editors. See Non-Interactive Workflows for patterns that work without user interaction.
See references/configuration.md for comprehensive configuration options including editor setup for interactive use.
Template Language
Customize output with -T/--template:
jj log -T 'change_id.short(8) ++ " " ++ description.first_line() ++ "\n"'
jj log -T 'if(conflict, "CONFLICT ", "") ++ description.first_line()'
Key: ++ concatenates, if(cond, then, else) conditionals, separate(sep, ...) joins non-empty. See references/templates.md.
Filesets
Select files in commands using fileset expressions:
jj diff 'glob:*.rs'
jj diff '~Cargo.lock'
jj split 'glob:**/test_*'
jj log -r 'files("src")'
See references/filesets.md.
Advanced Topics
For comprehensive documentation, see:
Troubleshooting
"Working copy is dirty" - Never happens in jj! Working copy is always a commit.
Conflicts after rebase - Normal in jj. Conflicts are recorded, resolve when convenient.
Lost commits - Use jj op log to find when commits existed, then jj op restore.
Divergent changes - Same change ID, different commits. Usually from concurrent edits:
jj log
jj diff --from xyz/0 --to xyz/1
jj abandon <unwanted>
jj restore --from xyz/1 --to xyz
Immutable commit error - Can't modify trunk(), tags, or untracked remote bookmarks (e.g. branches pushed via git/gh/git-chain, not jj git push) by default:
jj --ignore-immutable <command>
jj <command> --ignore-immutable
Non-Interactive Workflows
Many jj commands open an editor by default. Use these flags for automation and CLI workflows:
Commit Messages Without Editor
| Command | Non-Interactive Flag | Example |
|---|
jj describe | -m or --stdin | jj describe -m "Fix bug" |
jj commit | -m | jj commit -m "Add feature" |
jj new | -m | jj new -m "Start new work" |
jj squash | -m or -u | jj squash -u (use destination message) |
jj split | -m (first commit only) | jj split -m "First part" <files> |
Squash Without Editor
jj squash --use-destination-message
jj squash -m "Combined commit message"
Note: If either commit has an empty description, jj automatically uses the non-empty one without opening an editor.
Conflict Resolution Without Merge Tool
jj resolve --tool :ours <file>
jj resolve --tool :theirs <file>
jj restore --from <rev> <file>
Inherently Interactive Commands
These commands cannot be made non-interactive:
jj split (without file arguments) - requires diff selection (workaround: pass file paths, e.g. jj split -m "First commit" src/file1.rs)
jj diffedit - opens diff editor by design
jj resolve (without --tool) - opens merge tool
jj arrange - TUI by design
Common Pitfalls
Push Flag Combinations
Some jj git push flag combinations don't work together:
| Flags | Works? | Notes |
|---|
--all | ✓ | Pushes all bookmarks (skips ineligible) |
--tracked | ✓ | Pushes tracked bookmarks that changed |
--bookmark <name> | ✓ | Pushes specific bookmark |
--change <id> | ✓ | Creates/pushes auto-named bookmark |
--all --allow-new | ✗ | Incompatible |
--tracked --allow-new | ✗ | Incompatible |
--bookmark <name> --allow-new | ✓ | For new bookmarks |
--option key=value | ✓ | Pass push options to server |
Note (0.41+): --all, --tracked, and -r REVSETS no longer fail when revisions are private or have conflicts - ineligible bookmarks are silently skipped.
Working Copy Changes on Merge Commits
When you jj edit a merge commit, changes appear as "working copy changes" - this is expected. Run jj new to start a fresh commit on top. An empty, description-less, bookmark-less @ is auto-abandoned when you move off it (e.g. via jj new), so a manual jj abandon is usually unnecessary.
Bookmark Movement Refused
If jj bookmark set fails because it would move "backwards":
jj bookmark set name -r <rev> --allow-backwards
This flag is required when moving a bookmark to an ancestor of its current position.
Minimum Version Requirements
| Feature | Min Version |
|---|
xyz/n change offset syntax | 0.37+ |
jj file search, string patterns default to glob | 0.37+ |
divergent(), remote_tags(), diff_lines() revsets | 0.38+ |
git_web_url(), hyperlink() templates | 0.38+ |
jj arrange, jj bookmark advance | 0.39+ |
List methods (first, last, get, reverse, skip, take) | 0.39+ |
| Pattern aliases, fileset aliases | 0.39+ |
diff_lines_added(), diff_lines_removed() | 0.40+ |
replace() template, --no-integrate-operation | 0.41+ |