jj
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:
# No need for 'git add' - changes are tracked automatically
jj status # Shows working copy state
jj diff # Shows changes in working copy commit
When Snapshots Are Triggered
The working copy is snapshotted into the current commit (@) when running most jj commands. Key triggers:
jj new- Creates new commit, snapshots working copy into parentjj status- Triggers snapshot before showing statusjj diff,jj log,jj describe- All trigger snapshot first
Forcing a snapshot manually:
# If you edited files but need to ensure they're committed:
jj new # Snapshot into parent, create new @
jj abandon @ # Remove empty new commit if not needed
# Alternative: describe triggers snapshot
jj describe -m "updated" # Snapshot and update description
Important: When you jj edit a commit and make changes, those appear as "working copy changes" until a snapshot is triggered. This is normal behavior.
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.
No Staging Area
Instead of staging, use these patterns:
jj split- Split working copy into multiple commitsjj 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 # Succeeds even with conflicts
jj log # Shows conflicted commits with ×
jj new <conflicted> # Work on top of conflict
# Edit files to resolve, then:
jj squash # Move resolution into parent
Operation Log
Every operation is recorded and can be undone:
jj op log # View operation history
jj undo # Undo last operation
jj op restore <op-id> # Restore to specific operation
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 |
Common Workflows
Starting a New Change
# Working copy changes are auto-committed
# When ready to start fresh work:
jj new # Create new commit on top
jj describe -m "message" # Set description
# Or combine:
jj new -m "Start feature X"
Editing a Previous Commit
# Option 1: Edit in place
jj edit <change-id> # Make working copy edit that commit
# Make changes, they're auto-committed
jj new # Return to working on new changes
# Option 2: Squash changes into parent
jj squash # Move all changes to parent
jj squash -i # Interactively select changes
jj squash <file> # Move specific file
Rebasing Commits
# Rebase current branch onto main
jj rebase -d main
# Rebase specific revision and descendants
jj rebase -s <rev> -d <destination>
# Rebase only specific revisions (not descendants)
jj rebase -r <rev> -d <destination>
# Insert commit between others
jj rebase -r X -A Y # Insert X after Y
jj rebase -r X -B Y # Insert X before Y
Working with Bookmarks (Branches)
jj bookmark list # List bookmarks
jj bookmark create <name> # Create at current commit
jj bookmark set <name> # Move bookmark to current commit
jj bookmark delete <name> # Delete bookmark
jj bookmark track <name>@<remote> # Track remote bookmark
Bookmark gotchas:
# Moving backwards requires a flag:
jj bookmark set feature -r <ancestor> # FAILS if ancestor
jj bookmark set feature -r <ancestor> --allow-backwards # Works
# The * suffix means bookmark diverged from tracked remote:
# feature* 123abc ← Push to sync with remote
jj git push --bookmark feature
# Create vs Set:
jj bookmark create feature # FAILS if feature@origin exists
jj bookmark set feature -r @ # Works, moves existing bookmark
Pushing Changes
# Push specific bookmark
jj git push --bookmark <name>
# Push change by creating auto-named bookmark
jj git push --change <change-id>
# Push all bookmarks
jj git push --all
Resolving Conflicts
# After a rebase creates conflicts:
jj log # Find conflicted commit (marked with ×)
jj new <conflicted> # Create commit on top
# Edit files to resolve conflicts
jj squash # Move resolution into conflicted commit
# Or use external merge tool:
jj resolve # Opens merge tool for each conflict
jj resolve --list # List all conflicted files
Resolving Binary File Conflicts
Binary files (images, .wasm, compiled files) cannot have conflict markers. Resolve by choosing one version:
# Take version from specific revision (e.g., main):
jj restore --from main path/to/binary.wasm
# Take version from feature branch:
jj restore --from feature path/to/binary.wasm
# For multiple binary files:
jj resolve --list # See all conflicted files
for file in file1.wasm file2.wasm; do
jj restore --from main "path/to/$file"
done
Multi-Parent (Merge) Conflict Resolution
When a merge commit has conflicts:
# Option 1: Work on child of merge
jj new <conflicted-merge> # Create child of merge
# Edit files to resolve
jj squash # Move resolutions into merge
# Option 2: Edit the merge directly
jj edit <conflicted-merge> # Edit the merge itself
# Make changes - they appear as "working copy changes"
jj new # Snapshot changes into merge
jj abandon @ # Remove empty temp commit
Creating multi-parent merges:
jj new branch-a branch-b branch-c -m "integration: merge features"
Undoing Mistakes
jj undo # Undo last operation
jj op log # View operation history
jj op restore <op-id> # Restore to specific state
# View repo at past operation
jj --at-op=<op-id> log
Revsets Quick Reference
Revsets select commits using a functional language:
| 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 |
description(text) |
Commits with matching description |
Examples:
jj log -r '@::' # Working copy and descendants
jj log -r 'trunk()..@' # Commits between trunk and working copy
jj log -r 'mine() & ::@' # My commits in working copy ancestry
jj rebase -s 'roots(trunk()..@)' -d trunk() # Rebase branch onto 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> # Creates colocated repo (default)
jj git clone --no-colocate <url> # Non-colocated (jj only)
Using Git Commands
In colocated repos, Git changes are auto-imported. For non-colocated:
jj git import # Import changes from Git
jj git export # Export changes to Git
Converting Existing Git Repo
cd existing-git-repo
jj git init --colocate # Add jj to existing Git repo
Colocated Mode Deep Dive
In colocated mode, both jj and Git operate on the same repository. This creates some nuances to understand:
Understanding git status output:
$ git status
HEAD detached from 82f30e2c
nothing to commit, working tree clean
The "detached from X" message shows the original detachment point, not current HEAD. To verify actual HEAD position:
git log --oneline -1 HEAD # Shows current HEAD
Git index sync issues:
After jj conflict resolution, git may show unmerged paths:
$ git status
Unmerged paths:
both modified: Cargo.lock
Fix by updating the git index:
git add <files> # Clears unmerged entries
When git and jj disagree:
jj git import # Force import git state to jj
jj git export # Force export jj state to git
Best practice: Primarily use jj commands in colocated repos. Only use git for operations jj doesn't support (like interactive rebase with git add -p style workflows).
Configuration
Edit config with jj config edit --user:
[user]
name = "Your Name"
email = "your@email.com"
[ui]
default-command = "log" # Run 'jj log' when no command given
diff-editor = ":builtin" # For interactive diff editing (split, squash -i)
[revset-aliases]
'wip' = 'description(exact:"") & mine()' # Custom revset alias
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.
Advanced Topics
For comprehensive documentation, see:
- references/revsets.md - Complete revset reference
- references/commands.md - Full command reference
- references/git-comparison.md - Git to jj command mapping
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 # Shows divergent commits
jj abandon <unwanted> # Remove one version
Immutable commit error - Can't modify trunk/tagged commits by default:
jj --ignore-immutable <command> # Override protection
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
# Use destination's message (discard source)
jj squash --use-destination-message # or -u
# Provide explicit 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
# Use built-in tools instead of external merge tool
jj resolve --tool :ours <file> # Take "our" version (side #1)
jj resolve --tool :theirs <file> # Take "their" version (side #2)
# Or use restore for complete replacement
jj restore --from <rev> <file> # Take file from specific revision
Inherently Interactive Commands
These commands cannot be made non-interactive:
jj split(without file arguments) - requires diff selectionjj diffedit- opens diff editor by designjj resolve(without--tool) - opens merge tool
Workaround for split: Provide file paths to avoid interactive selection:
jj split -m "First commit" src/file1.rs src/file2.rs
Common Pitfalls
Push Flag Combinations
Some jj git push flag combinations don't work together:
| Flags | Works? | Notes |
|---|---|---|
--all |
✓ | Pushes all bookmarks |
--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 |
Working Copy Changes on Merge Commits
When you jj edit a merge commit, changes appear as "working copy changes" even if you're resolving conflicts. This is expected - use jj new to trigger snapshot:
jj edit <merge-commit> # Edit the merge
# Make changes...
jj new # Snapshot into merge, create new @
jj abandon @ # Remove empty commit
Git Status Shows Detached HEAD
In colocated repos, git status shows "HEAD detached from X" - this is normal. The message shows the original detachment point. Check actual HEAD with:
git log --oneline -1 HEAD # Current HEAD position
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.
More from dashed/claude-marketplace
playwright
Browser automation with Playwright for Python. Use when testing websites, taking screenshots, filling forms, scraping web content, or automating browser interactions. Triggers on browser, web testing, screenshots, selenium, puppeteer, or playwright.
22tmux
Remote control tmux sessions for interactive CLIs (python, gdb, git add -p, etc.) by sending keystrokes and scraping pane output. Use when debugging applications, running interactive REPLs (Python, gdb, ipdb, psql, mysql, node), automating terminal workflows, interactive git commands (git add -p, git stash -p, git rebase -i), or when user mentions tmux, debugging, or interactive shells.
11ai-friendly-cli
Build and refactor CLIs for AI agent compatibility. Use when making command-line interfaces machine-readable, adding structured JSON output, hardening inputs against hallucinations, implementing safety rails like dry-run flags, adding schema introspection, or designing multi-surface architectures (CLI + MCP).
3zellij
Terminal workspace and multiplexer for interactive CLI sessions. Use when managing terminal sessions, running interactive REPLs, debugging applications, automating terminal workflows, or when user mentions zellij, terminal multiplexer, floating panes, or session layouts. Simpler alternative to tmux with native session management.
2chrome-cdp
Interact with local Chrome browser session via Chrome DevTools Protocol. Use when asked to inspect, debug, or interact with a page open in Chrome, take screenshots of browser tabs, read accessibility trees, evaluate JavaScript, click elements, navigate pages, or automate browser interactions in a live Chrome session.
2mermaid-cli
Generate, validate, and fix diagrams from Mermaid markup using the mermaid-cli (mmdc) tool. Use when creating flowcharts, sequence diagrams, class diagrams, state diagrams, ER diagrams, Gantt charts, pie charts, mindmaps, or any Mermaid-supported diagram type. Also use when validating, verifying, or fixing Mermaid diagram syntax. Triggers on mentions of mermaid, mmdc, diagram generation, diagram validation, or converting .mmd files to images/SVG/PDF.
2