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

Using jj with Gerrit

Everything in this section so far has assumed GitHub, or something shaped like it: branches, pull requests, one review per branch. Gerrit is the other major shape of code review, and it turns out jj fits it remarkably well — arguably better than git does.

In Gerrit, the unit of review isn’t a branch, it’s a single commit, called a “change” (or a “CL”). You upload a commit, a reviewer comments on it, you rewrite the commit and upload it again, and Gerrit shows each uploaded version as a numbered “patch set” on the same change. If that sounds familiar, it should: it’s exactly how we’ve been treating jj changes all along. One jj change becomes one Gerrit change, and rewriting it — with jj squash, jj absorb, jj describe, whatever we like — produces its next patch set.

Change IDs, twice over

Gerrit needs a way to recognize that a rewritten commit is a new version of an existing change, and its answer predates jj: a Change-Id: trailer in the commit message. That’s the same problem jj’s change IDs solve, but they are two different identifiers. When we upload, jj adds the trailer for us, deriving it from the jj change ID, and leaves any existing trailer alone. We don’t need to manage it, but don’t be surprised to see it in our descriptions afterwards.

Uploading

In a traditional Gerrit workflow, you push with a magic refspec: git push origin HEAD:refs/for/main. jj wraps this up in a single command:

$ jj gerrit upload -r 'trunk()..@' --remote-branch main

The -r revset selects what to upload — here, our whole stack — and --remote-branch names the branch the changes are intended to land on. Each revision in the revset becomes its own Gerrit change, so uploading a stack of three commits opens three changes, each reviewable on its own, with their relationships intact. This is the “stacked” workflow that takes real effort on GitHub, and on Gerrit it’s just how things work.

Because uploading may rewrite our commits — adding missing Change-Id trailers — as well as push them, I like to dry-run a broad revset before running it for real:

$ jj gerrit upload -r 'trunk()..@' --remote-branch main --dry-run

Responding to review is the part that should feel comfortable by now: edit the change like any other — jj edit, or fix things in @ and jj squash or jj absorb them down — and upload again. The stable trailer tells Gerrit it’s the same change, and the reviewer sees a new patch set, with Gerrit’s UI able to diff one patch set against another. There’s no branch to force-push and no bookmark to move.

A default branch

Typing --remote-branch main every time gets old. We can set a default for the repository:

$ jj config set --repo gerrit.default-remote-branch main

After that, jj gerrit upload -r 'trunk()..@' is all we need. There’s a matching gerrit.default-remote setting for pointing at the Gerrit instance itself, if the remote isn’t where we usually push.