Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Merging anonymous branches

Let’s recall where we are:

> jj log
@  xrslwzvq steve@steveklabnik.com 2024-02-29 23:06:23 e78a9350
│  create hello and goodbye functions
│ ○  yykpmnuq steve@steveklabnik.com 2024-02-29 23:03:22 210283e8
├─╯  (empty) add better documentation
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world

We have what we consider to be the head of our repository, ootnlvpt, and then two branches, xrslwzvq and yykpmnuq. Let’s create another change with ootnlvpt as the parent, to simulate the idea that some changes have landed on our main branch while we were doing the work:

> jj new o -m "added some cool new feature"
Working copy  (@) now at: pzoqtwuv 9353442b (empty) added some cool new feature
Parent commit (@-)      : ootnlvpt b5db7940 only print hello world

Let’s take a look:

> jj log --limit 5
@  pzoqtwuv steve@steveklabnik.com 2024-03-01 15:06:59 9353442b
│  (empty) added some cool new feature
│ ○  xrslwzvq steve@steveklabnik.com 2024-02-29 23:06:23 e78a9350
├─╯  create hello and goodbye functions
│ ○  yykpmnuq steve@steveklabnik.com 2024-02-29 23:03:22 210283e8
├─╯  (empty) add better documentation
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world
○  nmptruqn steve@steveklabnik.com 2024-02-28 23:09:11 90a2e97f
│  add more comments

We passed the --limit flag so that we didn’t see every commit; our history is already getting a little long.

Merging branches

Now, you may expect that you’d use a command like jj merge to merge branches together. There is no such command: it was deprecated in 0.14.0 and has since been removed. So how the heck do we create merges?

Well, what is a merge anyway? It’s a new change that has more than one parent. How do we make new changes? With jj new. So let’s ask it to make a change that has both pzoqtwuv and yykpmnuq as parents:

> jj new pzoqtwuv yykpmnuq -m "merge better documentation"
Working copy  (@) now at: rxzyvnkx f1c1bde8 (empty) merge better documentation
Parent commit (@-)      : pzoqtwuv 9353442b (empty) added some cool new feature
Parent commit (@-)      : yykpmnuq 210283e8 (empty) add better documentation

Just like we’d pass a parent revision to jj new, we can pass multiple parents, and it just works. No need for a special command. Let’s look at our history, choosing six as the limit since we just added a new change:

> jj log --limit 6
@    rxzyvnkx steve@steveklabnik.com 2024-03-01 15:21:11 f1c1bde8
├─╮  (empty) merge better documentation
│ ○  yykpmnuq steve@steveklabnik.com 2024-02-29 23:03:22 210283e8
│ │  (empty) add better documentation
○ │  pzoqtwuv steve@steveklabnik.com 2024-03-01 15:06:59 9353442b
├─╯  (empty) added some cool new feature
│ ○  xrslwzvq steve@steveklabnik.com 2024-02-29 23:06:23 e78a9350
├─╯  create hello and goodbye functions
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world
○  nmptruqn steve@steveklabnik.com 2024-02-28 23:09:11 90a2e97f
│  add more comments

We can see the lines connecting to both of our parents, and we can still see the xrslwzvq branch is left over too.

But here’s something really wild: we can just do this as much as we want, no need to stop at two parents. To try this out, we’re going to run a command I haven’t told you about yet:

$ jj undo
Undid operation: 4a1f0e6c8b2d (2024-03-01 15:21:11) new empty commit
Restored to operation: 91c33bb0d5ef (2024-03-01 15:06:59) new empty commit
Working copy  (@) now at: pzoqtwuv 9353442b (empty) added some cool new feature
Parent commit (@-)      : ootnlvpt b5db7940 only print hello world
$ jj log --limit 5
@  pzoqtwuv steve@steveklabnik.com 2024-03-01 15:06:59 9353442b
│  (empty) added some cool new feature
│ ○  xrslwzvq steve@steveklabnik.com 2024-02-29 23:06:23 e78a9350
├─╯  create hello and goodbye functions
│ ○  yykpmnuq steve@steveklabnik.com 2024-02-29 23:03:22 210283e8
├─╯  (empty) add better documentation
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world
○  nmptruqn steve@steveklabnik.com 2024-02-28 23:09:11 90a2e97f
│  add more comments

That’s right, we can undo our last command with a simple jj undo. There’s a whole section on this later — “Fixing Problems” — because it goes much deeper than it looks. For now, it’s like our merge never happened.

Let’s try merging all three in at the same time:

