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

Updating trunk from upstream

Everything so far has assumed the repository stands still while we work. In a real project, other people will merge things into main, and our work will eventually need to catch up. Let’s see how to do that.

Getting the new commits

jj git fetch pulls new commits and bookmark positions down from the remote:

$ jj git fetch
bookmark: main@origin [updated] tracked

Notice what it didn’t do: nothing in our working copy moved, and no merge happened. Where git pull combines fetching with an integration step, jj keeps those two jobs separate. We can fetch first, inspect the result, and then choose how to move our work.

Let’s look at where things sit afterwards:

$ jj log
@  nxoupqvp steve@steveklabnik.com 2024-03-22 10:12:04 60d3e181
│  more work
○  tqmvurqz steve@steveklabnik.com 2024-03-22 10:12:04 my-feature d04bd54d
│  my feature
│ ◆  ztzvtupk steve@steveklabnik.com 2024-03-22 10:14:31 main 2fd78044
├─╯  upstream commit
◆  qoypmutx steve@steveklabnik.com 2024-03-22 09:58:12 23a248a4
│  initial
~

There’s the drift, drawn out. main moved forward, our two changes are still sitting on the old commit, and the two lines have forked apart.

The local main bookmark moved on its own, because it tracks main@origin and we hadn’t touched it locally. This is the normal case, and it’s why you rarely think about the local copy of trunk at all.

Also notice the diamonds: main and the initial commit are immutable. The default immutable heads are trunk() | tags() | untracked_remote_bookmarks(), and those heads plus all their ancestors are immutable. A tracked remote bookmark for a pull request is deliberately not an immutable head, so pushed review branches can still be rewritten.

Moving your work onto it

Now let’s catch up. jj rebase -b @ -o trunk() takes the whole branch our working copy is on and puts it on trunk:

$ jj rebase -b @ -o 'trunk()'
Rebased 2 commits to destination.
Working copy  (@) now at: nxoupqvp 355a63e0 more work
Parent commit (@-)      : tqmvurqz 71ade80c my-feature | my feature
Added 1 files, modified 0 files, removed 0 files
$ jj log
@  nxoupqvp steve@steveklabnik.com 2024-03-22 10:16:02 355a63e0
│  more work
○  tqmvurqz steve@steveklabnik.com 2024-03-22 10:16:02 my-feature 71ade80c
│  my feature
◆  ztzvtupk steve@steveklabnik.com 2024-03-22 10:14:31 main 2fd78044
│  upstream commit
~

One line again. Both changes kept their change IDs — nxoupqvp and tqmvurqz are the same changes they were before — and got new commit IDs, because their contents now sit on a different parent. The bookmark came along for the ride.

jj rebase takes two halves: which revisions move, and where they land.

There are three flags we can use to choose what moves:

  • -b (branch) moves everything connected to the given revision that isn’t already an ancestor of the destination. This is the one you want for “catch my work up to trunk”.
  • -s (source) moves that revision and its descendants.
  • -r (revision) moves exactly one revision, and reparents its descendants onto its old parent.

-o (onto) chooses where they land, leaving the destination’s existing descendants where they are. Its two siblings splice into a stack instead: -A inserts after the target and replants the target’s descendants on top of the moved work, and -B inserts before it.

trunk() is a revset function resolving to the main bookmark. jj git clone sets it up for you — you’ll see revset-aliases."trunk()" = "main@origin" in the repository’s config — so it points at whatever the project actually calls its trunk, and you can write trunk() in scripts and aliases without caring.

If the rebase produces conflicts, they get recorded in the rebased commits and you carry on, exactly as in the conflicts chapter. Nothing stops halfway, and there’s no --continue to remember.

Seeing what’s yours

The revset trunk()..@ means “everything from trunk up to my working copy”, which is a precise way of saying “my work”:

$ jj log -r 'trunk()..@'
@  nxoupqvp steve@steveklabnik.com 2024-03-22 10:16:02 355a63e0
│  more work
○  tqmvurqz steve@steveklabnik.com 2024-03-22 10:16:02 my-feature 71ade80c
│  my feature
~

That one earns a place in your config as an alias. We’ll do that in the customization section.

When trunk has diverged

Sometimes we commit directly onto main locally, and upstream moves too. Now both ends of the bookmark have moved, and jj won’t guess which one we meant:

$ jj git fetch
bookmark: main@origin [updated] tracked

$ jj log
@  oqtwrqnv steve@steveklabnik.com 2024-03-22 10:20:11 main?? main@git 914cd522
│  (empty) local commit on main
│ ◆  quvlnmky steve@steveklabnik.com 2024-03-22 10:20:37 main?? main@origin edd53bde
├─╯  upstream commit 2

The ?? marks a conflicted bookmark: one name, two candidate positions. It’s not a file conflict, and it doesn’t block you — but a bookmark pointing at two commits can’t be pushed anywhere sensible.

jj bookmark list lays out the disagreement:

$ jj bookmark list
main (conflicted):
  - ztzvtupk 2fd78044 upstream commit 1
  + oqtwrqnv 914cd522 (empty) local commit on main
  + quvlnmky edd53bde upstream commit 2
  @git (behind by 1 commits): oqtwrqnv 914cd522 (empty) local commit on main
  @origin (behind by 1 commits): quvlnmky edd53bde upstream commit 2
my-feature: tqmvurqz 71ade80c my feature
Hint: Some bookmarks have conflicts. Use `jj bookmark set <name> -r <rev>` to resolve.

The - line is the common ancestor the two sides started from, and the + lines are the two positions. Fix it the same way you’d fix the drift: put your local commit on top of the remote one.

$ jj rebase -r oqtwrqnv -o main@origin
Rebased 1 commits to destination.
Working copy  (@) now at: oqtwrqnv 07effb17 main* | (empty) local commit on main
Parent commit (@-)      : quvlnmky edd53bde main@origin | upstream commit 2
$ jj bookmark list
main: oqtwrqnv 07effb17 (empty) local commit on main
  @origin (behind by 1 commits): quvlnmky edd53bde upstream commit 2

The ?? is gone. The bookmark followed the commit it was pointing at, and once that commit sat on top of main@origin there was nothing left to disagree about. The * in main* means the local bookmark is ahead of the remote — the push you’d expect to make next.

If you’d rather throw the local side away than keep it, jj bookmark set main -r main@origin names the winner directly. Your commit isn’t lost; it’s still in the log, just no longer wearing the bookmark.

The habit

I like to fetch and rebase onto trunk regularly. The rebase is cheap, it never interrupts us, and any conflicts are recorded rather than blocking the command. That keeps catching up a small part of the ordinary workflow.