The Core Workflow

Lesson 3 · jj for Git Users · ~10 minutes

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.

The Basic Loop

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:

  1. Edit files — they're instantly part of the current commit (no add, no staging)
  2. Describe — set or update the commit message whenever you want
  3. New — when you're done, create a fresh commit and start the next change

That's it. Those three actions replace git's edit → add → commit cycle entirely.

┌─────────────────────────────────────────┐ │ The jj Loop │ │ │ │ Edit files ──→ jj describe ──→ jj new │ │ ↑ │ │ │ └────────────────────────────┘ │ │ │ │ (Your edits auto-amend the current │ │ commit. No staging. No add.) │ └─────────────────────────────────────────┘

jj new — Start a New Change

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
Think of jj new as "I'm done with this change"

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

jj describe — Set the Commit Message

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
Messages are always mutable

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 — See What Changed

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

Git

# 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

jj

# 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 — The Commit Graph

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()

Reading the Symbols

SymbolMeaning
@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)
Customizing jj log

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.

Introduction to Revsets

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:

RevsetMeaningGit equivalent
@The working-copy commitHEAD (sort of)
@-Parent of working copyHEAD~1
@--Grandparent of working copyHEAD~2
trunk()Main branch tiporigin/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
Revsets vs. git's rev syntax

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 — The Shortcut

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.

Git

# 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"

jj

# 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"

The Full Git vs. jj Contrast

Here's a typical development session side by side:

Git

# 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

jj

# 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.

Don't forget: jj describe can happen any time

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.

Putting It Into Practice

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

Check Your Understanding

You edit a file in a jj repo without running any jj command. What happens?
Correct — in jj there is no staging area or add command. Every file change automatically amends the working-copy commit. The next time you run any jj command, it snapshots the working directory into the current commit.
Not quite. In jj there is no staging area or add command. Every file change automatically amends the working-copy commit immediately. No explicit action is needed.
What does jj new do?
Right — 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.
Not quite. 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.
How do you see the diff of a specific past commit?
Exactly — the -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.
Not quite. The 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.
Primary Source

Steve Klabnik's Jujutsu Tutorial — Real World Workflows — walks through a realistic development session showing how the core commands compose.

Questions? Want to explore revsets further? Confused about when to use jj commit vs. the two-step flow? Wondering how this works with multiple parallel changes? Ask me anything.
← Prev Next →