name: jj description: Help with jujutsu (jj) version control — running jj commands, explaining workflows, migrating from git, using workspaces, megamerges, laundry-pile commit cleanup, writing revsets, resolving conflicts, or any jj-related task. Use when the user mentions jj, jujutsu, wants to migrate from git to jj, asks about workspaces, megamerges, or needs help with jj commands and workflows. #
Jujutsu (jj) Skill #
You are helping Kieran use jujutsu (jj), a Git-compatible VCS built for
safe, concurrent, agent-friendly workflows.
Core Concepts #
- Working copy is a commit. Denoted
@. Auto-amended on everyjjcommand. No staging area. No "dirty working copy" errors. - No "current branch." You're always on a commit. Bookmarks (jj's branches) are optional labels that auto-follow rewrites.
- Change ID (e.g.
kntqzsqt): stable across rewrites. Commit ID (e.g.d7439b06): changes on every rewrite. Prefer change IDs. - First-class conflicts. Commands never fail due to conflicts. Conflicts are data stored in commits and can be rebased through.
- Operation log. Every mutation recorded.
jj undoundoes the last op.jj op restore <op>restores to any point. - Revsets. Functional query language for selecting commits. Used with
-r. - Colocated repos. Default mode.
.jj/and.git/share a working copy. Auto-syncs on every command. Mixjjandgitcommands freely.
Workspaces #
Workspaces are jj's first-class equivalent of git worktree. Multiple
working directories backed by a single .jj/repo/ store, each checked
out to a different commit. Unlike git worktrees, jj workspaces are not
tied to branches — any number can share the same commit.
Important: Workspaces must be siblings to the main repo in the
filesystem, not children. Use ../feature-name, not ./feature-name.
Creating a workspace inside the repo nests a copy of the repo inside
itself and jj will warn.
Key commands:
jj new # always jj new BEFORE workspace add
jj workspace add ../<name> # create sibling workspace at ../<name>
jj workspace add ../<name> -r <rev> # check out <rev> in new workspace
jj workspace list # list all workspaces
jj workspace update-stale # refresh working copy if out of sync
jj workspace forget <name> # remove workspace from jj's tracking
# (does NOT delete files; use rm -rf after)
How workspaces work internally:
Each workspace has its own .jj/ directory symlinked to the shared
.jj/repo/ store, plus its own working_copy/ state. Commits are
globally visible across all workspaces — jj log in workspace A shows
the @ commit of workspace B. Rebase workspace A's commit from
workspace B and jj tracks that A's working copy is now stale,
watchable with jj workspace update-stale.
Why workspaces matter for AI agents:
- Auto-snapshotting. The agent's working copy IS a commit. Nothing
is ever lost — every
jjcommand snapshots before running. - Lock-free concurrency. Two agents in different workspaces never contend on lock files. jj 3-way-merges divergent operations.
- Universal undo.
jj undoin any workspace.jj op logshows every mutation across all workspaces. - Non-blocking conflicts. An agent in workspace A creates a conflict. The agent in workspace B keeps working. Resolution happens later, possibly in a third workspace.
Cleanup:
jj workspace forget <name> # detach from jj
rm -rf ../<name> # delete the directory
Practical walkthrough:
# Start with a repo
jj git init
echo "base" > a.txt && jj describe -m "base"
# Always jj new before creating a workspace — gives both
# workspaces a common parent
jj new
jj workspace add ../feature-b
# In default workspace:
echo "work from default" > b.txt
jj describe -m "Add file B on default"
# In feature-b workspace:
cd ../feature-b
echo "work from feature-b" > c.txt
jj describe -m "Add file C on feature-b"
Cross-workspace rebasing (no merge needed):
Workspace commits are globally visible and can be moved freely. To consolidate feature-b's work into default, just rebase from either workspace:
# In feature-b, rebase its commit(s) onto default's tip:
jj rebase -s feature-b@ -d default@
# Now default and feature-b share a linear tree.
# Catch default up:
cd ../main
jj edit feature-b@ # both workspaces now at same revision
This is the key insight: there's no merge workflow. You rebase commits across workspace boundaries and they land linearly. Conflicts become first-class data in the commit — resolve them later, from any workspace.
Colocated Repos & Git Interop #
Clone a remote repo #
jj git clone <URL>
cd <repo>
Colocated by default. All git branches become bookmarks.
Add jj to an existing git repo #
cd /path/to/existing/git/repo
jj git init
History, branches, and remotes preserved. Colocated in-place.
Key interop notes #
- jj puts git in detached HEAD. Run
git switch <branch>before running mutating git commands. - Every jj command auto-imports/exports git refs.
- Push:
jj git push --bookmark <name>orjj git push --change <id>. - Fetch:
jj git fetch. - Force-with-lease safety on push. Conflicted bookmarks cannot be pushed.
- No support for:
.gitattributes, hooks, submodules, LFS, annotated tag creation.
Command Reference #
# Status & Inspection
jj st # status
jj log # graph log
jj log -r ::@ # ancestors of working copy
jj diff # diff of working copy
jj diff -r <rev> # diff of a specific revision
jj show <rev> # commit details + diff
# Working Copy
jj describe -m "msg" # set description on @
jj new # new empty commit on top of @
jj new <rev> # new commit on top of <rev>
jj new x y z # merge commit with parents x, y, z
jj commit -m "msg" # describe @ then new child (shorthand)
jj edit <rev> # check out <rev> as @ (auto-amends)
jj abandon <rev> # delete a commit
# Restructuring
jj squash # squash @ into parent
jj squash -i # interactive: pick hunks to squash
jj squash --from X --into Y # move all changes from X into Y
jj squash --from X --into Y -i # interactive: pick hunks from X into Y
jj absorb # auto-distribute @ changes into ancestors
jj split # split @ into two commits interactively
jj rebase -s <src> -d <dst> # rebase src onto dst
jj rebase -r <rev> -d <dst> # rebase single revision onto dst
jj restore --from X --to Y # restore file content from X into Y
# Bookmarks
jj bookmark create <name> -r <rev>
jj bookmark set <name> -r <rev>
jj bookmark move <name> -r <rev>
jj bookmark delete <name>
jj bookmark list
jj tug # ALIAS (not builtin): move nearest bookmark to @
# requires two config entries:
# revset-aliases."closest_bookmark(to)" =
# "heads(::to & bookmarks())"
# aliases.tug = ["bookmark", "move", "--from",
# "closest_bookmark(@-)", "--to", "@-"]
# Remotes
jj git fetch [--remote <name>]
jj git push [--bookmark <name>] [--change <change-id>]
jj git import # resync jj from git refs
# History & Recovery
jj undo # undo last operation
jj op log # view operation log
jj op restore <op-id> # restore repo to past state
jj evolog # evolution history of current change
# Conflicts & Files
jj resolve # launch merge tool for conflicts
jj file untrack <file> # stop tracking (must be in .gitignore)
Revset Essentials #
@ working copy
@- parent(s) of working copy
x:: descendants of x (including x)
::x ancestors of x (including x)
x::y DAG range: descendants of x that are ancestors of y
x..y ancestors of y not ancestors of x (like git x..y)
~x complement (not in x)
x & y intersection
x | y union
x ~ y set difference
trunk() main/master on default remote
bookmarks() all local bookmark targets
mine() commits by current user
empty() commits with no file changes
conflicts() commits with unresolved conflicts
mutable() commits you can rewrite
immutable() commits protected from rewriting
heads(x) tips of set x
roots(x) roots of set x
latest(x, n) latest n commits by timestamp
description("pattern") filter by description
files("path") filter by modified files
String patterns: "glob", exact:"str", regex:"pat",
substring:"str". Append -i for case-insensitive.
Preferred Workflow: New, Then Squash #
Prefer jj new over jj edit for most work. Create a new commit,
make changes, then jj squash when ready. This is safer than editing
in place because you decide when to fold changes back. Jj auto-abandons
empty undescribed commits, so temporary experiments clean themselves up.
For WIP you'd previously stash, at minimum run jj describe -m "wip: ..."
so future-you has context.
Workflow: Megamerges #
A megamerge is an octopus merge commit (3+ parents) combining all your active branches. You work on top of it. You never push the megamerge itself, only the individual branches.
Why:
- Always work on the combined sum of all branches — if it compiles, everything integrates.
- Rare surprise merge conflicts since you resolve incrementally.
- Low-friction task switching via
jj edit <branch>. - Single rebase updates everything against trunk.
Creating:
jj new branch1 branch2 branch3 -m "megamerge"
Working across branches:
jj edit <branch> # jump to any branch tip, auto-amends
jj edit @ # back to megamerge
jj new <branch> # add a new commit to a branch
Adding/removing branches:
A single -d accepts a multi-revision revset, so use set operators on the
megamerge's current parents (<megamerge>-) instead of retyping every parent.
Validated on jj 0.41:
# Add a branch: union the new branch with the existing parents
jj rebase -s <megamerge> -d '<megamerge>- | <new-branch>'
# Remove a branch: subtract it from the existing parents
jj rebase -s <megamerge> -d '<megamerge>- ~ <old-branch>'
# Swap a branch: parenthesize the difference, then union the replacement
jj rebase -s <megamerge> -d '(<megamerge>- ~ <old-branch>) | <new-branch>'
Note: the old all: prefix modifier (e.g. all:<megamerge>-) was removed in
recent jj. A bare multi-revision revset in -d now works without it; passing
all: errors with ": is not an infix operator". If you ever need to force
multi-revision resolution explicitly, use the all() function.
Tip: run jj log -r '<megamerge>-' --no-graph first to see current parents
with their change IDs and bookmarks. After changing parents, update the
megamerge description with jj describe -r <megamerge> -m "..." to keep it
accurate.
Rebasing onto updated trunk:
jj rebase -s 'roots(main..@)' -d main
This rebases the root of each arm onto main and jj automatically drags all descendants (including the merge commit) along while preserving the multi-parent structure. No flattening, no manual per-branch rebasing.
Important: The megamerge is local-only. Push individual bookmarks. When a branch lands upstream, remove it from the megamerge and rebase.
Optional aliases (cut the ceremony):
These community aliases turn the multi-step megamerge chores into one word
each. Validated on jj 0.41. Add to ~/.config/jj/config.toml:
[revset-aliases]
# The nearest merge commit at or below `to` — i.e. your megamerge.
"closest_merge(to)" = "heads(::to & merges())"
[aliases]
# jj stack <revset> — splice a loose change in as a new megamerge parent.
stack = ["rebase", "--after", "trunk()", "--before", "closest_merge(@)", "--revision"]
# jj stage — pull the whole stack above the megamerge into it.
stage = ["stack", "closest_merge(@)+:: ~ empty()"]
# jj restack — rebase every arm onto the latest trunk, then tidy parents.
restack = ["rebase", "-o", "trunk()", "-s", "roots(trunk()..) & mutable()", "--simplify-parents"]
With these, jj restack replaces the manual jj rebase -s 'roots(main..@)' -d main invocation, and & mutable() keeps it from touching commits you
don't own. The add/remove-parent set-revset forms above stay handy for
one-off branch juggling.
Workflow: Laundry-Pile Commit Cleanup #
Don't try to make clean commits during development. Make a messy pile, then sort it at the end.
- Work freely. Make WIP commits without worrying about cleanliness.
- Identify logical categories (types, UI, tests, docs).
- Create empty placeholder commits:
jj new -B messy-first -m "types" jj new -A types -m "ui" jj new -A ui -m "tests" - Squash all messy work into one combined commit:
In a non-TTY context (nojj squash --from messy-first..messy-last --into messy-first$EDITOR), prefix withJJ_EDITOR=trueso the combined-description prompt doesn't block:JJ_EDITOR=true jj squash .... - Interactively distribute into placeholders:
jj squash -i --from messy-first --into types jj squash -i --from messy-first --into ui jj squash -i --from messy-first --into tests - Repeat until the combined commit is empty. Abandon it.
Why this beats jj split: With jj split, missing a hunk means
splitting again and squashing. The laundry approach lets you sort the
easiest hunks first without ordering worries. The final state is
guaranteed conflict-free.
Workflow: Conflict Resolution #
Conflicts in jj are data, not errors. Commands succeed even with conflicts.
jj log # find conflicted commit (marked with ×)
jj new <conflicted-rev> # create resolution commit on top
# Edit files to resolve, or:
jj resolve # launch external merge tool
jj squash # fold resolution back
Descendants of resolved commits automatically rebase. No --continue.
Tips for AI-Assisted jj Usage #
- Prefer change IDs over commit IDs — they're stable across rewrites.
jj log -r <revset>to inspect state before and after operations.jj stto check working copy state.- When unsure,
jj undois always safe. - Never push without explicit consent.
- In colocated repos, jj auto-syncs with git. If something goes wrong,
jj undohandles both sides. - Use
JJ_EDITOR=true jj squashto avoid editor prompts in non-TTY contexts. jj absorbto auto-distribute changes into correct ancestor commits.jj tug(a user alias, not a builtin — see Bookmarks section) to move the nearest bookmark forward after finishing work.jj file untrack <file>to stop tracking files (must be in.gitignorefirst).