$ jj new pzoqtwuv yykpmnuq xrslwzvq -m "merge three branches"
Working copy  (@) now at: vuztuxmz 717232df (empty) merge three branches
Parent commit (@-)      : pzoqtwuv 9353442b (empty) added some cool new feature
Parent commit (@-)      : yykpmnuq 210283e8 (empty) add better documentation
Parent commit (@-)      : xrslwzvq e78a9350 create hello and goodbye functions
Added 0 files, modified 1 files, removed 0 files
$ jj log --limit 6
@      vuztuxmz steve@steveklabnik.com 2024-03-01 15:38:49 717232df
├─┬─╮  (empty) merge three branches
│ │ ○  xrslwzvq steve@steveklabnik.com 2024-02-29 23:06:23 e78a9350
│ │ │  create hello and goodbye functions
│ ○ │  yykpmnuq steve@steveklabnik.com 2024-02-29 23:03:22 210283e8
│ ├─╯  (empty) add better documentation
○ │  pzoqtwuv steve@steveklabnik.com 2024-03-01 15:06:59 9353442b
├─╯  (empty) added some cool new feature
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world
○  nmptruqn steve@steveklabnik.com 2024-02-28 23:09:11 90a2e97f
│  add more comments

Just as easy as that: a merge commit with three different parents.

Once again I am reminded of the theme I discussed at the start: simpler can also be more powerful. jj has eliminated the need for an entire command but not lost any functionality.

But what if we didn’t want to create a merge commit? Don’t worry, jj has rebase as well.

Rebasing branches

Like git, jj has a command called rebase. It does what it says, it takes a change and, instead of its current “base,” aka parent, moves it to have a different parent, “basing” it again, or “re-basing” it.

Let’s undo our merge again:

$ jj undo
Undid operation: 4a1f0e6c8b2d (2024-03-01 15:21:11) new empty commit
Restored to operation: 91c33bb0d5ef (2024-03-01 15:06:59) new empty commit
Working copy  (@) now at: pzoqtwuv 9353442b (empty) added some cool new feature
Parent commit (@-)      : ootnlvpt b5db7940 only print hello world
$ jj log --limit 5
@  pzoqtwuv steve@steveklabnik.com 2024-03-01 15:06:59 9353442b
│  (empty) added some cool new feature
│ ○  xrslwzvq steve@steveklabnik.com 2024-02-29 23:06:23 e78a9350
├─╯  create hello and goodbye functions
│ ○  yykpmnuq steve@steveklabnik.com 2024-02-29 23:03:22 210283e8
├─╯  (empty) add better documentation
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world
○  nmptruqn steve@steveklabnik.com 2024-02-28 23:09:11 90a2e97f
│  add more comments

Excellent. Let’s keep a linear history by using a rebase instead of a merge.

You can use jj rebase in a few different ways. Let’s show off the simplest: rebasing a single change. Let’s rebase our “create hello and goodbye functions” change on top of our current change:

$ jj rebase -r xrslwzvq -o pzoqtwuv
Rebased 1 commits to destination.

This rebases a single revision with -r, onto a certain destination revision, hence -o. Since our branch only had one revision, this would be the same as passing -b xrslwzvq, which would move the whole branch that revision is on, or -s xrslwzvq, which rebases that revision as well as all of its descendants.

Note that the working copy didn’t move. Let’s look at our log:

$ jj log --limit 5
○  xrslwzvq steve@steveklabnik.com 2024-03-01 16:08:37 6c4afc8f
│  create hello and goodbye functions
@  pzoqtwuv steve@steveklabnik.com 2024-03-01 15:06:59 9353442b
│  (empty) added some cool new feature
│ ○  yykpmnuq steve@steveklabnik.com 2024-02-29 23:03:22 210283e8
├─╯  (empty) add better documentation
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world
○  nmptruqn steve@steveklabnik.com 2024-02-28 23:09:11 90a2e97f
│  add more comments

We have rebased our commit successfully. But you may have noticed something surprising: @ is still at pzoqtwuv. This actually belies a very deep difference between jj and git that I learned from Austin Seipp, one of jj’s maintainers. And here it is:

jj commands primarily operate on the data structures stored in its repository, rather than on the working copy.

This simple statement has some profound implications. One of the simplest consequences of this is jj’s speed. Because the working copy is itself a commit, and commits are in the database, it can treat it like any other commit. In this case, the difference is even larger: git rebase works on the working copy. This is why it has to stop you and make you resolve things in the case where a conflict happens, because it’s about to create a new commit from the working copy, and if that’s in conflict, it has to be fixed or the next commit is nonsense. We’ll talk about how jj handles conflicts shortly, but as I said before: rebases always succeed in jj. So this change is quick: it’s only modifying information in the repository, not touching any of the files we have in our working copy. This also means our working copy hasn’t changed, so @ is in the same place it was before the rebase.

