This is the payoff lesson. Everything from Lessons 1–3 — the working copy as a commit, auto-rebase, conflicts as data — exists to make history rewriting safe and routine. In git, rewriting history is an advanced operation you do carefully. In jj, it's the normal workflow.
You'll learn six operations that cover every history rewriting scenario: jumping into past commits, splitting, squashing, reordering, and automatically distributing fixups. Each one is dramatically simpler than its git equivalent.
jj edit — Jump Into Any CommitThe most powerful command. jj edit moves your working copy into an existing commit so you can modify it directly. Any changes you make amend that commit in place, and all descendants are automatically rebased on top.
# You have a stack: A ← B ← C ← D (@)
# You realize commit B has a typo.
$ jj edit B # @ moves to B
# Fix the typo in your editor — it auto-amends into B
$ jj new # Create a fresh commit, returning to the top of your stack
That's it. No interactive rebase. No marking commits as "edit". No --continue. No praying for clean conflict resolution at each step.
# Fix commit B in a stack of A ← B ← C ← D:
$ git rebase -i B~1
# Editor opens — mark B as "edit"
# Save and close
$ # Make the fix
$ git add -A
$ git commit --amend --no-edit
$ git rebase --continue
# ⚠️ Conflict at C? Resolve, git add, continue.
# ⚠️ Conflict at D? Resolve again.
# Finally done (or gave up and ran --abort)
# Fix commit B in a stack of A ← B ← C ← D:
$ jj edit B
# Make the fix (auto-amended into B)
$ jj new
# Done. Descendants rebased automatically.
# If conflicts: stored in commits as data.
# Resolve whenever you want — nothing blocks.
jj new creates a fresh empty commit on top of the current one — "start something new." jj edit moves your working copy into an existing commit — "go back and fix something." They are complementary: you edit to jump somewhere, then new to leave.
jj split — Break One Commit Into TwoYou made a commit that does two things and want to separate them. jj split breaks the current commit into two sequential commits. You choose what goes in the first; everything else goes in the second.
# Current commit has changes to auth.rs AND readme.md.
# You want those as separate commits.
$ jj split
# Interactive editor opens — select which changes go in the FIRST commit.
# Everything else automatically goes into the SECOND commit.
You can also split by file path without the interactive editor:
# Put auth.rs changes first, readme.md changes second:
$ jj split auth.rs
# Split the most recent commit:
$ git reset HEAD~1
$ git add auth.rs
$ git commit -m "auth changes"
$ git add readme.md
$ git commit -m "docs update"
# Split a commit 3 back? Buckle up:
$ git rebase -i HEAD~3
# Mark as "edit" in editor
$ git reset HEAD~1
$ git add auth.rs && git commit -m "part 1"
$ git add readme.md && git commit -m "part 2"
$ git rebase --continue
# Hope nothing conflicts...
# Split any commit — same ease regardless of position:
$ jj edit <change-id> # jump to it (skip if already there)
$ jj split # interactively choose
$ jj new # return to top
# Or split by file path:
$ jj split auth.rs
# Split the current commit in one step:
$ jj split
Because jj edit lets you jump to any commit, you can combine it with jj split to break apart any commit in your history. Descendants always auto-rebase. The position of the commit in your stack doesn't affect the difficulty.
jj squash — Move Changes Into Parentjj squash moves all changes from the current commit into its parent. If the current commit becomes empty afterward, it's automatically discarded. This is like git commit --amend, but far more flexible.
# You have: A ← B ← WIP-fix (@)
# You want the WIP-fix changes folded into B.
$ jj squash
# Changes from @ move into B.
# If @ is now empty, it vanishes. Done.
Don't want to squash everything? Use -i to interactively select which hunks to move into the parent:
$ jj squash -i
# Interactive diff editor opens.
# Select hunks to move into parent.
# Unselected hunks stay in the current commit.
This is where jj goes far beyond git. You can squash changes into any ancestor commit, not just the immediate parent:
# Stack: A ← B ← C ← D (@)
# Move some changes from D directly into B:
$ jj squash --into B
# Changes from @ move into B. C and D are auto-rebased.
# Move changes into an ancestor commit:
$ git stash
$ git rebase -i <ancestor>~1
# Mark ancestor as "edit" in editor
$ git stash pop
$ git add -A
$ git commit --amend --no-edit
$ git rebase --continue
# Resolve any conflicts along the way...
# Move changes into any ancestor:
$ jj squash --into <change-id>
# Done. One command. Auto-rebase handles the rest.
jj rebaseNeed to change the order of commits in a stack? jj rebase has three key flags for different scenarios:
# Stack: A ← B ← C ← D (@)
# You want C to come after D instead.
$ jj rebase -r C -A D
# -r: the single revision to move
# -A: place it After this revision
You can also move a commit and all its descendants together with -s (source):
# Move commit C and everything after it onto a different base:
$ jj rebase -s C -d A
# Now: A ← C' ← D' (B is orphaned from this branch)
-r moves a single revision, re-parenting its children to its former parent. -s moves a revision and all its descendants together as a subtree. -d sets the destination parent. -A inserts after a specific revision in an existing chain.
# Reorder commits in a stack:
$ git rebase -i HEAD~4
# Editor opens with all 4 commits listed.
# Manually rearrange the lines.
# Save and close.
# ⚠️ Conflicts possible at each step.
# Resolve, add, continue for each one.
# Reorder commits in a stack:
$ jj rebase -r C -A D
# One command. Immediate. Auto-rebase.
# Or move a subtree:
$ jj rebase -s C -d main
# Moves C and all descendants onto main.
Every operation above — edit, split, squash, rebase — modifies commits that have descendants. In git, this is where things get terrifying. In jj, every history rewrite immediately and automatically propagates to all descendants.
The key insight: if conflicts arise during auto-rebase, they're stored as data in the affected commits. Your operation always succeeds. You're never stuck in a half-finished rebase state with no clear way out.
# Rewriting commit 5 back in a stack:
$ git rebase -i HEAD~5
# ⚠️ Conflict at commit 3!
# Must resolve NOW or abort everything.
$ git add . && git rebase --continue
# ⚠️ Another conflict at commit 4!
# Resolve again...
$ git add . && git rebase --continue
# Finally done. Or gave up: git rebase --abort
# (losing all progress)
# Rewriting commit 5 back in a stack:
$ jj edit <change-id>
# Make changes. Done immediately.
# If descendants conflict:
$ jj log # Shows which commits have conflicts
# Fix them whenever you want, in any order.
# No blocked state. No --abort. No lost work.
When auto-rebase creates conflicts, jj marks those commits clearly in jj log (you'll see "conflict" markers). You still need to resolve them before pushing. The difference: you choose when to resolve, and you can continue working on other commits in the meantime. You're never forced to stop everything mid-operation.
jj absorb — Auto-Distribute Fixupsjj absorb is the cherry on top. You have uncommitted changes in your working copy that are clearly fixups to specific earlier commits. Instead of manually figuring out which commit each fix belongs to, absorb does it automatically.
# You've made small fixes to 3 files.
# Each fix touches lines introduced by different prior commits.
$ jj absorb
# jj analyzes each changed line, finds which commit introduced it,
# and squashes each hunk into the appropriate ancestor commit.
# All descendants auto-rebase.
Think of it as an automatic git commit --fixup + git rebase --autosquash, but in a single command with no manual commit creation or rebase step.
# Fix belongs in commit abc123:
$ git add file.rs
$ git commit --fixup=abc123
# Repeat for each fix...
$ git commit --fixup=def456
$ git add other.rs
$ git commit --fixup=abc123
# Now autosquash them all:
$ git rebase -i --autosquash HEAD~5
# Hope no conflicts...
# All fixes are in your working copy:
$ jj absorb
# Done. Each hunk sent to the right commit.
# No fixup commits. No rebase step.
Use jj absorb when you've accumulated small fixes in your working copy that logically belong in different prior commits. It's most useful when preparing a stack for code review — you address feedback, then absorb sends each fix to the right place automatically.
Here's a realistic workflow showing these commands in sequence — preparing a feature stack for review:
# Your feature stack:
$ jj log --short
@ vrukpmsm (empty) working copy
○ zsqolypn add API endpoint
○ tpylkqrs add database migration
○ kwmxnslp add data model
# Reviewer says: "Split the API endpoint commit — tests are mixed in."
$ jj edit zsqolypn
$ jj split tests/
$ jj describe -m "add API tests"
$ jj new
# Reviewer says: "The model commit has a typo in a field name."
$ jj edit kwmxnslp
# Fix the typo
$ jj new
# You notice two small fixes that belong in different commits:
$ jj absorb
# Each fix sent to the right ancestor automatically.
# All done. Every descendant auto-rebased after each operation.
# Force-push to update the PR:
$ jj git push
jj edit and jj new?jj edit makes an existing commit your working copy — changes amend it in place. jj new creates a fresh empty commit on top of wherever you are. They're complementary: edit to jump back, new to move forward.jj edit moves your working copy into an existing commit so any changes amend it. jj new creates a fresh empty commit on top of the current one. Think: "edit = go back and fix," "new = start something fresh."jj split opens an interactive editor where you select what goes in the first commit. Everything you don't select automatically becomes the second commit. You can also pass file paths directly: jj split file1.rs file2.rs.jj split is the dedicated command for this. It opens an interactive selector (or accepts file paths as arguments) to choose what goes in the first commit. The rest goes in the second. No rebase or undo needed.jj edit modifies a commit mid-stack, what happens to its descendants?Official jj Tutorial — Moving content between commits — covers edit, split, squash, and absorb with additional examples and edge cases.