https://dotat.at/@/2025-09-11-cover-letter.html
Although it looks really good, I have not yet tried the Jujutsu (jj)
version control system, mainly
because it's not yet clearly superior to Magit.
But I have been following jj discussions with great interest.
One of the things that jj has not yet tackled is how to do better than
git refs / branches / tags. As I underestand it, jj currently has
something like Mercurial bookmarks, which are more like raw git ref
plumbing than a high-level porcelain feature. In particular, jj lacks
signed or annotated tags, and it doesn't have branch names that
always automatically refer to the tip.
This is clearly a temporary state of affairs because jj is still
incomplete and under development and these gaps are going to be
filled. But the discussions have led me to think about how git's
branches are unsatisfactory, and what could be done to improve them.
branch
One of the huge improvements in git compared to Subversion was git's
support for merges. Subversion proudly advertised its support for
lightweight branches, but a branch is not very useful if you can't
merge it: an un-mergeable branch is not a tool you can use to help
with work-in-progress development.
The point of this anecdote is to illustrate that rather than trying to
make branches better, we should try to make merges better and branches
will get better as a consequence.
Let's consider a few common workflows and how git makes them all
unsatisfactory in various ways. Skip to cover
letters and previous branch below
where I eventually get to the point.
merge
A basic merge workflow is,
- create a feature branch
- hack, hack, review, hack, approve
- merge back to the trunk
The main problem is when it comes to the merge, there may be conflicts
due to concurrent work on the trunk.
Git encourages you to resolve conflicts while creating the merge
commit, which tends to bypass the normal review process. Git also
gives you an ugly useless canned commit message for merges, that hides
what you did to resolve the conflicts.
If the feature branch is a linear record of the work then it can be
cluttered with commits to address comments from reviewers and to fix
mistakes. Some people like an accurate record of the history, but
others prefer the repository to contain clean logical changes that
will make sense in years to come, keeping the clutter in the code
review system.
rebase
A rebase-oriented workflow deals with the problems of the merge
workflow but introduces new problems.
Primarily, rebasing is intended to produce a tidy logical commit
history. And when a feature branch is rebased onto the trunk before it
is merged, a simple fast-forward check makes it trivial to verify that
the merge will be clean (whether it uses separate merge commit or
directly fast-forwards the trunk).
However, it's hard to compare the state of the feature branch before
and after the rebase. The current and previous tips of the branch
(amongst other clutter) are recorded in the reflog of the person who
did the rebase, but they can't share their reflog. A force-push erases
the previous branch from the server.
Git forges sometimes make it possible to compare a branch before and
after a rebase, but it's usually very inconvenient, which makes it
hard to see if review comments have been addressed. And a reviewer
can't fetch past versions of the branch from the server to review them
locally.
You can mitigate these problems by adding commits in --autosquash
format, and delay rebasing until just before merge. However that
reintroduces the problem of merge conflicts: if the autosquash doesn't
apply cleanly the branch should have another round of review to make
sure the conflicts were resolved OK.
squash
When the trunk consists of a sequence of merge commits, the
--first-parent
log is very uninformative.
A common way to make the history of the trunk more informative, and
deal with the problems of cluttered feature branches and poor rebase
support, is to squash the feature branch into a single commit on the
trunk instead of mergeing.
This encourages merge requests to be roughly the size of one commit,
which is arguably a good thing. However, it can be uncomfortably
confining for larger features, or cause extra busy-work co-ordinating
changes across multiple merge requests.
And squashed feature branches have the same merge conflict problem as
rebase --autosquash
.
fork
Feature branches can't always be short-lived. In the past I have
maintained local hacks that were used in production but were not (not
yet?) suitable to submit upstream.
I have tried keeping a stack of these local patches on a git branch
that gets rebased onto each upstream release. With this setup the
problem of reviewing successive versions of a merge request becomes
the bigger problem of keeping track of how the stack of patches
evolved over longer periods of time.
cover letters
Cover letters are common in the email patch workflow that predates
git, and they are supported by git format-patch. Github and other
forges have a webby version of the cover letter: the message that
starts off a pull request or merge request.
In git, cover letters are second-class citizens: they aren't stored in
the repository. But many of the problems I outlined above have neat
solutions if cover letters become first-class citizens, with a Jujutsu
twist.
A first-class cover letter starts off as a prototype for a merge
request, and becomes the eventual merge commit.
Instead of unhelpful auto-generated merge commits, you get helpful
and informative messages. No extra work is needed since we're
already writing cover letters.
Good merge commit messages make good --first-parent
logs.
The cover letter subject line works as a branch name. No more need
to invent filename-compatible branch names!
Jujutsu doesn't make you name branches, giving them random names
instead. It shows the subject line of the topmost commit as a
reminder of what the branch is for. If there's an explicit cover
letter the subject line will be a better summary of the branch as
a whole.
I often find the last commit on a branch is some post-feature
cleanup, and that kind of commit has a subject line that is never
a good summary of its feature branch.
As a prototype for the merge commit, the cover letter can contain
the resolution of all the merge conflicts in a way that can be
shared and reviewed.
In Jujutsu, where conflicts are first class, the cover letter
commit can contain unresolved conflicts: you don't have to clean
them up when creating the merge, you can leave that job until
later.
If you can share a prototype of your merge commit, then it becomes
possible for your collaborators to review any merge conflicts and
how you resolved them.
To distinguish a cover letter from a merge commit object, a cover
letter object has a "target" header which is a special kind of parent
header. A cover letter also has a normal parent commit header that
refers to earlier commits in the feature branch. The target is what
will become the first parent of the eventual merge commit.
previous branch
The other ingredient is to add a "previous branch" header, another
special kind of parent commit header. The previous branch header
refers to an older version of the cover letter and, transitively, an
older version of the whole feature branch.
Typically the previous branch header will match the last shared
version of the branch, i.e. the commit hash of the server's copy of
the feature branch.
The previous branch header isn't changed during normal work on the
feature branch. As the branch is revised and rebased, the commit hash
of the cover letter will change fairly frequently. These changes are
recorded in git's reflog or jj's oplog, but not in the "previous
branch" chain.
You can use the previous branch chain to examine diffs between
versions of the feature branch as a whole. If commits have
Gerrit-style or jj-style change-IDs then it's fairly easy to find and
compare previous versions of an individual commit.
The previous branch header supports interdiff code
review,
or allows you to retain past iterations of a patch series.
workflow
Here are some sketchy notes on how these features might work in
practice.
One way to use cover letters is jj-style, where it's convenient to
edit commits that aren't at the tip of a branch, and easy to reshuffle
commits so that a branch has a deliberate narrative.
When you create a new feature branch, it starts off as an empty
cover letter with both target and parent pointing at the same
commit.
Alternatively, you might start a branch ad hoc, and later cap it
with a cover letter.
If this is a small change and rebase + fast-forward is allowed,
you can edit the "cover letter" to contain the whole change.
Otherwise, you can hack on the branch any which way. Shuffle the
commits that should be part of the merge request so that they
occur before the cover letter, and edit the cover letter to
summarize the preceding commits.
When you first push the branch, there's (still) no need to give it
a name: the server can see that this is (probably) going to be a
new merge request because the top commit has a target branch and
its change-ID doesn't match an existing merge request.
Also when you push, your client automatically creates a new
instance of your cover letter, adding a "previous branch" header
to indicate that the old version was shared. The commits on the
branch that were pushed are now immutable; rebases and edits
affect the new version of the branch.
During review there will typically be multiple iterations of the
branch to address feedback. The chain of previous branch headers
allows reviewers to see how commits were changed to address
feedback, interdiff style.
The branch can be merged when the target header matches the
current trunk and there are no conflicts left to resolve.
When the time comes to merge the branch, there are several options:
For a merge workflow, the cover letter is used to make a new
commit on the trunk, changing the target header into the first
parent commit, and dropping the previous branch header.
Or, if you like to preserve more history, the previous branch
chain can be retained.
Or you can drop the cover letter and fast foward the branch on to
the trunk.
Or you can squash the branch on to the trunk, using the cover
letter as the commit message.
questions
This is a fairly rough idea: I'm sure that some of the details won't
work in practice without a lot of careful work on compatibility and
deployability.
Do the new commit headers ("target" and "previous branch") need to
be headers?
What are the compatibility issues with adding new headers that
refer to other commits?
How would a server handle a push of an unnamed branch? How could
someone else pull a copy of it?
How feasible is it to use cover letter subject lines instead of
branch names?
The previous branch header is doing a similar job to a remote
tracking branch. Is there an opportunity to simplify how we keep a
local cache of the server state?
Despite all that, I think something along these lines could make
branches / reviews / reworks / merges less awkward. How you merge
should me a matter of your project's preferred style, without
interference from technical limitations that force you to trade off
one annoyance against another.
There remains a non-technical limitation: I have assumed that
contributors are comfortable enough with version control to use a
history-editing workflow effectively. I've lost all perspective on how
hard this is for a newbie to learn; I expect (or hope?) jj makes it
much easier than git rebase.