Rebasing (or any other form of rewriting) a branch that others
have based work on is a bad idea: anyone downstream of it is
forced to manually fix their history. This section explains how
to do the fix from the downstream's point of view. The real fix,
however, would be to avoid rebasing the upstream in the first
place.
To illustrate, suppose you are in a situation where someone
develops a subsystem branch, and you are working on a topic that
is dependent on this subsystem. You might end up with a history
like the following:
o---o---o---o---o---o---o---o master
\
o---o---o---o---o subsystem
\
*---*---* topic
If subsystem is rebased against master, the following happens:
o---o---o---o---o---o---o---o master
\ \
o---o---o---o---o o'--o'--o'--o'--o' subsystem
\
*---*---* topic
If you now continue development as usual, and eventually merge
topic to subsystem, the commits from subsystem will remain
duplicated forever:
o---o---o---o---o---o---o---o master
\ \
o---o---o---o---o o'--o'--o'--o'--o'--M subsystem
\ /
*---*---*-..........-*--* topic
Such duplicates are generally frowned upon because they clutter
up history, making it harder to follow. To clean things up, you
need to transplant the commits on topic to the new subsystem tip,
i.e., rebase topic. This becomes a ripple effect: anyone
downstream from topic is forced to rebase too, and so on!
There are two kinds of fixes, discussed in the following
subsections:
Easy case: The changes are literally the same.
This happens if the subsystem rebase was a simple rebase and
had no conflicts.
Hard case: The changes are not the same.
This happens if the subsystem rebase had conflicts, or used
--interactive
to omit, edit, squash, or fixup commits; or if
the upstream used one of commit --amend
, reset
, or a full
history rewriting command like filter-repo
[2].
The easy case
Only works if the changes (patch IDs based on the diff contents)
on subsystem are literally the same before and after the rebase
subsystem did.
In that case, the fix is easy because git rebase knows to skip
changes that are already present in the new upstream (unless
--reapply-cherry-picks
is given). So if you say (assuming you're
on topic)
$ git rebase subsystem
you will end up with the fixed history
o---o---o---o---o---o---o---o master
\
o'--o'--o'--o'--o' subsystem
\
*---*---* topic
The hard case
Things get more complicated if the subsystem changes do not
exactly correspond to the ones before the rebase.
Note
While an "easy case recovery" sometimes appears to be
successful even in the hard case, it may have unintended
consequences. For example, a commit that was removed via git
rebase --interactive
will be resurrected
!
The idea is to manually tell git rebase "where the old subsystem
ended and your topic began", that is, what the old merge base
between them was. You will have to find a way to name the last
commit of the old subsystem, for example:
• With the subsystem reflog: after git fetch, the old tip of
subsystem is at subsystem@{1}
. Subsequent fetches will
increase the number. (See git-reflog(1).)
• Relative to the tip of topic: knowing that your topic has
three commits, the old tip of subsystem must be topic~3
.
You can then transplant the old subsystem..topic
to the new tip
by saying (for the reflog case, and assuming you are on topic
already):
$ git rebase --onto subsystem subsystem@{1}
The ripple effect of a "hard case" recovery is especially bad:
everyone downstream from topic will now have to perform a "hard
case" recovery too!