In this first chapter, we'll be jumping right into using jj
for a
real-world task: creating a pull request on GitHub. But before we get
to that, let's get jj
installed.
Just go here: https://martinvonz.github.io/jj/latest/install-and-setup/
Let's create a pull request! We're going to do that on GitHub, but these instructions should be easy to adopt to any of the various similar code forges.
I've got a sample repository located here: [https://github.com/jj-tutorial/hello-world][]. We'll be using this repo as an example for this first part of the tutorial. Click "fork" to create a fork of your own.
Next, let's clone down our fork. Go to the directory where you'd like to create
your clone, in my case, that's ~/src
. And then type this:
❯ jj git clone --colocate git@github.com:<YOUR USERNAME>/hello-world
Fetching into new repo in "/home/<YOUR USERNAME>/src/hello-world"
bookmark: trunk@origin [new] untracked
Setting the revset alias "trunk()" to "trunk@origin"
Working copy now at: kzmwwmru 6e2297c3 (empty) (no description set)
Parent commit : ptrqnyzv 0c72abbb trunk | Hello, world!
Added 4 files, modified 0 files, removed 0 files
❯ cd hello-world/
❯
Just like git clone
, jj git clone
will clone a remote repository
to your local disk. However, we are passing a certain flag to this
command, --colocate
. jj
supports two different kinds of repositories:
colocated, and non-colocated. What's the difference? Well, let's take a look
at our repository:
❯ tree . -a -L 1
.
├── .git
├── .gitignore
├── .jj
├── Cargo.lock
├── Cargo.toml
└── src
We have both a .jj
and a .git
directory at the top level. This means both
jj's information as git's information are co-located: they're next to each
other. A non-colocated directory stores .git
inside of .jj
. For your
first foray into jj
, I strongly recommend a colocated repository, as it
allows you to still easily run git
commands as well as jj
's. This can
help ease you into things. It also means tooling that expects to see a .git
at the root of the repository will still work.
Let's see what our repository looks like:
❯ jj log
@ kzmwwmru steve@steveklabnik.com 2024-10-28 16:49:15 6e2297c3
│ (empty) (no description set)
◆ ptrqnyzv steve@steveklabnik.com 2024-09-23 20:43:36 trunk HEAD@git 0c72abbb
│ Hello, world!
~
The CLI has lots of pretty colors that don't come across on this page, oh well.
This looks a bit different than git log
, but it's the same general idea: we
can see where we our in our history.
There's a lot I could say about this output, but I'd rather show you how to get work done first. Let's make our first change.
jj
is a very flexible tool, but in this section, we're going to show the
simplest possible workflow. If you're a fan of building up small commits via the
git
index, we'll learn how to do that in the next chapter. Baby steps!
If you remember from the end of the last section, we're on an empty change.
You can double check with jj status
:
❯ jj status
The working copy is clean
Working copy : kzmwwmru 6e2297c3 (empty) (no description set)
Parent commit: ptrqnyzv 0c72abbb trunk | Hello, world!
jj st
is an alias to make that a bit easier.
If you look closely in the output we just saw, you'll see four identifiers:
kzmwwmru 6e2297c3
ptrqnyzv 0c72abbb
In git
, we often talk about commits and their ID numbers. Those look like the
6e2297c3
and 0c72abbb
bits in that output, and you'd be right. What about
kzmwwmru
and ptrqnyzv
though? Those are called "change ID"s. jj
has a
concept called a "change", and it's like a commit that evolves over time. This
change ID will remain stable over the life of the change, but every time we
modify it, we'll see a new commit ID. In a sense, a change represents the
evolution of a commit over time.
Let's see how that works by modifying a file. Our change ID is kzmwwru
and our
commit is 6e2297c3
. Let's modify a file and see what happens.
Here's the contents of src/main.rs
. Don't worry, you won't need to actually
know Rust to complete this tutorial, we just want some code to work with:
fn main() {
println!("Hello, world!");
}
Let's change that to something else:
fn main() {
println!("Goodbye, world!");
}
A bit fatalistic, but it works. Let's run jj st
again:
❯ jj st
Working copy changes:
M src/main.rs
Working copy : kzmwwmru abbbf36c (no description set)
Parent commit: ptrqnyzv 0c72abbb trunk | Hello, world!
We can see we've modified src/main.rs
. Whenever we run a jj
command,
it updates the contents of @
, the commit that represents the working copy,
to contain all of the changes we've made.
We still see our change ID kzmwwru
, but now, instead of 6e2297c3
, we have
abbbf36c
. Our change ID is stable, but now that we've evolved it by making
changes to our working directory, we get a new commit that represents this
latest state. This is a bit of a mental shift from git
! In git
, once we
have a commit, it's "done," unless we decide to modify it later. With jj
,
changes aren't just for finished work: they're also for tracking in-progress
work. We'll talk more about mutable vs immutable changes eventually, but for
now, just know that changes and commits are two different things, and that
a change represents the evolution of a commit over time, giving it a stable
identifier that we can talk about.
Let's say we're happy with the contents of this change. We want to
finish up this work, and start something else. To do that, we can use jj commit
:
❯ jj commit -m "Goodbye, world!"
Working copy now at: nkztqpus 6dcc526e (empty) (no description set)
Parent commit : kzmwwmru e2dd22df Goodbye, world!
To see our changes in context, let's look at jj log
again:
❯ jj log
@ nkztqpus steve@steveklabnik.com 2024-10-28 17:32:40 6dcc526e
│ (empty) (no description set)
○ kzmwwmru steve@steveklabnik.com 2024-10-28 17:32:40 HEAD@git e2dd22df
│ Goodbye, world!
◆ ptrqnyzv steve@steveklabnik.com 2024-09-23 20:43:36 trunk 0c72abbb
│ Hello, world!
~
We can see that @
is now on a new, empty change. And we have kzmwwmru
as its parent.
In the next section, we'll make a pull request for this change!
In the last section, we made a new change, built on top of trunk
:
❯ jj log
@ nkztqpus steve@steveklabnik.com 2024-10-28 17:32:40 6dcc526e
│ (empty) (no description set)
○ kzmwwmru steve@steveklabnik.com 2024-10-28 17:32:40 HEAD@git e2dd22df
│ Goodbye, world!
◆ ptrqnyzv steve@steveklabnik.com 2024-09-23 20:43:36 trunk 0c72abbb
│ Hello, world!
~
Let's make a pull request for this.
In order to make a pull request, we need a branch. Well, GitHub wants a branch. But we haven't made any branches! Did you notice that?
jj
lets us work without naming any branches. That's great when we're working
locally, but when we interact with GitHub, it needs a branch name. To bridge
this gap, jj
has a feature called "bookmarks". They're closer to git
tags
than git
branches, but since branches are a special type of tag, they'll work.
To create a bookmark, we can use jj bookmark
:
❯ jj bookmark set goodbye-world -r @-
Created 1 bookmarks pointing to kzmwwmru e2dd22df goodbye-world | Goodbye, world!
Hint: Consider using `jj bookmark move` if your intention was to move existing bookmarks.
❯
jj bookmark set
takes a name for the bookmark, and then we also pass a -r
flag.
This is short for "revision," and it means we can pass in a change ID, a commit ID,
another bookmark name... lots of things. In this case, we pass @-
, which means
"the parent of @
."
Let's look at our log:
❯ jj log
@ nkztqpus steve@steveklabnik.com 2024-10-28 17:32:40 6dcc526e
│ (empty) (no description set)
○ kzmwwmru steve@steveklabnik.com 2024-10-28 17:32:40 goodbye-world HEAD@git e2dd22df
│ Goodbye, world!
◆ ptrqnyzv steve@steveklabnik.com 2024-09-23 20:43:36 trunk 0c72abbb
│ Hello, world!
~
We can now see goodbye-world
listed on the right. Great! Let's push that up
to GitHub:
❯ jj git push
Changes to push to origin:
Add bookmark goodbye-world to e2dd22df5f5d
remote:
remote: Create a pull request for 'goodbye-world' on GitHub by visiting:
remote: https://github.com/<YOUR USERNAME>/hello-world/pull/new/goodbye-world
remote:
A jj git push
will push up all of our changes. In this case, we have our new
bookmark, which has turned into a git
branch. We could use that link to
create a pull request just like any other.
Because this is a tutorial repository, I won't be merging pull requests. But, if this was a PR that eventually got merged, it's easy to pull the changes down:
❯ jj git fetch
Nothing changed.
If there were changes, we'd have some output describing what happens.
With that, you have the basics down! Let's go over everything one more time, just to double check what you've learned.
Let's go over what we've learned in this chapter:
We can clone a repository with jj git clone
. The --colocated
flag allows us
to keep using our normal git
tooling if we wish.
jj
has both changes and commits. A change represents the evolution of a commit
over time.
We can look at the history of our repository with jj log
, and the state of
the current change with jj st
.
@
is a special name for the change representing the working directory. Every
time we run a jj
command, @
is updated with the latest snapshot of the
working directory.
When we're done with building up our changes in @
, we can use jj commit -m
with a message to describe our change with, and then start a new change.
Bookmarks are jj
's feature to work with git
's branches, and jj bookmark set <name> -r <revision>
will set a bookmark named <name>
to the given revision.
jj git push
will push any changes in our local repository to our git
remote,
and jj git fetch
will synchronize our local repository with any changes that
have been made on the remote that we don't have yet.
That's a bunch of stuff! With these steps, you have the basics down, but there's
still a bunch to learn. In the next chapter, we'll learn more about the two most
popular workflows for getting real work done with jj
.
as -> and