The jj Mental Model

Lesson 1 ยท Jujutsu (jj) ยท ~8 minutes

Git has a complex internal model that leaks through its interface: working directory, staging area (index), HEAD, branches, detached HEAD state, stashes, reflogs. Each is a distinct concept you must understand to use git effectively.

jj's thesis: most of that complexity is accidental, not essential. You can get a more powerful version control system by simplifying the model to just two things:

  1. Commits โ€” the only user-visible object
  2. Operations โ€” an undo-able log of everything you've done to the repo

That's it. Everything else git exposes as separate concepts (working copy, index, stashes, branches) is either eliminated or subsumed into commits.

๐Ÿ“– Primary Source

Git Comparison โ€” Official jj Docs
Read this alongside the lesson. It's concise and covers every point below with more detail.

What jj Removes

Git conceptjj equivalentWhy it's gone
Staging area (index) Eliminated The working copy is a commit. No intermediate step needed.
Stash Eliminated Every change is already a commit. Just jj new and come back later.
Detached HEAD Eliminated There's no HEAD to detach. jj tracks all visible commits by position in the DAG.
Branch (as required) Optional "bookmarks" Commits are never lost. You only need a bookmark when pushing to a remote.
Reflog Operation log Instead of per-ref history, jj tracks every operation atomically. Much more powerful.

The Three Key Shifts

1. The Working Copy Is a Commit

In git, your working directory is a separate world. You git add to stage, then git commit to create a commit. Three distinct states: working dir โ†’ index โ†’ commit.

In jj, the moment you change a file, the working-copy commit is automatically amended. There is no staging step. The working copy always is the latest commit.

Git

# Three steps to persist a change
$ echo "hello" > file.txt
$ git add file.txt
$ git commit -m "add file"

jj

# One step โ€” it's already committed
$ echo "hello" > file.txt
# Done. The working-copy commit now
# includes this change.
$ jj describe -m "add file"
Key insight

In jj, jj describe sets the message on a commit that already exists. In git, git commit -m creates a new commit. This means in jj you can write your commit message at any time โ€” before, during, or after making changes.

When you're done with a change and want to start the next one, you run jj new. This creates a fresh empty commit on top of your current one. Now your edits go into that new commit.

2. Automatic Rebasing

In git, if you amend or rebase a commit, its descendants are orphaned. You must manually rebase them. This is why git rebase -i is scary โ€” one wrong move and you're in conflict-resolution hell with dangling commits.

In jj, descendants are automatically rebased whenever you modify an ancestor. Modify a commit in the middle of a stack? All children get new hashes instantly, and if there are conflicts, they're recorded in the commit (not blocking your workflow).

Git: edit commit B manually         jj: edit commit B

A โ† B โ† C โ† D (HEAD)               A โ† B โ† C โ† D (@)
      โ†“                                    โ†“
A โ† B' ... C and D are orphaned!    A โ† B' โ† C' โ† D' (@)
      You must rebase C, D                 โ†‘ automatic!

3. Conflicts Are Data, Not Blockers

In git, a conflict halts your rebase. You must resolve it right now before you can continue. This makes experimentation risky.

In jj, a conflict is stored as part of the commit. The commit exists, it just has conflicted content. You can resolve it later, or not at all, and keep working on other things. The conflict doesn't block any operation.

Why this matters for history management

These three shifts combine to make rewriting history safe. You can freely edit any commit in your history โ€” change its content, split it, reorder it, squash it โ€” and jj will propagate those changes through descendants automatically, storing any resulting conflicts as data rather than aborting the operation.

Two Identifiers Per Commit

Every commit in jj has two IDs:

ID typeWhat it isWhen it changes
Commit ID (hash) SHA-like hash, same concept as git Every time the commit content changes (just like git)
Change ID Stable identifier for the logical change Never โ€” survives amends, rebases, rewrites

The change ID is jj's secret weapon for history management. When you amend a commit, its hash changes (just like git) but its change ID stays the same. This means you can always refer to "that change I made" regardless of how many times you've rewritten it. No more losing track of commits after rebasing.

The Full jj Model

JJ'S MODEL

COMMITS (A DAG)

Every commit has a change ID (stable) and a commit ID (content hash). The working copy is just another commit, marked with @. Conflicts are stored inside commits as data.

BOOKMARKS (OPTIONAL LABELS)

Only needed for pushing to remotes. Not required for local work. Commits are never lost without them.

OPERATION LOG

Every repo mutation is recorded atomically. Full undo/redo at any time. Replaces reflogs entirely.

Quick Comparison: Common Scenarios

ScenarioGitjj
Amend the last commit git add -A && git commit --amend Just edit files โ€” it's auto-amended
Start new work git commit then edit jj new then edit
Set aside current work git stash jj new โ€” the old commit is still there
Fix a commit 3 back git rebase -i, mark edit, fix, continue jj edit <change-id>, fix, jj new
Undo last operation Consult reflog, construct reset command jj undo
See what happened git reflog (per-ref, cryptic) jj op log (atomic, human-readable)

Check Your Understanding

In jj, what happens when you modify a file in your working directory?
Right โ€” in jj there is no separate working tree or staging area. Every file change immediately amends the working-copy commit.
Not quite. In jj, there is no separate working tree or staging area โ€” every file change immediately amends the working-copy commit. The working copy is the commit.
What happens to descendant commits when you rewrite a commit in the middle of a stack?
Correct โ€” jj automatically rebases all descendants. Any resulting conflicts are stored as data in the commit rather than blocking the operation.
Not quite. jj automatically rebases all descendants when you modify an ancestor. Any resulting conflicts are stored as data in the commit, not as a blocking operation.
What is the purpose of jj's "change ID" as distinct from the commit hash?
Exactly โ€” the change ID stays the same even when you amend, rebase, or otherwise rewrite a commit (which changes its hash). You can always refer to the same logical change.
Not quite. The change ID is a stable identifier that stays the same even when you amend, rebase, or otherwise rewrite a commit (which changes its hash). It lets you track a logical change through any number of rewrites.

What's Next

Now that you have the mental model, the next lesson gets jj installed and walks you through your first real workflow: cloning one of your existing GitHub repos and making changes with jj's approach.

Recommended Reading

Git Comparison โ€” jj Documentation โ€” The official comparison page. Covers everything in this lesson with additional detail and examples. ~10 min read.

Questions? Ask me anything that's unclear about the mental model. I can explain any concept differently, show how a specific git workflow maps to jj, or dive deeper into why a particular design choice was made.
Next โ†’