Michael Pratt
Last updated: 2024-12-28
I use jj on a day-to-day basis for all of my contributions to the Go project.
Go uses Gerrit for code review, which is more complex to work with from jj than GitHub.
This note describes my configuration and workflow.
Note: While this document is focuses on working with the Go open source project specifically, most of the contents are generally applicable to any Gerrit-based project.
The main difference is that some Gerrit servers allow SSH authentication (rather than .gitcookies).
This page assumes basic familiarity with jj.
For more guidance on jj itself, see the references below.
See upstream instructions.
Working with Gerrit from jj presents three critical complications:
refs/for/master to upload changes intended for branch master, however jj git push does not support pushing to arbitrary refs (only "normal" branches).git this is handled transparently via some global git configuration. jj uses libgit2 rather than git itself, so it does not support this configuration and is thus unable authenticate to Gerrit.Change-Id lines in commit messages, but jj does not support git's pre-commit hooks (or any form of pre-commit hook).Note: jj has an open issue for explicit Gerrit command, plus an in-progress PR for a jj gerrit command that promises to make this more polished.
My approach to address these limitations is:
jj using the standard unauthenticated URL. e.g., https://go.googlesource.com/go. I call this remote origin (the default). This allows me to fetch updates using the built in jj git fetch command.jj hides the underlying git repository somewhere in .jj/ directory. In a colocated repository, a .git directory is created just like any other git repository. This makes it possible to use normal git commands on the repository.Warning: Mixing jj and git commands can easily get the repository in a confusing state.
I recommend reading the colocated repository documentation.
I limit my use of git commands to the ones below for pushing changes to Gerrit.
Change-Id lines to commit messages, I use a custom commit message table assigned automatically add a Change-Id line to commit messages.Let's put it all together and contribute to a project using jj!
jj first if you haven't already.$ jj config set --user user.name "Michael Pratt"
$ jj config set --user user.email "prattmic@example.com"
~/.config/jj/config.toml.$ jj git clone --colocate https://go.googlesource.com/go
Fetching into new repo in "/tmp/go"
bookmark: dev.boringcrypto@origin [new] untracked
...
Setting the revset alias "trunk()" to "master@origin"
Working copy now at: wpwmovqk a34ab857 (empty) (no description set)
Parent commit : rpzkxnvr 17bf224a master | crypto/internal/nistec: don't use go:embed
Added 13679 files, modified 0 files, removed 0 files
$ echo "Hello from jj" >> README.md
$ jj commit
Working copy now at: mpuptrql 666d0e7e (empty) (no description set)
Parent commit : tkqzmxpq 47f1f842 (…) jj test commit
$ jj git fetch
$ jj rebase --source t --destination master@origin
Note: --source t comes from the change ID reported by jj commit as tkqzmxpq. In the terminal, t is colored to indicate that is the unique short version of this change ID.
$ git push origin HEAD:refs/for/master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 358 bytes | 35.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2)
remote: Waiting for private key checker: 1/1 objects left
remote: Processing changes: refs: 1, new: 1, done
remote:
remote: SUCCESS
remote:
remote: https://go-review.googlesource.com/c/go/+/630275 WIP: jj test commit [WIP] [NEW]
remote:
To https://go.googlesource.com/go
* [new reference] HEAD -> refs/for/master
Note: See the Gerrit upload documentation if you are unfamiliar with the Gerrit upload syntax/options.
Warning: Here we pushed git HEAD to Gerrit. In a colocated repository, git HEAD is the state of the repository when the most recent jj command was run (Even semantically read-only commands like jj status may mutate the git repository due to jj's "everything is a commit" model).
Since I just made my commit and have not modified anything else or switched to a different commit, HEAD is exactly what I intend to upload.
If that is too subtle for you, you can also specify the exact git commit you want to push. The git commit sha1 is the second random-looking string shown with commits.
i.e., in tkqzmxpq 47f1f842 (…) jj test commit, tkqzmxpq is the jj change ID and 47f1f842 is the git sha1.
Push this explicitly with git push origin 47f1f842:refs/for/master.
Personally, I have never used git-codereview, so I don't feel its loss when using jj.
jj has no hooks, so you won't get the built-in hooks:
#123 to golang/go#123 in commit messages.gofmt when making a commit.Someone more familiar with git-codereview is welcome to add more documentation here!
From the source, git codereview mail looks like it may work OK (it just constructs a git push command line), but use at your own risk.
Sometimes it is nice to download CLs from Gerrit's "Download Patch" dialog, which provides a command like:
$ git fetch https://go.googlesource.com/go refs/changes/77/626277/8 && git checkout -b change-626277 FETCH_HEAD
In theory, jj can import this new branch from the colocated git repository.
In practice, I find this often makes a mess of things. In particular, if the Gerrit CL uses commits that are older versions of jj change IDs, jj gets very upset (this is normally an impossible condition in jj because jj automatically rebases changes whenever their ancestors change).
When in this state, the repository still technically works, but any time you reference one of these change IDs jj asks you which version of the change you meant, which gets old really fast.
I'd love a better solution here. For now, since I download CLs fairly rarely, I just use a separate git checkout for that purpose.
jj has a unique model where "everything is a commit". There is no index or staging area; when you have a commit checked out any modifications automatically become part of that commit.
This has interesting implications for the workflow you might use. Steve Klabnik's Tutorial covers two popular workflows you can use with jj.
I personally use something like the "squash" workflow, where I make all changes in an empty commit, which I then either turn into a named commit with jj commit, or squash into an existing commit with jj squash.
jj squash will squash the current commit into its immediate parent.
--into argument, you can squash into a commit further back in history.--from and --into, you can squash any commit in history into another.--from can be specified multiple times to squash a bunch of commits at once.This template should be added to ~/.config/jj/config.toml.
This extends the standard draft commit message to add a Change-Id line if one does not already exist.
If you split a commit into two, make sure to remove the Change-Id line from all but one of the commits to avoid duplicates.
Keep the original Change-Id on the commit you want assosciated with the existing Gerrit CL (if you have already uploaded.
jj describe the new commits to re-add a Change-Id line.
Thanks to Vamsi Avula and Filippo Valsorda for this suggestion.
[templates]
# 6a6a636c is hex("jjcl")
draft_commit_description = '''
concat(
description,
if(
!description.contains("Change-Id: I"),
"\n\n" ++ "Change-Id: I6a6a636c" ++ change_id.normal_hex() ++ "\n",
),
surround(
"\nJJ: This commit contains the following changes:\n", "",
indent("JJ: ", diff.summary()),
),
)
'''