The daily loop: edit, describe, new. Plus diff, log, and your first revsets. This lesson covers the commands you'll use 90% of the time.
In git, a commit is an action you take when you're ready. In jj, a commit is a place you're currently editing. This one shift simplifies everything. Your daily workflow becomes:
That's it. Those three actions replace git's edit → add → commit cycle entirely.
When you run jj new, jj creates a new empty commit on top of your current working-copy commit. Your working copy now points to this new commit. Any edits you make from here go into it.
$ jj new
Working copy now at: ksrmwuon 4a8b3c2d (empty) (no description set)
Parent commit : yostqsxw 9f1e7a3b refactor auth module
In git you run git commit to seal a change. In jj the change is already committed — jj new just says "that one's finished, give me a fresh canvas." The previous commit stays exactly as you left it.
You can also create a new commit on top of a specific revision:
$ jj new @-- # new commit on top of grandparent
$ jj new ksrmwuon # new commit on top of a specific change ID
In git, you write your commit message at commit time — you'd better know what the change is about before you make it. In jj, the commit already exists. You can describe it at any point: before you start editing, while you're in the middle, or three commits later when you realize you forgot.
$ jj describe -m "refactor auth module to use JWT tokens"
Without -m, it opens your editor (just like git commit without -m):
$ jj describe # opens $EDITOR
You can also describe any other commit by specifying a revision:
$ jj describe -r @- -m "fix typo in README" # describe the parent
$ jj describe -r abc123 -m "better message" # describe by change ID
There is no --amend flag because description is always a rewrite. Every jj describe replaces the commit message. Want to add to it? Edit the full text in your editor. This works the same whether the commit is your working copy or ten commits back.
jj diff shows what changed in the working-copy commit relative to its parent — roughly equivalent to git diff HEAD (but remember, there's no staging area to confuse things).
$ jj diff
Modified regular file src/auth.rs:
1 1: use std::collections::HashMap;
...
42 : - let token = create_token(user);
42: + let token = create_jwt(user, &config.secret);
To see the diff of a different commit, use -r:
$ jj diff -r @- # what did the parent commit change?
$ jj diff -r @-- # what did the grandparent change?
$ jj diff -r abc123 # diff of a specific change ID
# Unstaged changes
$ git diff
# Staged changes
$ git diff --cached
# All changes vs last commit
$ git diff HEAD
# What a past commit changed
$ git show abc123
$ git diff abc123~1..abc123
# Working copy changes (the only kind)
$ jj diff
# What a specific commit changed
$ jj diff -r @-
$ jj diff -r abc123
# That's it. No staged/unstaged split.
jj log shows your commit history as a graph. It's opinionated about what to show — by default you see only your own commits and their relationship to trunk, not the entire repository history.
$ jj log
@ ksrmwuon you@example.com 2026-07-01 15:32
│ (empty) (no description set)
○ yostqsxw you@example.com 2026-07-01 15:30
│ refactor auth module to use JWT tokens
○ vputmzrl you@example.com 2026-07-01 14:15
│ add rate limiting middleware
│ ○ nwlqzpkx you@example.com 2026-06-30 09:00
├─╯ fix typo in README
◆ zzzzzzzz root()
| Symbol | Meaning |
|---|---|
@ | The working-copy commit (where your edits go) |
○ | A regular mutable commit |
◆ | An immutable commit (already pushed, or the root) |
│ | Parent-child relationship (line connecting commits) |
├─╯ | A branch merging back into the main line |
trunk() | Marks the main branch tip (usually main@origin) |
To see more history: jj log -r 'all()'. To see just a linear stack: jj log -r '::@'. The -r flag accepts revset expressions, which we cover next.
Revsets are jj's query language for selecting commits. Think of them like CSS selectors for your history. You've already seen the most common ones:
| Revset | Meaning | Git equivalent |
|---|---|---|
@ | The working-copy commit | HEAD (sort of) |
@- | Parent of working copy | HEAD~1 |
@-- | Grandparent of working copy | HEAD~2 |
trunk() | Main branch tip | origin/main |
You can use revsets anywhere a revision is expected:
$ jj diff -r @- # diff of parent
$ jj new @-- # new commit on top of grandparent
$ jj log -r '@-::' # parent and all its descendants
$ jj log -r 'trunk()..@' # commits between trunk and working copy
Git uses HEAD~3, HEAD^2, branch-name, commit hashes, and reflog entries — all different syntax for different things. jj unifies all of this into one composable expression language. We'll cover more advanced revsets in later lessons.
jj commit is syntactic sugar for jj describe + jj new in one step. If you know the message and you're ready to move on:
$ jj commit -m "add rate limiting middleware"
# Equivalent to:
# jj describe -m "add rate limiting middleware"
# jj new
This is the closest analog to git commit -am "message". Many jj users prefer the two-step approach (describe separately, then new) because it lets you refine the message independently, but jj commit is there when you want speed.
# The full ceremony
$ vim src/auth.rs
$ git add src/auth.rs
$ git commit -m "refactor auth"
# Or with -a to skip staging
$ vim src/auth.rs
$ git commit -am "refactor auth"
# Two-step (more common)
$ vim src/auth.rs
$ jj describe -m "refactor auth"
$ jj new
# One-step shortcut
$ vim src/auth.rs
$ jj commit -m "refactor auth"
Here's a typical development session side by side:
# Start work
$ git checkout -b feat/auth
# Make first change
$ vim src/auth.rs
$ git add src/auth.rs
$ git commit -m "refactor auth"
# Make second change
$ vim src/config.rs
$ git add src/config.rs
$ git commit -m "add JWT config"
# Oh wait, first commit needs a fix
$ git stash
$ git rebase -i HEAD~2
# mark "edit", fix, add, continue
$ git stash pop
# Start work (no branch needed)
$ jj new
# Make first change
$ vim src/auth.rs
$ jj describe -m "refactor auth"
$ jj new
# Make second change
$ vim src/config.rs
$ jj describe -m "add JWT config"
# Oh wait, first commit needs a fix
$ jj edit @- # go back to it
$ vim src/auth.rs # fix auto-amends
$ jj new # back to top
Notice what's absent from the jj side: no add, no stash, no interactive rebase, no branch creation for local work. The commands directly express your intent.
A common mistake for git converts is trying to compose the perfect message before starting work. In jj, start editing immediately. Describe when you understand what you've built. Redescribe if you change your mind. The message is just metadata on an existing commit.
Open the repo you cloned in Lesson 2 and try this sequence:
# 1. Check where you are
$ jj log
# 2. Make any edit (add a comment, create a file, anything)
$ echo "# Notes" > NOTES.md
# 3. See it's already committed
$ jj diff
$ jj log # @ now shows your change
# 4. Describe what you did
$ jj describe -m "add notes file"
$ jj log # message appears on @
# 5. Start next change
$ jj new
$ jj log # new empty @ on top
# 6. Check the diff of your previous commit
$ jj diff -r @- # shows the notes file you added
jj new do?jj new creates a fresh empty commit on top of your current one. It doesn't discard anything or create a branch — your previous commit stays exactly as it was, and you get a clean canvas for the next change.jj new creates a fresh empty commit on top of your current one. It doesn't discard anything or create a branch — your previous commit stays exactly as it was.-r flag on jj diff lets you specify any revision. For example, jj diff -r @-- shows what the grandparent commit changed, and jj diff -r abc123 works with change IDs.jj diff -r <revision> command shows what a specific commit changed. For example, jj diff -r @-- for the grandparent, or jj diff -r abc123 for a specific change ID.Steve Klabnik's Jujutsu Tutorial — Real World Workflows — walks through a realistic development session showing how the core commands compose.
jj commit vs. the two-step flow? Wondering how this works with multiple parallel changes? Ask me anything.