Git's documentation for the rebase
command is quite brief:
--preserve-merges
Instead of ignoring merges, try to recreate them.
This uses the --interactive machinery internally, but combining it
with the --interactive option explicitly is generally not a good idea
unless you know what you are doing (see BUGS below).
So what actually happens when you use --preserve-merges
? How does it differ from the default behavior (without that flag)? What does it mean to "recreate" a merge, etc.
Git 2.18 (Q2 2018) will improve considerably the
--preserve-merge
option by adding a new option."
git rebase
" learned "--rebase-merges
" to transplant the whole topology of commit graph elsewhere.See commit 25cff9f, commit 7543f6f, commit 1131ec9, commit 7ccdf65, commit 537e7d6, commit a9be29c, commit 8f6aed7, commit 1644c73, commit d1e8b01, commit 4c68e7d, commit 9055e40, commit cb5206e, commit a01c2a5, commit 2f6b1d1, commit bf5c057 (25 Apr 2018) by Johannes Schindelin (
dscho
).See commit f431d73 (25 Apr 2018) by Stefan Beller (
stefanbeller
).See commit 2429335 (25 Apr 2018) by Phillip Wood (
phillipwood
).(Merged by Junio C Hamano --
gitster
-- in commit 2c18e6a, 23 May 2018)git rebase
man page now has a full section dedicated to rebasing history with merges.Extract:
See commit 1644c73 for a small example:
What is the difference with
--preserve-merge
?Commit 8f6aed7 explains:
And by "yours truly", the author refers to himself: Johannes Schindelin (
dscho
), who is the main reason (with a few other heros -- Hannes, Steffen, Sebastian, ...) that we have Git For Windows (even though back in the day -- 2009 -- that was not easy).He now works at Microsoft, since Microsoft manages the largest Git repository on the planet!
You can see Johannes speak in this video for Git Merge 2018 in April 2018.
Here Jonathan is talking about Andreas Schwab from Suse.
You can see some of their discussions back in 2012.
(The Git garden shears script is referenced in this patch in commit 9055e40)
Git 2.19 (Q3 2018) improves the new
--rebase-merges
option by making it work with--exec
.The "
--exec
" option to "git rebase --rebase-merges
" placed the exec commands at wrong places, which has been corrected.See commit 1ace63b (09 Aug 2018), and commit f0880f7 (06 Aug 2018) by Johannes Schindelin (
dscho
).(Merged by Junio C Hamano --
gitster
-- in commit 750eb11, 20 Aug 2018)As with a normal git rebase, git with
--preserve-merges
first identifies a list of commits made in one part of the commit graph, and then replays those commits on top of another part. The differences with--preserve-merges
concern which commits are selected for replay and how that replaying works for merge commits.To be more explicit about the main differences between normal and merge-preserving rebase:
git checkout <desired first parent>
), whereas normal rebase doesn't have to worry about that.First I will try to describe "sufficiently exactly" what rebase
--preserve-merges
does, and then there will be some examples. One can of course start with the examples, if that seems more useful.The Algorithm in "Brief"
If you want to really get into the weeds, download the git source and explore the file
git-rebase--interactive.sh
. (Rebase is not part of Git's C core, but rather is written in bash. And, behind the scenes, it shares code with "interactive rebase".)But here I will sketch what I think is the essence of it. In order to reduce the number of things to think about, I have taken a few liberties. (e.g. I don't try to capture with 100% accuracy the precise order in which computations take place, and ignore some less central-seeming topics, e.g. what to do about commits that have already been cherry-picked between branches).
First, note that a non-merge-preserving rebase is rather simple. It's more or less:
Rebase
--preserve-merges
is comparatively complicated. Here's as simple as I've been able to make it without losing things that seem pretty important:Rebase with an
--onto C
argument should be very similar. Just instead of starting commit playback at the HEAD of B, you start commit playback at the HEAD of C instead. (And use C_new instead of B_new.)Example 1
For example, take commit graph
m is a merge commit with parents E and G.
Suppose we rebased topic (H) on top of master (C) using a normal, non-merge-preserving rebase. (For example, checkout topic; rebase master.) In that case, git would select the following commits for replay:
and then update the commit graph like so:
(D' is the replayed equivalent of D, etc..)
Note that merge commit m is not selected for replay.
If we instead did a
--preserve-merges
rebase of H on top of C. (For example, checkout topic; rebase --preserve-merges master.) In this new case, git would select the following commits for replay:Now m was chosen for replay. Also note that merge parents E and G were picked for inclusion before merge commit m.
Here is the resulting commit graph:
Again, D' is a cherry-picked (i.e. recreated) version of D. Same for E', etc.. Every commit not on master has been replayed. Both E and G (the merge parents of m) have been recreated as E' and G' to serve as the parents of m' (after rebase, the tree history still remains the same).
Example 2
Unlike with normal rebase, merge-preserving rebase can create multiple children of the upstream head.
For example, consider:
If we rebase H (topic) on top of C (master), then the commits chosen for rebase are:
And the result is like so:
Example 3
In the above examples, both the merge commit and its two parents are replayed commits, rather than the original parents that the original merge commit have. However, in other rebases a replayed merge commit can end up with parents that were already in the commit graph before the merge.
For example, consider:
If we rebase topic onto master (preserving merges), then the commits to replay will be
The rewritten commit graph will look like so:
Here replayed merge commit m' gets parents that pre-existed in the commit graph, namely D (the HEAD of master) and E (one of the parents of the original merge commit m).
Example 4
Merge-preserving rebase can get confused in certain "empty commit" cases. At least this is true only some older versions of git (e.g. 1.7.8.)
Take this commit graph:
Note that both commit m1 and m2 should have incorporated all the changes from B and F.
If we try to do
git rebase --preserve-merges
of H (topic) onto D (master), then the following commits are chosen for replay:Note that the changes (B, F) united in m1 should already be incorporated into D. (Those changes should already be incorporated into m2, because m2 merges together the children of B and F.) Therefore, conceptually, replaying m1 on top of D should probably either be a no-op or create an empty commit (i.e. one where the diff between successive revisions is empty).
Instead, however, git may reject the attempt to replay m1 on top of D. You can get an error like so:
It looks like one forgot to pass a flag to git, but the underlying problem is that git dislikes creating empty commits.