Overengineering PR create with jj
This note is about a script I wrote that makes creating a PR marginally more convenient, and how it had to change when I switched to Jujutsu. It’s not an intro to Jujutsu — see the learning resources for that.
Make a PR in six steps, or one
You’ve written some code and you’re ready to make a PR. Getting to the PR create form on GitHub takes a lot of steps:
- Push your branch
- Find the repo on GitHub
- Go to pull requests
- Click “New pull request”
- Find the right branch (what did you call it?)
- Click “Create pull request”
Thanks to the wonderful GitHub CLI, you can do all that from the terminal in one line:
alias gprc='git push -u && gh pr create --web'
For git users, this post is over. Use that. I used it for 3 years. It’s the best.
In Jujutsu, you create the branch last
If you’re using Jujutsu, you can do more or less the same thing, except at this point you don’t have a named branch yet. In Jujutsu, you don’t start with a branch and work “on” that branch — there is no concept of a “current” branch. You just make commits on top of main
, and only when it’s time to push do you typically create a bookmark.1

If you’ve just switched from git, this is annoying because our beautiful gprc
isn’t enough anymore: you have to create a branch too, and naming a branch is slightly more than zero effort, i.e., too much effort.
Many jj users avoid naming the branch by passing --change <REVSETS>
to jj git push
, which tells jj to use the change ID, resulting in a branch like push-urzrzuzsurwx
.2 Indeed, branch names like that are one of the few signs visible on GitHub that someone is using jj.
Branch names matter, but not that much
The thing is, meaningful branch names can be useful. When your colleague wants to check out your work locally so they can test it, they’re probably using your branch name to find it. You might be using branch names yourself to switch between ongoing streams of work.
Branch names do not have to be perfect to be useful in this way. Unlike PR titles and descriptions (which tell people what you did and why, and which become commit messages on merge), branch names just need to vaguely point to the right change. Naming them is not some great art.
Let’s make the computer do it.
Generating a branch name
How can you automatically generate a decent branch name? It sounds impossible. Oh, right: in 2025, impossible things cost three hundredths of a cent.

Well, that was easy. I piped the diff and commit log to an LLM CLI3 that sent it to Gemini 2.5 Flash (the best fast and cheap model at time of writing) and got out a branch name in half a second for $0.00036. The only real trick here is using the LLM CLI as a super simple way of hooking into an LLM and knowing which model to use.4
Now let’s see what it takes to plug this operation into a usable script.
The final product: jprc
You can see the full jprc.ts
in my dotfiles, powered by Deno, dax, and Cliffy, my scripting tools of choice.

It’s 54 lines because it turns out there’s more you have to do besides generate the branch name. Half the lines are devoted to helping the user pick a base branch if there are any branches between main
and the target revision. Sometimes we are making a small PR on top of a big PR, and if we use the full diff since main
to generate the branch name, it will be not be specific enough to the small change. We will also pass the base branch to gh pr create
with --base
so we don’t have to manually change it from main
in the form.

Now we generate the branch name and prompt for confirmation:

After the branch name is confirmed, we create it locally, push it, and run gh pr create
as before.
Here’s the core logic:
// 1. Pick a base branch
const base = await pickBase(r)
// 2. Make sure base is on the remote
const result = await $`jj bookmark list --remote origin ${base}`.text()
if (!result) throw new ValidationError(`Base '${base}' not found on origin.`)
console.log(`\nCreating PR with base %c${base}\n`, "color: #ff6565")
// 3. Print the commit log
const range = `${base}..${r}`
await $`jj log -r ${range}`.printCommand()
// 4. Generate a branch name and confirm it (allowing editing)
const generated = await $`jj diff -r ${range}; jj log -r ${range}`
.pipe($`ai --system "${prompt}" -m flash --raw --ephemeral`)
.text()
// 5. Confirm branch name, allowing editing
const opts = { noClear: true, default: generated.trim() }
const bookmark = await $.prompt("\nCreate branch?", opts)
// 6. Push the branch
await $`jj git push --named ${bookmark}=${r}`.printCommand()
// 7. Create the PR
await $`gh pr create --head ${bookmark} --base ${base} --web`.printCommand()
Isn’t the LLM overkill?
It’s natural to wonder whether bringing LLMs into this little moment is overkill. But I think that reaction is based partly on an intuition I’ve shown to be wrong: that calling an LLM is complicated, costly, or risky. With the right tools and in the right scenario, LLMs fit so cleanly into Unix pipelines (text in, text out) that building a silly idea like this into a personal tool is trivial. That triviality makes it equally easy to ask “why bother?” and “why not?” It makes it like asking: isn’t bringing awk
into this script overkill? Maybe, but only if it’s doing absolutely nothing for you.
It’s worth thinking through why the downside risk of a potentially unreliable helper is so small in this particular case. The stakes are low, the output is human-reviewed (or otherwise easy to verify5), and it’s easy to recover if the branch name is bad: you just make up a different name. If any of those things weren’t true, the downside risk might be intolerable. It also needs to be fast and cheap, and to be good enough that you don’t have to recover very often — if the branch name was bad half the time, it wouldn’t be worth it. All of these are contingent factors that need to be evaluated case by case.
The LLM integration is one line of code, the one that starts with const generated = ...
. If we take that line out, instead prompting the user for the branch name, the script would still be worth using for the other things it does. In fact, that is how an earlier version of jprc
worked. With such a simple integration, it’s easy to try generating the branch name, evaluate whether it’s useful, and take it out if not.
Can you generate the PR title and body? Should you?
You could generate a PR title and body too and pass them to gh pr create
with --title
and --body
. I wouldn’t — my PR descriptions are a lot more about the why than the what, and that’s rarely represented in the diff or the commit messages. I would spend more time reading and rewriting whatever it produced than I would save from having it prefilled — this violates the requirement mentioned above that the output be easy to verify.
If you included the text of an issue that is closed by the PR, maybe the model could explain how the change closes the issue, but even that sounds dicey. Coding agents like Cursor or Claude Code probably have a better shot at generating decent PR bodies based on the conversation that produced the diff.
Jujutsu resources
- The jj README — I actually like this better than the intro on their site
- Steve’s Jujutsu Tutorial — everybody loves Steve
- Git and jujutsu: in miniature — the post that made it finally click for me
- What I’ve learned from jj — the “what’s cool about jj” post I wish I’d written
Footnotes
-
Branches are called bookmarks in Jujutsu. The name comes from Mercurial. The jj devs changed the name from branches to bookmarks while I was learning it, and for whatever reason the new name really helped me get past my git-based expectations. ↩
-
You can configure the prefix. People like to add their username to the front, like
david-crespo/push-
. ↩ -
I’m using my own LLM CLI here, but it’s pretty stripped down and tailored to my needs, so I usually recommend Simon Willison’s
llm
. ↩ -
I tried other light models like Claude 3.5 Haiku, GPT-4.1 mini, Llama 3.3 70B (through both Groq and Cerebras), and Llama 4 Maverick and Scout. They were mostly fine, though none was as consistently good as Gemini 2.5 Flash. Llama 4 Scout is clearly not good enough: at one point it generated the branch name
branch-names-are-hard
. ↩ -
I have in mind something like how typechecking and tests are a constraint on how wrong LLM-generated code can be. ↩