🗣️ this is a useful guide to help you memorize + better understand how Jujutsu works, but it's not a replacement for learning the mental model or deeper strategies!
- No staging area — all changes in the working copy are automatically part of the current “change”
- Changes vs commits — jj tracks “changes” (mutable) that become “commits” (immutable) when pushed
@— a symbol pointing to the current working copy change (likeHEAD, but for changes)@-— the parent of@- Change ID — stable identifier that survives amends; commit ID changes on every edit
| Git | jj |
|---|---|
git pull |
jj git fetch + jj rebase -d main@origin |
git commit -a |
jj commit |
git commit --amend |
jj describe -m "msg" or jj squash |
git stash |
jj new (WIP stays in @-) |
git checkout -b |
jj new -B <name> |
git add -p |
(automatic — use jj split to separate) |
git reset --soft HEAD~1 |
jj edit @- (modify parent directly) |
git reset --hard HEAD~1 |
jj abandon |
jj describe -m "feat: my change"
jj bookmark set main -r @
jj git pushjj new main -B new-feature # new change + bookmark
# work, jj describe as you go
jj git push -b new-feature # push to remoteTo push more changes later:
jj new
jj describe -m "more work"
jj bookmark set new-feature -r @ # move bookmark forward
jj git push -b new-featurejj bookmark create new-feature -r @ # bookmark current change
jj git push -b new-feature # push it
# keep working, jj bookmark set to advancejj new main new-feature # create merge commit with both parents
jj bookmark set main -r @ # advance main to the merge
jj git push -b mainOr rebase (linear history):
jj rebase -r new-feature -d main # rebase onto main
jj bookmark set main -r new-feature
jj git push -b main| Git | jj |
|---|---|
git fetch |
jj git fetch |
git pull |
jj git fetch then jj rebase -d main@origin |
git pull --rebase |
Same as above — jj rebases by default |
| Git | jj |
|---|---|
git push |
jj git push |
git push -u origin branch |
jj git push -b branch |
git push --force |
jj git push (force is automatic for rewritten changes) |
Note: You need a bookmark (jj’s term for branch) to push. Create one with jj bookmark create <name>.
| Git | jj |
|---|---|
git commit --amend |
jj squash (squashes @ into @-) |
git commit --amend -m "new msg" |
jj describe -m "new msg" on the target change |
git add -p && git commit --amend |
Just edit files — they’re auto-included in @, then jj squash |
You can also amend any change directly:
jj edit <change-id> # switch to that change
# make edits
jj new # create a new change on top when donejj calls branches “bookmarks” — they’re just pointers to changes.
| Git | jj |
|---|---|
git branch <name> |
jj bookmark create <name> |
git checkout -b <name> |
jj new -B <name> (create change + bookmark) |
git branch -d <name> |
jj bookmark delete <name> |
git branch -m old new |
jj bookmark rename old new |
git checkout <branch> |
jj new <bookmark> |
List bookmarks: jj bookmark list
Key difference: Bookmarks don’t auto-advance. Use jj bookmark set <name> -r @ to move them forward.
| Git | jj |
|---|---|
git remote add origin <url> |
jj git remote add origin <url> |
git remote add upstream <url> |
jj git remote add upstream <url> |
git remote -v |
jj git remote list |
jj git push --remote fork -b my-featurejj git fetch --remote upstream
jj rebase -d main@upstreamjj bookmark track main@upstreamjj calls worktrees “workspaces”.
| Git | jj |
|---|---|
git worktree add ../path branch |
jj workspace add ../path |
git worktree list |
jj workspace list |
git worktree remove ../path |
jj workspace forget (from that workspace) |
Switch which change a workspace is editing:
jj workspace update-stale # sync after changes in another workspacejj doesn’t need stashing — the working copy is always a change. But equivalent workflows:
| Git | jj |
|---|---|
git stash |
jj new (your WIP stays in @-) |
git stash pop |
jj squash --from <wip-change> |
git stash list |
Just use jj log — nothing special about “stashed” changes |
To temporarily set aside work:
jj new # current work becomes @-, you get empty @
jj edit @- # go back to it laterUnlike git rebase -i, jj doesn’t have a single interactive command. Each operation is atomic:
| Task | jj |
|---|---|
| Squash into parent | jj squash (squashes @ into @-) |
| Squash specific change | jj squash --from xyz (into its parent) |
| Squash into arbitrary target | jj squash --from xyz --into abc |
| Split a change | jj split (interactive) |
| Reorder changes | jj rebase -r <change> -d <new-parent> |
| Edit old change in-place | jj edit <change>, make edits, jj new |
| Reword message | jj describe -m "new message" |
| Abandon (delete) a change | jj abandon <change> |
| Git | jj | What it does |
|---|---|---|
git reset --soft HEAD~1 |
jj edit @- |
Go back to parent, modify it directly |
git reset --hard HEAD~1 |
jj abandon |
Throw away the change entirely |
git reset --mixed HEAD~1 |
(no equiv) | jj has no staging area |
| Git | jj |
|---|---|
git bisect start |
jj bisect start |
git bisect good <ref> |
jj bisect good or jj bisect good <change> |
git bisect bad <ref> |
jj bisect bad or jj bisect bad <change> |
git bisect reset |
jj bisect reset |
git bisect skip |
jj bisect skip |
Typical flow:
jj bisect start
jj bisect bad @ # current is broken
jj bisect good xyz # xyz was working
# jj checks out the midpoint, test, then:
jj bisect good # or bad
# repeat until found
jj bisect reset| Task | Command |
|---|---|
| See history | jj log |
| Last N ancestors | jj log -r 'ancestors(@, 10)' |
| All bookmarked changes | jj log -r 'bookmarks()' |
| Status | jj status or jj st |
| Diff working copy | jj diff |
| Diff specific change | jj diff -r <change> |
| Show a change | jj show <change> |
| Undo last operation | jj undo |
| Operation log | jj op log |
jj commit=jj describe+jj new— set message and start next changejj describe— set/update message, keep working in same change
Use jj describe while working, jj commit when you’re done and moving on.
# Add to .zshrc
source <(jj util completion zsh)Or for oh-my-zsh, generate to a file:
jj util completion zsh > ~/.oh-my-zsh/completions/_jj