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:
That's it. Everything else git exposes as separate concepts (working copy, index, stashes, branches) is either eliminated or subsumed into commits.
Git Comparison โ Official jj Docs
Read this alongside the lesson. It's concise and covers every point below with more detail.
| Git concept | jj equivalent | Why 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. |
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.
# Three steps to persist a change
$ echo "hello" > file.txt
$ git add file.txt
$ git commit -m "add file"
# One step โ it's already committed
$ echo "hello" > file.txt
# Done. The working-copy commit now
# includes this change.
$ jj describe -m "add file"
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.
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!
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.
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.
Every commit in jj has two IDs:
| ID type | What it is | When 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.
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.
Only needed for pushing to remotes. Not required for local work. Commits are never lost without them.
Every repo mutation is recorded atomically. Full undo/redo at any time. Replaces reflogs entirely.
| Scenario | Git | jj |
|---|---|---|
| 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) |
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.
Git Comparison โ jj Documentation โ The official comparison page. Covers everything in this lesson with additional detail and examples. ~10 min read.