Changes, commits, and revisions
If there's one concept that's at the heart of jj
, that would be the change.
Let's talk about them, as well as two related concepts: commits and
revisions.
Changes
Changes serve the same purpose, conceptually, that commits do in git
: they're a
snapshot of your project, and the graph of these changes forms a sort of
timeline of the history of the project.
So why not call them commits? Well, there's a big difference between changes in
jj
and commits in git
, and that's because git
's commits are immutable, whereas
(by default) changes are mutable. Let's talk about how we use these tools to
compare and contrast the two.
Creating Changes vs Commits
I really like this description from Pro Git:
The basic Git workflow goes something like this:
- You modify files in your working tree.
- You selectively stage just those changes you want to be part of your next commit, which adds only those changes to the staging area.
- You do a commit, which takes the files as they are in the staging area and stores that snapshot permanently to your Git directory.
"Creating a commit" is an action that you do after you've got things set up the way you want them to be saved. You can use the index to do this incrementally if you'd like.
If we were to talk about jj
in a similar way, I'd say this:
The basic
jj
workflow goes something like this:
- You create a new change
- You modify files in your working tree.
- There is no step 3.
We are always working within the context of some change. jj
refers to the
change we're working on as @
. Whenever you run a jj
command, it will sync up
your working tree and @
. You can also ask Watchman to do this
synchronization every time you save a file.
This means that in some sense, you work "backwards" in jj
from git
's
perspective: it's not "modify files in your working tree, produce a commit"
it's "make a new change, modify files in your working tree your working
tree."
Descriptions
We haven't really talked about descriptions yet. We've used jj commit
to give some of our changes descriptions, but as you've seen, jj new
is totally fine with creating a new change without one. We can use
jj describe
to give @
a description. This will open up your $EDITOR
and works very similar to git commit
. If you give a lengthy description
to a commit, jj
will use its first line as the description in jj log
.
Just as with git
, it's recommended that you create a relatively short
first line for this reason.
jj commit
is like running jj describe
followed by a jj new
. Many
people find jj commit
to be a useful tool when they start learning
jj
, but a lot of people end up not using it as much once they're more
comfortable with the tool. There's nothing wrong with using it if it's
the workflow you like, though!
Change IDs
You may have noticed change IDs use purely letters instead of letters and
numbers. This is deliberate, and in fact, doesn't include a
through f
,
so you cannot confuse a change ID for a commit ID.
Immutable Changes
While changes are mutable, there are some instances when you'd prefer not to
mutate one. jj
considers some commits immutable, and if you try to mutate
one, it will refuse unless you pass --ignore-immutable
as an argument.
Let's look at jj log
:
$ jj log
@ opqvmvrn steve@steveklabnik.com 2025-02-06 09:43:34 95d5c471
│ (empty) (no description set)
○ tnmounps steve@steveklabnik.com 2025-02-06 09:43:34 goodbye-world git_head() 326253c2
│ Goodbye, world!
◆ ptrqnyzv steve@steveklabnik.com 2024-09-24 12:43:36 trunk 0c72abbb
│ Hello, world!
~
We have the green @
, but its parent instead has a ○
. This change
is mutable. But what about p
? It has a ◆
. This change is immutable.
jj
considers a change immutable if it's part of your trunk
/main
/master
branch, if it's got a git
tag, if it's an untracked remote bookmark.
You can override this, but I don't recommend it.
Oh, and the root change is also immutable. Let's talk about that!
The root change
One interesting thing about jj
is that there's a special change that
exists in every repository, the root change. While git
allows for
multiple root commits, jj
does not: every change other than the root
change must have a parent change. This simplifies a lot of the various
algorithms used to modify history, but the specifics aren't important
to us right now.
We can use jj show
to show information about changes. The root change
has a change ID of all z
s, let's check it out:
$ jj show zz
Commit ID: 0000000000000000000000000000000000000000
Change ID: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
Author : (no name set) <(no email set)> (1970-01-01 11:00:00)
Committer: (no name set) <(no email set)> (1970-01-01 11:00:00)
(no description set)
Pretty fun. Wait, what's that Commit ID
doing there? Okay, let's
talk about commits.
Commits
So we've been talking about how changes are different than git
's commits...
but jj
also has commits. Let me explain. Let's use jj st
to look at our
current change:
$ jj st
The working copy has no changes.
Working copy (@) : opqvmvrn 95d5c471 (empty) (no description set)
Parent commit (@-): tnmounps 326253c2 goodbye-world | Goodbye, world!
Do you see how we have four identifiers there?
opqvmvrn 95d5c471
tnmounps 326253c2
The two on the left are change IDs, but the two on the right are commit IDs.
What are commits for? Well, whenever you modify a change, that has to be stored
in git
somehow. And since git
's commit IDs are based on (among other things)
the contents of the commit, when you create a new git commit, you're also going
to get a different ID. Here, let's give it a try: modify src/main.rs
, in any
way that you'd like. Then we'll run jj st
again:
$ jj st
Working copy changes:
M src/main.rs
Working copy (@) : opqvmvrn c920ae70 (no description set)
Parent commit (@-): tnmounps 326253c2 goodbye-world | Goodbye, world!
o
used to have a commit ID of 95d5c471
, but now it's c920ae70
. Our change
ID remains stable, but the commit ID will change over time.
This is very powerful! Part 4 of the tutorial is titled "Fixing Problems," and
a lot of the stuff we will talk about there is powered by commits. We can
use jj evolog
, the "evolution log," to see how a change has evolved over time:
$ jj evolog --summary
@ opqvmvrn steve@steveklabnik.com 2025-02-06 09:43:34 c920ae70
│ (no description set)
│ M src/main.rs
○ opqvmvrn hidden steve@steveklabnik.com 2025-02-06 09:43:34 95d5c471
(empty) (no description set)
There are a lot of flags to control jj evolog
's output. I've chosen the
summary
flag here to show which files we changed.
Revisions
The term revision is sometimes used as well. "revision" is a synonym for "commit." It's a slightly more generic term.
The jj
project is still working out which terminology is best, and so sometimes,
you'll find things like this. In particular, there's a desire to turn these three
concepts into only two, but it's not clear which word ends up being best, so for
now, there's three of them.