Jujutsu megamerges for fun and profit

This article is written for both intermediate Jujutsu users and Git users who are curious about Jujutsu.

I’m a big Jujutsu user, and I’ve found that I’m becoming more and more dependent on what we colloquially call the “MegaMerge” workflow in the JJ community for my daily development. It’s surprisingly little discussed outside of a handful of power users, so I wanted to share what it looks like and why it’s so useful, especially if you’re in a complex development environment or ship a lot of small PRs.

In a hurry? Skip to the end for some quick tips.

Merge commits are not what you think

If you’re an average Git user (or even a Jujutsu user who hasn’t delved too deeply into more advanced workflows), you might be surprised to learn that there’s nothing special about merge commits. This is not a special case with its own rules. This is simply a common commitment many parents have. It doesn’t even have to be empty!1

@  myzpxsys Isaac Corbrey 12 seconds ago 634e82e2
(empty) (no description set)
mllmtkmv Isaac Corbrey 12 seconds ago git_head() 947a52fd
├─╮  (empty) Merge the things
│ ○  vqsqmtlu Isaac Corbrey 12 seconds ago f41c796e
│ │  deps: Pin quantum manifold resolver
○ │  tqqymrkn Isaac Corbrey 19 seconds ago 0426baba
├─╯  storage: Align transient cache manifolds
  zzzzzzzz root() 00000000
Gotta put it all together!

You may be even more surprised to learn that merge commitment is not limited to just having two parents. We informally call merge commits with three or more parents an “octopus merge”, and while you may be thinking to yourself “in what world would I want to merge more than two branches?”, it’s actually a pretty powerful idea. Octopus merges the power of the entire MegaMerge workflow!

So what is Megamerge?

Basically, in a megamerge workflow you are rarely working directly from the tips of your branches. Instead, you create an Octopus merge commit (hereafter referred to as a “megamerge”) as a child of each working branch you care about. This means bug fixes, feature branches, branches you’re waiting on a PR for, other people’s branches that you need your code to work with, local environment setup branches, even private commits that may not be in any branch or belong to any other branch.
Everything You care about going to megamerge. It is important to remember You do not proceed with the megamerge.Only the branches that make it up.

@  mnrxpywt Isaac Corbrey 25 seconds ago f1eb374e
(empty) (no description set)
wuxuwlox Isaac Corbrey 25 seconds ago git_head() c40c2d9c
├─┬─╮  (empty) megamerge
│ │ ○  ttnyuntn Isaac Corbrey 57 seconds ago 7d656676
│ │ │  storage: Align transient cache manifolds
│ ○ │  ptpvnsnx Isaac Corbrey 25 seconds ago 897d21c7
│ │ │  parser: Deobfuscate fleem tokens
│ ○ │  zwpzvxmv Isaac Corbrey 37 seconds ago 14971267
│ │ │  infra: Refactor blob allocator
│ ○ │  tqxoxrwq Isaac Corbrey 57 seconds ago 90bf43e4
│ ├─╯  io: Unjam polarity valves
○ │  moslkvzr Isaac Corbrey 50 seconds ago 753ef2e7
│ │  deps: Pin quantum manifold resolver
○ │  qupprxtz Isaac Corbrey 57 seconds ago 5332c1fd
├─╯  ui: Defrobnicate layout heuristics
wwtmlyss Isaac Corbrey 57 seconds ago 5804d1fd
│  test: Add hyperfrobnication suite
  zzzzzzzz root() 00000000
Scary! Too much merge!

It’s okay if it seems like too much. After all, you know, among other things, how much effort it takes to change the context if you have to revisit an old PR to review it. However, it enables some really valuable things for you:

  1. You are always working on the combined sum of all your actions. This means that if your working copy compiles and runs without any problems, you know that all your work will interact without any problems.
  2. You will rarely have to worry about merge conflicts. You don’t have to worry about merge conflicts in the first place because conflicts are a first-class concept in Jujutsu, but since you’re literally always merging your changes together, you’ll never be hit with surprise merge conflicts on the Forge side. There can sometimes be problems with changes made by contributors, but in my experience this has not been a major problem.
  3. There is less friction when switching between tasks. Since you’re always working on top of MegaMerge, you never need to go to your VCS to change tasks. You can edit what you want. This also means it’s easier to create small PRs for drive-by refactors and bugfixes.
  4. It’s easy to keep your branches updated. With a little magic, you can keep your entire megamerge up to date with your trunk branch with a single rebase command. I’ll show you how to do this later.

How do I make it?

Starting a megamerge is very simple: just create a new commit with each branch you want in the megamerge as a parent. I like to give that commit a name and leave it blank, like this:

jj new x y z
jj commit --message "megamerge"

Creating a megamerge. It’s not that hard after all!

Then you’re left with an empty commitment on top of the whole thing. This is where you do your work! Anything above the megamerge commit is considered WIP. You’re free to split things as needed, create multiple branches based on that megamerge commit, do whatever you want. Everything you write will be based on the sum of everything inside MegaMerge, just the way we wanted!

Of course, at some point you will be happy with what you have, and you will be surprised:

How do I actually submit my changes?

How you get your WIP changes into your megamerge depends on where they land. If you are making changes that should be included in existing changes, you can use squash order with --to Flag them for tweaking in the right downstream commit. If your commit contains changes from multiple commits, you can either split Split it into multiple commits before squashing them or (which I prefer) interactively squash --interactive To select only specific pieces to move.

# Squash an entire WIP commit (defaults to `--from @`)
jj squash --to x --from y

# Interactively squash part of a WIP commit (defaults to `--from @`)
jj squash --to x --from y --interactive

Hunk, I choose you!

Of course, Jujutsu is beautiful software and it even has some automation in it! absorb The command will do a lot for you by identifying which downstream mutable commit each line or chunk of your current commit belongs to and
automatically crush them for you. It feels like magic every time I use it (and not evil black box black magic where nothing can be understood), and it’s one of the core pieces of Jujutsu’s functionality that makes the MegaMerge workflow so intuitive.

# Automagically autosquash your changes (defaults to `--from @`)
jj absorb --from x

Oh, that was fast.

Absorption won’t always catch everything in your commit, but it will usually catch at least 90% of your changes. The rest are either easily squashable downstream or unrelated to any previous commit.

Conveniently, if I have the changes in the new commit things aren’t much more complicated. If the commit is in one of the branches I’m working on, I can rebase it myself and move the bookmark accordingly.

jj commit
jj rebase --revision x --after y --before megamerge
jj bookmark move --from y --to x

Let’s break down that rebase to better understand how it works:

# We're gonna move some commits around!
jj rebase
    # Let's move our WIP commit(s) x...
    --revision x
        # so that they come after y (e.g. trunk())...
        --after y
            # and become a parent of the megamerge.
            --before megamerge

A little rocket surgery as a cure.

If I’ve started work on a completely new feature or found an unrelated bug to fix, it’s even easier! Using some aliases, I can easily incorporate new changes into my megamerge:2

There are also template aliases that let you change the way you log to the Jujutsu terminal using the templating language, and fileset aliases, which function similarly to revset aliases but act on files instead of modifications using the fileset language.

[revset-aliases]
# Returns the closest merge commit to `to`
"closest_merge(to)" = "heads(::to & merges())"

[aliases]
# Inserts the given revset as a new branch under the megamerge.
stack = ["rebase", "--after", "trunk()", "--before", "closest_merge(@)", "--revision"]

Here’s a quick explanation of what closest_merge(to) Actually doing:

heads(                 # Return only the topologically tip-most commit within...
      ::to             # the set of all commits that are ancestors of `to`...
           & merges()) # ...that are also merge commits.

Using that rivset alias, stack Let’s target any revset we want and put it in the middle trunk() (your main development branch) and our megamerge commitment:

jj stack x::y

Wow, that was neat!

It’s more useful if I have multiple Stacks of changes I want to incorporate in parallel; If it’s just one, I have another alias that gets the whole stack of changes after the megamerge:

[aliases]
stage = ["stack", "closest_merge(@).. ~ empty()"]
closest_merge(@)..           # Return the descendants of the closest merge
                             # commit to the working copy...
                   ~ empty() # ...without any empty commits.

This requires no input! Just have your commitments ready and make them step by step:

jj stage

What are you waiting for? you can do that?

The last missing piece of this megamerge puzzle is (unfortunately) to deal with reality. other people: :

How do I keep all this nonsense up to date?

This is a great question, and I spent a few months trying to answer it in a general sense. Jujutsu has a very easy way to rebase your entire working tree onto your main branch:

jj rebase --onto trunk()

Good.

However, this only works if your entire worktree is Yours Change. When you try to reference commits that are not owned by you (such as untracked bookmarks or other people’s branches) Jujutsu will close them quickly to prevent them from being rewritten.3

Wait, not so good. How do I do this?

Let’s fix this by rebasing only the commits we actually control. I struggled with this for a while, but thankfully the Jujutsu community is amazing. Congratulations to Stephen Jennings for coming up with this wonderful review:

[aliases]
restack = ["rebase", "--onto", "trunk()", "--source", "roots(trunk()..) & mutable()"]
roots(                       # Get the furthest upstream commits...
      trunk()..)             # ...in the set of all descendants of ::trunk()...
                 & mutable() # ...and only return ones we're allowed to modify.

Instead of trying to rebase our entire working tree (e.g. jj rebase --onto trunk() ), this alias only targets commits that we are actually allowed to move. This leaves behind branches we don’t control as well as work that is placed on top of other people’s branches. It hasn’t failed me yet, even with the monstrous ninefold mixed-contributor megamerge! (say five times fast)

There we go, it’s better!

TL;DR

Jujutsu MegaMerges are great and let you work on multiple different streams at once. Read the full article for an in-depth explanation of how they work. For a super ergonomic setup, add these to your configuration jj config edit --user: :

[revset-aliases]
"closest_merge(to)" = "heads(::to & merges())"

[aliases]
# `jj stack ` to include specific revs
stack = ["rebase", "--after", "trunk()", "--before", "closest_merge(@)", "--revision"]

# `jj stage` to include the whole stack after the megamerge
stage = ["stack", "closest_merge(@).. ~ empty()"]

# `jj restack` to rebase your changes onto `trunk()`
restack = ["rebase", "--onto", "trunk()", "--source", "roots(trunk()..) & mutable()"]

Use absorb and/or squash --interactive To receive new changes to existing commitments, commit And rebase to make new commitments under its megamerge, and commit with stack Or stage To move entire branches into your megamerge.4

# Changes that belong in existing commits
jj absorb
jj squash --to x --interactive

# Changes that belong in new commits
jj rebase --revision y --after x

# Stack anything on top of the megamerge into it
jj stage

# Stack specific revsets into the megamerge
jj stack w::z

Remember that MegaMerges aren’t actually meant to be pushed to your remote; They’re a convenient way to show yourself the full picture. You’ll still want to publish branches individually as usual.

I live in it constantly, and you can too.

Megamerges may not be everyone’s cup of tea – I certainly got a few horrified looks after showing off my working tree – but once you try them, you’ll likely find that they let you bounce between tasks with almost no effort. Give them a try!



<a href

Leave a Comment