Some commands do move @ by default, like jj new. This is because if you’re creating a new change, you probably want to start working on it. But it has a flag you can pass instead to create a new change but not modify @:

$ jj new -m "not gonna start this yet" --no-edit
Created new commit owlpoptm df6620cb (empty) not gonna start this yet
$ jj log --limit 6
○  owlpoptm steve@steveklabnik.com 2024-03-01 16:28:54 df6620cb
│  (empty) not gonna start this yet
│ ○  xrslwzvq steve@steveklabnik.com 2024-03-01 16:08:37 6c4afc8f
├─╯  create hello and goodbye functions
@  pzoqtwuv steve@steveklabnik.com 2024-03-01 15:06:59 9353442b
│  (empty) added some cool new feature
│ ○  yykpmnuq steve@steveklabnik.com 2024-02-29 23:03:22 210283e8
├─╯  (empty) add better documentation
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world
○  nmptruqn steve@steveklabnik.com 2024-02-28 23:09:11 90a2e97f
│  add more comments

New change, yet we’re still where we are. Let’s undo that real quick:

$ jj undo
Undid operation: 7d2b41ac9e08 (2024-03-01 16:28:54) new empty commit
Restored to operation: 1e5f77c2ab3d (2024-03-01 16:08:37) rebase commit
$ jj log --limit 5
○  xrslwzvq steve@steveklabnik.com 2024-03-01 16:08:37 6c4afc8f
│  create hello and goodbye functions
@  pzoqtwuv steve@steveklabnik.com 2024-03-01 15:06:59 9353442b
│  (empty) added some cool new feature
│ ○  yykpmnuq steve@steveklabnik.com 2024-02-29 23:03:22 210283e8
├─╯  (empty) add better documentation
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world
○  nmptruqn steve@steveklabnik.com 2024-02-28 23:09:11 90a2e97f
│  add more comments

Cool. Okay so that theory sounds cool, and it’s nice that it makes things fast, but what about when we do want to move @? Well, the fact that the --no-edit flag is what we passed to jj new gave it away:

$ jj edit xrslwzvq
Working copy  (@) now at: xrslwzvq 6c4afc8f create hello and goodbye functions
Parent commit (@-)      : pzoqtwuv 9353442b (empty) added some cool new feature
Added 0 files, modified 1 files, removed 0 files
$ jj log --limit 5
@  xrslwzvq steve@steveklabnik.com 2024-03-01 16:08:37 6c4afc8f
│  create hello and goodbye functions
○  pzoqtwuv steve@steveklabnik.com 2024-03-01 15:06:59 9353442b
│  (empty) added some cool new feature
│ ○  yykpmnuq steve@steveklabnik.com 2024-02-29 23:03:22 210283e8
├─╯  (empty) add better documentation
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world
○  nmptruqn steve@steveklabnik.com 2024-02-28 23:09:11 90a2e97f
│  add more comments

We can now rebase our other change on top too:

$ jj rebase -r yykpmnuq -o xrslwzvq
Rebased 1 commits to destination.
$ jj log --limit 5
○  yykpmnuq steve@steveklabnik.com 2024-03-01 16:35:47 7bea29b6
│  (empty) add better documentation
@  xrslwzvq steve@steveklabnik.com 2024-03-01 16:08:37 6c4afc8f
│  create hello and goodbye functions
○  pzoqtwuv steve@steveklabnik.com 2024-03-01 15:06:59 9353442b
│  (empty) added some cool new feature
○  ootnlvpt steve@steveklabnik.com 2024-02-28 23:26:44 b5db7940
│  only print hello world
○  nmptruqn steve@steveklabnik.com 2024-02-28 23:09:11 90a2e97f
│  add more comments

Excellent. But before we move @, I want to show you a little trick. We could type in the change ID, and in this case, yyk is the unique prefix, so it isn’t that hard. But we can also use a revset:

$ jj edit @+
Working copy  (@) now at: yykpmnuq 7bea29b6 (empty) add better documentation
Parent commit (@-)      : xrslwzvq 6c4afc8f create hello and goodbye functions

+ means “the child of this revision”, so @+ is “the child revision of the working copy”, which in this case is exactly where we wanted to go.

We’ve alluded to conflicts several times in this tutorial. We’re finally ready to address those.