Skip to content

Instantly share code, notes, and snippets.

@steveklabnik
Last active November 13, 2024 12:03
Show Gist options
  • Save steveklabnik/53b51724920dac76fc623d91e2afa5ab to your computer and use it in GitHub Desktop.
Save steveklabnik/53b51724920dac76fc623d91e2afa5ab to your computer and use it in GitHub Desktop.
A chapter from v2 of steve's jj tutorial. Feedback welcome!

Getting Started

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.

Installation

Just go here: https://martinvonz.github.io/jj/latest/install-and-setup/

Cloning a repository

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.

Forking and cloning

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

Taking a look around

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.

Making a new 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.

Changes and commits

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.

Modifying a file

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.

Using jj commit

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!

Interacting with GitHub

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.

A summary of the basics

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.

@jimt
Copy link

jimt commented Nov 3, 2024

This means both jj's information as git's information

as -> and

@steveklabnik
Copy link
Author

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment