Jujutsu megamerges for fun and profit
73 points by knl
73 points by knl
Megamerges are one of the reasons why I use Jujutsu. But the main reason is the ability to defer solving conflicts.
With 'git rebase' you either have to solve a conflict immediately or abandon the rebase (and I'm always worried I solved them wrong and there won't be a history). With 'jj' if I find myself I side a complicated conflict resolution I can look at the history and choose to edit some ancestors to maybe eliminate the conflict in the first place (especially for add/add conflicts if I just add my variables or functions in different places that can sometimes avoid the conflict; refactoring code on both sides can also help). And there is always the 'jj' operation log that can track how a commit evolved over rebases/merges/splits (unfortunately only local, and can't be pushed to a remote for backup).
This improved conflict handling then makes it very easy to use the mega merge workflow. It can also be used as a "test merge" of all your pending PRs, to try to keep them conflict free as much as possible, especially as they evolve during a review.
You mention it in passing, but the ability to do jj undo to undo the last operation is pretty incredible when you are used to git.
Yes, and there are also other nice features.
For example you can configure it to auto format commits automatically, e.g. with ocamlformat: https://codeberg.org/edwintorok/dotfiles/src/branch/master/jj/.config/jj/config.toml This can be done using a merge driver with 'git', but it is a bit more automatic with 'jj' (it reformats all sides of a merge, so can avoid most of the conflicts that'd arrive from reformatting, though not all)
There are a few downsides as well:
GitHub changes the commit hash on merge (it adds the person that merged the PR as committer), so your merged PR branches will show up as divergent, and you have to explicitly abandon your version
By default it auto adds all files from the local dir that is not ignored, so your gitignore files must be up to date (or have a good global gitignore), otherwise you end up adding random pieces of log files and test outputs to your commits without realizing. Especially annoying if this happens when you run 'jj bisect'.
Newer versions of 'jj' keep requiring newer versions of Rust than available in distro repositories (unless you are on a bleeding edge rolling distro), so you end up having to install the latest Rust explicitly.
merged PR branches will show up as divergent, and you have to explicitly abandon your version
For this one, I use rebase --skip-emptied. For example, I have this alias:
git-sync = ["util", "exec", "--", "bash", "-c", """
set -euo pipefail
jj git fetch
jj rebase -b @ -d main --skip-emptied
"""]
And then I generally run jj git-sync from my branches (or even from main).
my personal favs are jj op log and doing rebase in jjui. It feels very intuitive. also not having to think in terms of branches.
With 'git rebase' you either have to solve a conflict immediately or abandon the rebase
Yes, this is rather awkward. When doing complex rebases in git I sometimes fall back to using manual cherry-picking so I don't risk losing merge resolutions if I get stuck.
I'm always worried I solved them wrong and there won't be a history
There will be a history. To see everything your branch has ever been (even prior to rebases) you can just paste this command into your terminal.
BRANCH=$(git branch --show-current); \
PAGER="less -S" \
git log --graph \
--decorate \
--pretty=format:"%C(auto)%h %<(7,trunc)%C(auto)%ae%Creset%C(auto)%d %s [%ar]%Creset" \
$(git reflog $BRANCH | cut '-d ' -f1)
Find more of my top git tips in my git survival guide.
Megamerges being easy to manage is awesome.
The section on absorbing changes is technically correct but the jj absorb isn't good enough in practice to work from a megamerge and then absorb your changes. If you try to work like this you'll be disappointed. I can't explain exactly what it does wrong but it at least (a) chooses target commits poorly and (b) it even puts you in a conflict state sometimes. I've used mercurial/sapling absorb thousands of times with no surprise behavior ever but jj just has something wrong with the algorithm. Or my mental model needs adjusting but I don't understand how.
I read the trunk() mention and I wanted to say you can alias that away: set a revset alias of T = trunk(). Then you can jj rebase -d T. The uppercase avoids conflicting with change ids.
I use '/' = 'trunk()' so for me it's jj rebase -d/
T also works, but I have found that / maps to my mental model of a "root" better.
It’s important to remember that you don’t push the megamerge
You absolutely can though! It's not the most common use case, but for something like an operating system fork where you have a whole bunch of patchsets you want to have "active" at the same time it's totally appropriate.
Very neat stack/stage aliases, I've been mostly doing this kind of thing visually in jjui but I'll take them for a spin next time! For restack I've had a rebaseall = ["rebase", "--source", "all:roots(trunk()..@)", "--onto", "trunk()"]… hm, I really don't remember what's with the all: haha.
I've also had this one addparent = ["rebase", "--source", "@", "--onto", "all:@-", "--onto"] for only manually adding a parent to the octomerge, but I haven't really been using it since discovering jjui.
Can someone help me understand how a megamerge workflow is better than working on stacked commits in a linear graph? I suppose if the streams of work are not dependent, a megamerge allows you to work on them simultaneously with less risk of merge conflicts. Is that basically it?
Yeah that's pretty much it. Megamerges are very helpful when you're working on something and spot an unrelated thing worth fixing. The Git worktree and/or branch switching workflow are too much ceremony for a 10 line patch so either the thing doesn't get done or gets pulled into the current stream where it doesn't belong.
The "standard" workflow is to just have a bunch of commits off of main and then rebase as those commits land. Given how good jj is at rebasing, and given the fact that I generally don't want to accidentally couple unrelated streams of work, I'm not sure how much I would gain from a megamerge workflow. Perhaps I'm missing some other benefits, though.
That's interesting, I have been using megamerges in git and I didn't know they had a name already! (I call them "join merges" but "megamerges" is much more descriptive.) I haven't used jj and I wasn't able to understand from the article whether it has some special support for megamerges, or whether it just has a lot of features that just happen to make it reasonably nice to work with megamerges (I think the latter?).
I have been using megamerges in git and I didn't know they had a name already!
I believe git calls them octopus merges, both in the CLI and the docs. The article even mentions the term in passing (although the article draws a distinction, saying that "octopus merges power the entire megamerge workflow", but I don't see a difference; they are one and the same).
I use them all the time in Git; what makes them usable is the wonderful combination of git rebase --interactive --keep-base --rebase-merges --update-refs.
what makes them usable is the wonderful combination of
git rebase --interactive --keep-base --rebase-merges --update-refs.
This is very interesting, thanks! I will give it a try.
Man I just don't get jujtsu at all. The first screenshot in this blog post looks like alien language to me. Like, I read at least 6-10 different blog posts about it over the last 3 months, several different introductions as well as more advanced stuff like this. I don't get it. I don't understand what's happening, why it's happening, or why i would want to use it over git. Maybe I just don't do complicated enough stuff with VCS.
Some context: I work mostly on my own projects or with like 1-2 other people. When I work alone, I push to main or make some prs, and with people I/they make prs, review and merge. I use the git integration in vscode to self-review and commit, and the git cli or more recently lazygit for "more complex" operations (discarding, reverting, merging, stashing).
If anyone wants to try giving me a sort of high-level explanation of jj, I'm all ears.