jj bough, a useful alias for stacked branches
I’ve recently switched to using the new jj VCS, which you can think of as a Mercurial-like frontend for Git. I appreciate how it makes common workflows simpler by removing concepts from Git (like the index).
This week, I was working with a stack of branches with associated PRs. jj’s automatic rebasing already made this a really nice experience. But, I was pushing frequently - maybe more than I should have been, as I was just playing around and getting used to working this way with jj - and I found it a mild nuisance to push up all my branches.
Say I have a main branch, off which I’m working on a stack of two branches, stack/part-1 and stack/part-2.
I’ve just done some work in change pqr, and squashed that work into change def, so pqr is now empty:
○ mno [main]
|
| ○ jkl [stack/part-2] *
| |
| ○ ghi [stack/part-1] *
| |
| | ○ pqr @ (empty change)
| | /
| ○ def
| /
○ abc
|
.
The stack/part-1 and stack/part-2 branches are now both changed from what’s pushed to GitHub, and I need to push them.
If I just jj git push from @ where it is, nothing will happen, as there are no branches in the ancestors of @ that need pushing (i.e. changes abc and def don’t have branches on them).
I did try jj git push --all to push up all branches that need updating, but that was catching a lot of unrelated work, and felt… somehow wrong.
I resolved this with a quick set of commands:
# get "onto the top of" the stack
jj new stack/part-2
# push all branches "behind" me
jj git push
# get back to where I was
jj new def
This is fine, but:
- it’s a slight nuisance to have to navigate around instead of just telling jj what I want to do
- it wouldn’t work if there was no single “tip” of the stack, e.g. say I had
stack/part-3that also branched offpart-1, a “sibling” ofpart-2 - I wanted to learn about revsets!
Revsets are a concept that JJ borrows from Mercurial.
It’s a small expression language to refer to sets of changes.
I had fiddled with revsets when following this blog post to tweak my default jj log output, but hadn’t yet tried to use them to achieve a specific outcome.
In this case, I wanted a revset expression that would capture “all the changes in the subtree I’m in, until the point where it meets the trunk”. I decided to call this concept a “bough”, like a large branch of a tree that might have other branches coming off it.
Here’s the revset alias I ended up with in my JJ config file:
[revset-aliases]
'bough(x, m)' = 'descendants(ancestors(x) ~ ancestors(m))'
'bough(x)' = 'bough(x, trunk())'
'bough' = 'bough(@)'
This alias builds a set of all the ancestors (parents) of change x, then removes any changes that are also ancestors of the trunk commit, leaving the branch back from x, but then adds to that all the descendants (children) of those commits, which fills “forwards” to get the whole connected limb.
Notice how I defined some “convenience” aliases for ease of use:
boughby itself refers to the bough containing the current change (@) offtrunk()bough(x)selects the bough containing changexbough(x, m)allows changing the “trunk” that bough-ness is determined relative to, just in case I ever want that!
So in the diagram above, running jj log -r bough would have selected the commits marked with X:
○ mno [main]
|
| X jkl [stack/part-2]
| |
| X ghi [stack/part-1]
| |
| | X pqr @
| | /
| X def
| /
○ abc
|
.
Then, if I were to jj git push -r bough, it would notice both the stack/part-1 and stack/part-2 bookmarks needed pushing, as they were part of the bough.
I think this is a great demonstration of how jj’s concepts are composable - I can use the same revset alias to log or to push.
And, if I want to get really fancy, the inputs are also composable - here’s how to show two boughs containing different changes:
jj log -r 'bough(xyz | abc)'
I set this alias up halfway through working on my stack (which ended up being 6 branches!) and it made life just that little bit easier. I could spend less time navigating the repo when I needed to perform action on changes not in the direct line of ancestry of where I’m working.