Git → Jujutsu, step by step
If you know Git well, jj will feel familiar in some places and upside-down in others. This tutorial walks the path one workflow at a time, always showing what you’d do in Git next to what you do in jj — and, where it helps, the equivalent move in wyrm.
You don’t need to memorize new commands up front. Start with the mental shift, then follow the phases in order.
The core mental shift
Section titled “The core mental shift”A handful of ideas explain almost every difference you’ll hit:
- There is no staging area. Every file change is immediately part of the current commit.
- The working copy is a commit — marked
@in the log. Every jj command auto-snapshots your files into it. - Commits are cheap and disposable. You don’t squash to “clean up before pushing”; you squash when you want to, and jj handles the cascade.
- Rewriting history is the normal workflow, not a dangerous escape hatch.
- Anonymous branches are normal. Git’s scary “detached HEAD” is just everyday jj.
- Bookmarks are shipping labels, not workspaces. You don’t work “on a bookmark” — you work on commits, then label one when it’s time to push.
In wyrm, the top row of the log graph is @. As you edit files, that row updates live — there’s no separate “uncommitted changes” panel to manage.
Step 1 — Starting work
Section titled “Step 1 — Starting work”In Git you create a branch, edit, stage, then commit. In jj you just start editing.
In Git you…
# 1. You're on developgit checkout -b feature/new-ui # branch BEFORE you work# 2. Edit files (now in a dirty, unsaved state)# 3. Stage and commitgit add src/ui/*.ts && git commit -m "feat: new ui"In jj you…
# 1. You're on develop (same)# 2. Just start editing. That's it.# No branch. No staging. Every save is captured in @.# 3. Name the work when you're ready:jj describe -m "feat: new ui"# 4. (Optional, only when pushing) add a bookmark:jj bookmark create feature/new-uiYour work is safe the moment you save a file. No branch is needed until you push, and there’s no “staging” — everything lives in @.
Step 2 — Resuming old work
Section titled “Step 2 — Resuming old work”You have an old commit with good changes, but develop has moved 20 commits ahead. How do you catch up?
The trick: order doesn’t matter. Rebase first or edit first — jj is fine either way, and conflicts won’t block you.
Option A — rebase first, edit after (most common):
# Move your old commit onto the latest develop:jj rebase -r old_change_id -d develop
# Then continue working on it:jj edit old_change_id# Now @ IS that commit; edits amend it directly.# The change ID stays the same even after the rebase.Option B — edit first, rebase later:
jj edit old_change_id # jump to it right now# ...work...jj rebase -r @ -d develop # rebase when you're readyIn Git this is “checkout → rebase → resolve conflicts → continue”, and a conflict freezes you mid-rebase. In jj you can rebase, see a conflict, go do something else, and come back to resolve later — the conflict is just a state the commit is in.
Step 3 — Immutable commits
Section titled “Step 3 — Immutable commits”Double-clicked an old commit and got “Revision is immutable”? In Git you can check out anywhere, so this feels like jj getting in your way. It isn’t.
jj protects commits that are already public — shared with the world:
| Immutable (◆) | Mutable (●) |
|---|---|
Commits on main@origin | Your working copy @ |
Commits on any *@origin | Local-only commits |
| Tagged commits | Anything you haven’t pushed |
Why it’s a good thing: in Git you can git push --force over main or amend a shared commit and wreck history others depend on — nothing stops you. jj says “this is already public; you can’t rewrite what the team relies on.” That kills an entire category of “oh no” moments.
What to do instead — don’t edit the immutable commit, build a new one on top:
jj new immutable_change_idYour new @ carries all the files from that commit; you edit freely, and the original is untouched.
| Mutable commit (●) | Immutable commit (◆) |
|---|---|
jj edit change_id | jj new change_id |
@ moves to that commit | Creates a new child commit |
| Edits amend it in place | @ is the new child |
Quick reference: new vs edit vs describe vs commit
Section titled “Quick reference: new vs edit vs describe vs commit”| Command | What it does | Closest Git move | When to use |
|---|---|---|---|
jj new | New empty child commit; move @ there | git checkout -b | ”Done with this, on to the next thing.” |
jj edit | Move @ onto an existing mutable commit | git rebase -i → edit | Go back to fix an earlier commit. |
jj describe | Set/change @’s message | git commit --amend -m | Name what you’re working on. |
jj commit | describe + new in one | git commit -m | Finish this work and start the next. |
Step 4 — Partial commits & cleanup
Section titled “Step 4 — Partial commits & cleanup”You edited 10 files. Only 7 belong in this commit; 3 are test junk. Some files even mix real changes with throwaway lines.
Git is opt-in: you stage what you want and discard the rest. jj is opt-out: everything is already in @, so you remove what you don’t want. Same destination, opposite direction.
Method 1 — jj restore (discard whole files):
# Revert these files to the parent's version:jj restore test-setup.ts dev-nav-hack.svelte .env.test# @ now has only the 7 real files. Name it:jj describe -m "fix: navigation and auth"jj restore is the equivalent of “Discard Changes” — it resets a file to its parent.
Method 2 — jj split (keep some, separate the rest):
jj split # pick what goes into the FIRST commit# Result:# ○ @ — leftover (3 test files)# ● fix: navigation and auth (7 real files)# ◆ developjj bookmark create nav-fix -r @-jj git push --bookmark nav-fixjj abandon @ # throw away the test-junk commitMixed files (hunk level) — when one file has 10 real lines and 5 test hacks:
jj split -iThis opens an interactive diff of every changed hunk; you choose which hunks land in the first commit and which stay in the second. It’s jj’s git add -p.
Test junk spread across several commits? You don’t need one split per commit:
# Option A — squash everything, then split once:jj squash --from "commit1::commit5" --into commit1jj split -i -r that_commitjj abandon junk_commit# Option B — jj absorb:# 1. In @, simply UNDO the test changes (delete the files,# remove the test lines). jj snapshots those "undo" edits.# 2. Route them home automatically:jj absorbjj absorb looks at each undo hunk in @, finds the ancestor commit that added those lines, and folds the fix into that commit — no interactive rebase, one command. More on it in Step 7.
Step 5 — Squash, rebase & conflicts
Section titled “Step 5 — Squash, rebase & conflicts”Squash
Section titled “Squash”In Git you…
git rebase -i HEAD~5# Editor opens; change "pick" → "squash" on 4 lines,# write the combined message, save.In jj you…
# One at a time, into the parent:jj squash -r commit5jj squash -r commit4
# …or a whole range in one command:jj squash --from "commit1::commit5" --into commit1Descendants auto-rebase after every squash. No editor, no --continue, no ceremony.
Rebase
Section titled “Rebase”In Git you…
git checkout feature/new-uigit fetch origingit rebase origin/develop# On conflict: STOP, fix files, git add ., git rebase --continue# …repeated for EACH conflicting commit.In jj you…
jj git fetchjj rebase -r your_change -d develop# On conflict: nothing stops. The commit is just marked CONFLICT.jj edit your_change # fix the markers when you're ready — that's it.Which commits move depends on the flag:
| Flag | What it moves | Git analogue |
|---|---|---|
-r (revision) | Only this one commit; children stay put | (no direct equivalent) |
-s (source) | This commit and all its descendants | git rebase --onto |
-b (branch) | Everything since it diverged from the destination | git rebase develop |
Conflicts — the biggest difference
Section titled “Conflicts — the biggest difference”Resolving one is just editing a file:
jj edit conflicted_change# Open the file. jj's markers show the base plus what each# side intended, not two opaque full versions:## <<<<<<< Conflict 1 of 1# %%%%%%% diff from base to side #1# context# -old line# +your change# +++++++ side #2# their change# >>>>>>> Conflict 1 of 1 ends## Fix it, save. jj snapshots. Resolved. No git add. No --continue.Because the markers describe intent (base + each diff) rather than two final blobs, conflicts are usually easier to reason about. See Conflict resolution for the wyrm workflow.
Putting Steps 1–5 together
Section titled “Putting Steps 1–5 together”A complete feature, end to end:
- Start working —
jj describe -m "feat: new navigation UI" - Snapshot milestones as you go —
jj commit -m "wip: nav routing done" - Clean up into one commit —
jj squash --from "first::last" --into first - Drop test junk —
jj restore test-setup.ts(orjj split -i) - Rebase onto latest develop —
jj git fetch && jj rebase -r feature -d develop - Label and push —
jj bookmark create feature/x && jj git push --bookmark feature/x --allow-new - Open the PR, get review
- Address feedback —
jj edit feature_change→ make changes →jj git push --bookmark feature/x
No fixup commits, no second squash. Edit the commit, push — jj force-pushes safely on its own.
Step 6 — Bookmarks
Section titled “Step 6 — Bookmarks”A jj bookmark is not a Git branch. A Git branch follows HEAD: commit, and it moves with you — you’re always “on” a branch. A jj bookmark is a sticky note on a specific commit. It does not move when you make new commits, and you’re never “on” it. You’re always on @.
Git: write the address on the box before packing it. jj: pack the box, then stick a shipping label on when you’re ready to send.
So you work first and label later. Three common situations when an old bookmark already exists:
A — the old work is obsolete (your new commits replace it):
jj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesign # safe force-pushB — keep the old work and build on it:
jj rebase -s commit1 -d feature/nav-redesign # your commits on topjj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesignC — merge old + new work (different files, want both):
jj squash --from old_work_change_id --into commit1jj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesign| Behavior | Git branch | jj bookmark |
|---|---|---|
| Moves on commit? | Yes — auto-follows HEAD | No — stays put; move it manually |
| Required to work? | Yes — always “on” one | No — work without any |
| Required to push? | Yes | Yes — a push needs a name |
| ”Current” one? | Yes — HEAD points to it | No concept; @ is always current |
| Follows a rebase? | Sometimes | Yes — tracks the change ID through rewrites |
| Detached-HEAD fear? | Yes | None — that’s just normal |
| Force push | Manual, scary | Automatic, safe (with-lease) |
Because bookmarks track the change ID (not the commit SHA), they survive rewrites: after a rebase you just push, no manual force-push needed to update the remote.
Step 7 — Push, fetch & the PR cycle
Section titled “Step 7 — Push, fetch & the PR cycle”Push — one verb, not four
Section titled “Push — one verb, not four”Git has several push shapes (-u first time, plain push, --force-with-lease, --delete). jj funnels them through one command:
jj git push --bookmark my-thing --allow-new # first pushjj git push --bookmark my-thing # every push afterjj git push --all # all local bookmarksjj git push --delete old-bookmark # delete a remote bookmarkThere is no --force flag. jj always behaves like --force-with-lease: if someone pushed to that bookmark since your last fetch, it refuses and tells you to fetch first. You literally cannot clobber a teammate’s work by accident.
Fetch — never a surprise
Section titled “Fetch — never a surprise”jj git fetch # downloads remote state; does NOT merge, # rebase, or touch your working copy.# Then YOU decide:jj rebase -d main@origin # rebase your work onto the update — or don't.There is no jj pull. Fetching and rebasing are always separate, so you’ll never get an unexpected merge commit.
The review loop
Section titled “The review loop”Address feedback by editing the commit in place and pushing again — no fixup commits:
jj edit feature_change# ...make the requested changes...jj git push --bookmark feature/x # auto force-push, safelyStep 8 — Power moves
Section titled “Step 8 — Power moves”A few jj capabilities that have no clean Git equivalent.
jj absorb — auto-route fixes to the right commits
Section titled “jj absorb — auto-route fixes to the right commits”Reviewer says “line 42 in commit 2 is wrong, and line 88 in commit 4 has a typo.” In Git that’s a git commit --fixup per spot plus git rebase -i --autosquash.
# 1. In @, just fix BOTH lines wherever they live now.# jj snapshots; @ holds both fixes.# 2. Route them home:jj absorbjj finds, for each changed line, the most recent ancestor that touched it, moves the fix there, and rebases the descendants. Both bugs land in the right commits and @ is empty again.
No stash — every state is already a commit
Section titled “No stash — every state is already a commit”Boss says “drop everything, hotfix this.” In Git that’s git stash / branch / fix / git stash pop and a prayer.
jj new develop -m "hotfix: prod is down"# Your previous work is still there as a sibling commit.# Fix, push, then return:jj edit feature_a_id # right back where you left offThere’s no stash because there’s nothing to stash — your work was never uncommitted.
Stacked PRs — automatic restacking
Section titled “Stacked PRs — automatic restacking”Ship four small, reviewable PRs that build on each other instead of one giant one:
● commit 4 ← bookmark feature/tests● commit 3 ← bookmark feature/ui● commit 2 ← bookmark feature/api● commit 1 ← bookmark feature/auth-refactor◆ developIn Git, fixing a bug in the base PR means rebasing and force-pushing every PR above it. In jj, edit the base commit, let all descendants auto-rebase, and update every PR at once:
jj edit feature/auth-refactor # fix the base# ...descendants auto-rebase...jj git push --all # all four PRs update togetherjj ↔ git cheat-sheet
Section titled “jj ↔ git cheat-sheet”Work lifecycle
| Goal | jj | git |
|---|---|---|
| Status | jj status | git status |
| Log | jj log | git log --graph |
| Diff current work | jj diff | git diff HEAD |
| Start new commit | jj new | git checkout -b |
| Set commit message | jj describe -m "…" | git commit --amend -m |
| Commit and move on | jj commit -m "…" | git commit -m |
Editing commits
| Goal | jj | git |
|---|---|---|
| Edit an old mutable commit | jj edit <rev> | git rebase -i (edit) |
| Child of any commit | jj new <rev> | git checkout -b |
| Fold into parent | jj squash | fixup + autosquash |
| Interactive fold | jj squash -i | git add -p + amend |
| Split a commit | jj split | git rebase -i (split) |
| Auto-route fixes | jj absorb | (none) |
| Discard a commit | jj abandon | git reset --hard HEAD~1 |
| Restore a file | jj restore <file> | git checkout HEAD -- <file> |
Remote operations
| Goal | jj | git |
|---|---|---|
| Fetch | jj git fetch | git fetch |
| Push | jj git push --bookmark <name> | git push |
| First push | jj git push --bookmark <name> --allow-new | git push -u |
| Force push (auto) | jj git push --bookmark <name> | git push --force-with-lease |
| Push all | jj git push --all | git push --all |
| Delete remote | jj git push --delete <name> | git push --delete |
Bookmarks
| Goal | jj | git |
|---|---|---|
| Create | jj bookmark create <name> | git branch <name> |
| Move | jj bookmark set <name> -r <rev> | git branch -f <name> |
| Delete | jj bookmark delete <name> | git branch -d <name> |
| List | jj bookmark list | git branch -a |
Safety & selectors
| Goal | jj | git |
|---|---|---|
| Undo anything | jj undo | git reflog + reset |
| Operation history | jj op log | git reflog |
| ”Stash” work | jj new @- then jj edit back | git stash |
| Revset | Meaning |
|---|---|
@ | Working copy |
@- | Parent of the working copy |
trunk() | Main branch on origin |
trunk()..@ | Your local work not yet in trunk |
mine() | Your own commits |
conflicts() | All conflicted commits |
bookmarks() | All bookmarked commits |
description("fix") | Commits with “fix” in the message |
Next steps
Section titled “Next steps”- jj Basics — change IDs, the working copy, and the operation log in depth.
- Log Graph — navigate and filter history in wyrm.
- Conflict Resolution — resolve conflicts visually with your merge tool.
- Push Preview — review exactly what will land on the remote before you push.