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`)