I've been using exclusively [[jj | Jujutsu]] for the past two months (ever since I left Meta and joined Astral) for my version control needs. So far I'm liking it, I'll probably stick around. The setup I use will likely continue evolving, but this is the current snapshot:
![[Pasted image 20251027213756.png]]
This is [jjui](https://github.com/idursun/jjui), which I typically keep open all the time with the preview window open (the right side panel, which shows the selected commit's details) and is the primary way I navigate any repo. I've got a few customizations:
## jjui config
```toml
[custom_commands]
"new main" = { key = ["N"], args = ["new", "main"] }
"my work" = {key = ["M"], revset = "main|main..(@|bookmarks(zsol/)|my_heads())"}
"create pr" = {key = ["ctrl+p"], args = ["pr"]}
[custom_commands.tug]
key = ["T"]
args = ["bookmark", "move", "--from", "closest_bookmark($change_id)", "--to", "closest_pushable($change_id)"]
[custom_commands."move commit down"]
key = ["J"]
args = ["rebase", "-r", "$change_id", "--insert-before", "$change_id-"]
[custom_commands."move commit up"]
key = ["K"]
args = ["rebase", "-r", "$change_id", "--insert-after", "$change_id+"]
```
Pressing `shift-n` always makes a new commit off of `main`.
`shift-m` switches the commit graph (on the left side) to show only my commits, the current commit (`@` or `HEAD`), and how these all relate to `main`. This is helpful when rebasing my commits on each other or `main`. When I want to look at other stuff, I press `shift-l` and then `enter` (that's the default view).
`ctrl-p` creates a new GitHub pull request based off the closest relevant commit, opening a browser tab that lets me fill out the PR title, description, etc. The PR is only actually created when I submit the form in the browser.
`shift-t` tugs the closest bookmark towards the currently highlighted change. This is handy when I already have a bookmark, and have since made a few new commits and I want to update the bookmark to them. The selection is typically already on the new commit, so usually there's no need to move around before pressing `shift-t`.
Then there's `shift-j` and `shift-k` which drags the currently selected change up and down the history, sort of like how `alt-up` and `alt-down` drag the current line in modern IDEs like VSCode. I rarely need these, but when I do they're super satisfying.
There are a few non-standard helpers in the above config, which are defined in my:
## jj config
```toml
[ui]
editor = ["zed", "-w"]
conflict-marker-style = "git"
merge-editor = "meld"
diff-formatter = [
"difft", "--color=always",
"$left", "$right"
]
[[--scope]]
--when.commands = ["diff"]
[--scope.ui]
pager = "delta"
diff-formatter = ":git"
```
I use [Zed](https://zed.dev) as my main editor, which is fast enough to use in places I'd previously used `vim`, so it's configured here, too.
While I prefer jj's own conflict marker style for reading and understanding conflicts, the tooling to quickly resolve them just isn't there yet in my experience, so I fall back to git-style markers, and use `meld` or just the built-in Zed conflict resolver.
The default diff display is provided by the fantastic [difftastic](https://difftastic.wilfred.me.uk/) tool, but occasionally I want to look at the syntactic changes too, so when I type `jj diff` (or, more likely, press `d` in `jjui` on a change), I fall back to a git-style diff.
```toml
[templates]
git_push_bookmark = '"zsol/jj-" ++ change_id.short()'
[git]
fetch = "glob:*"
auto-local-bookmark = true
sign-on-push = true
[signing]
backend = "gpg"
behavior = "own"
```
jj can create new git branches based on the change ID, and `git_push_bookmark` customizes the branch name to include my username. I typically want to have `jj git fetch` (or `g f enter` in `jjui`) fetch all remotes, and make local tracking bookmarks for each.
Then I set up GPG signing (I think I have ssh key signing on another machine), and make signing only happen on pushes, since they're significantly slower than creating commits.
And now, for the exciting bits:
```toml
[revset-aliases]
'closest_bookmark(to)' = 'heads(::to & bookmarks())'
'closest_pushable(to)' = 'heads(::to & mutable() & ~description(exact:"") & (~empty() | merges()))'
'my_heads()' = '(visible_heads() & mine()) ~ empty()'
[aliases]
fetch = ["git", "fetch"]
tug = ["bookmark", "move", "--from", "closest_bookmark(@)", "--to", "closest_pushable(@)"]
pr = ["util", "exec", "--", "bash", "-c", """
set -ueo pipefail
bookmark=$(jj log -r 'closest_bookmark(@)' -T bookmarks --no-graph | awk '{print $1}')
trunk=$(jj log -r 'trunk()' -T bookmarks --no-graph | awk '{print $1}')
if [ "$bookmark" = "$trunk" ]; then
jj git push --allow-new -c @
bookmark=$(jj log -r 'closest_bookmark(@)' -T bookmarks --no-graph | awk '{print $1}')
fi
gh pr create --head $bookmark --fill-verbose --web
"""]
```
`closest_bookmark`, `fetch`, and `my_heads` are reasonably self-explanatory, and they power some of the customizations in `jjui` above.
`closest_pushable` maybe deserves a bit more explanation: it takes a change, and finds the nearest change that's non-empty (both in terms of changed files, as well as a commit message) and [mutable](https://jj-tutorial.github.io/tutorial/core-concepts/changes-commits-and-revisions.html#immutable-changes). These are generally good candidates for making PRs out of.
`tug` is reimplemented here, and it's almost the same thing that happens on `shift-t` in `jjui`: Take the `closest_bookmark()` from the currently checked out change (`@` here, but in `jjui` it's actually the selected change, which can be different from what's checked out), and move it to the `closest_pushable()` change.
The last one is a monster, mostly because it does two things:
1. Create a new bookmark if there's none, pushing it to the default remote, and then open a new browser window to create a PR out of it
2. If there's already a bookmark near this commit, simply create a PR out of it the same way.
## My PR workflow
With all the above, my workflow is typically:
1. Always start on an empty change off of main (`shift-n`)
2. Make some changes, then add a quick summary (`enter` then type the summary, then `ctrl-s` to save it)
3. Make a PR (`ctrl-p` and fill out the GitHub form)
Then, if I need to iterate more, I keep doing this in a loop:
1. Make a new change (`n`)
2. Make more changes (same as step 2)
Once I'm ready to update the PR, I can:
1. tug the auto-generated bookmark (`shift-t`)
2. push the new commits (`g p`)