|
|
|
Date: Fri, 19 Dec 2008 00:45:19 -0800
|
|
|
|
From: Linus Torvalds <torvalds@linux-foundation.org>, Junio C Hamano <gitster@pobox.com>
|
|
|
|
Subject: Re: Odd merge behaviour involving reverts
|
|
|
|
Abstract: Sometimes a branch that was already merged to the mainline
|
|
|
|
is later found to be faulty. Linus and Junio give guidance on
|
|
|
|
recovering from such a premature merge and continuing development
|
|
|
|
after the offending branch is fixed.
|
|
|
|
Message-ID: <7vocz8a6zk.fsf@gitster.siamese.dyndns.org>
|
|
|
|
References: <alpine.LFD.2.00.0812181949450.14014@localhost.localdomain>
|
|
|
|
|
|
|
|
Alan <alan@clueserver.org> said:
|
|
|
|
|
|
|
|
I have a master branch. We have a branch off of that that some
|
|
|
|
developers are doing work on. They claim it is ready. We merge it
|
|
|
|
into the master branch. It breaks something so we revert the merge.
|
|
|
|
They make changes to the code. they get it to a point where they say
|
|
|
|
it is ok and we merge again.
|
|
|
|
|
|
|
|
When examined, we find that code changes made before the revert are
|
|
|
|
not in the master branch, but code changes after are in the master
|
|
|
|
branch.
|
|
|
|
|
|
|
|
and asked for help recovering from this situation.
|
|
|
|
|
|
|
|
The history immediately after the "revert of the merge" would look like
|
|
|
|
this:
|
|
|
|
|
|
|
|
---o---o---o---M---x---x---W
|
|
|
|
/
|
|
|
|
---A---B
|
|
|
|
|
|
|
|
where A and B are on the side development that was not so good, M is the
|
|
|
|
merge that brings these premature changes into the mainline, x are changes
|
|
|
|
unrelated to what the side branch did and already made on the mainline,
|
|
|
|
and W is the "revert of the merge M" (doesn't W look M upside down?).
|
|
|
|
IOW, "diff W^..W" is similar to "diff -R M^..M".
|
|
|
|
|
|
|
|
Such a "revert" of a merge can be made with:
|
|
|
|
|
|
|
|
$ git revert -m 1 M
|
|
|
|
|
|
|
|
After the developers of the side branch fix their mistakes, the history
|
|
|
|
may look like this:
|
|
|
|
|
|
|
|
---o---o---o---M---x---x---W---x
|
|
|
|
/
|
|
|
|
---A---B-------------------C---D
|
|
|
|
|
|
|
|
where C and D are to fix what was broken in A and B, and you may already
|
|
|
|
have some other changes on the mainline after W.
|
|
|
|
|
|
|
|
If you merge the updated side branch (with D at its tip), none of the
|
|
|
|
changes made in A nor B will be in the result, because they were reverted
|
|
|
|
by W. That is what Alan saw.
|
|
|
|
|
|
|
|
Linus explains the situation:
|
|
|
|
|
|
|
|
Reverting a regular commit just effectively undoes what that commit
|
|
|
|
did, and is fairly straightforward. But reverting a merge commit also
|
|
|
|
undoes the _data_ that the commit changed, but it does absolutely
|
|
|
|
nothing to the effects on _history_ that the merge had.
|
|
|
|
|
|
|
|
So the merge will still exist, and it will still be seen as joining
|
|
|
|
the two branches together, and future merges will see that merge as
|
|
|
|
the last shared state - and the revert that reverted the merge brought
|
|
|
|
in will not affect that at all.
|
|
|
|
|
|
|
|
So a "revert" undoes the data changes, but it's very much _not_ an
|
|
|
|
"undo" in the sense that it doesn't undo the effects of a commit on
|
|
|
|
the repository history.
|
|
|
|
|
|
|
|
So if you think of "revert" as "undo", then you're going to always
|
|
|
|
miss this part of reverts. Yes, it undoes the data, but no, it doesn't
|
|
|
|
undo history.
|
|
|
|
|
|
|
|
In such a situation, you would want to first revert the previous revert,
|
|
|
|
which would make the history look like this:
|
|
|
|
|
|
|
|
---o---o---o---M---x---x---W---x---Y
|
|
|
|
/
|
|
|
|
---A---B-------------------C---D
|
|
|
|
|
|
|
|
where Y is the revert of W. Such a "revert of the revert" can be done
|
|
|
|
with:
|
|
|
|
|
|
|
|
$ git revert W
|
|
|
|
|
|
|
|
This history would (ignoring possible conflicts between what W and W..Y
|
|
|
|
changed) be equivalent to not having W nor Y at all in the history:
|
|
|
|
|
|
|
|
---o---o---o---M---x---x-------x----
|
|
|
|
/
|
|
|
|
---A---B-------------------C---D
|
|
|
|
|
|
|
|
and merging the side branch again will not have conflict arising from an
|
|
|
|
earlier revert and revert of the revert.
|
|
|
|
|
|
|
|
---o---o---o---M---x---x-------x-------*
|
|
|
|
/ /
|
|
|
|
---A---B-------------------C---D
|
|
|
|
|
|
|
|
Of course the changes made in C and D still can conflict with what was
|
|
|
|
done by any of the x, but that is just a normal merge conflict.
|
|
|
|
|
|
|
|
On the other hand, if the developers of the side branch discarded their
|
|
|
|
faulty A and B, and redone the changes on top of the updated mainline
|
|
|
|
after the revert, the history would have looked like this:
|
|
|
|
|
|
|
|
---o---o---o---M---x---x---W---x---x
|
|
|
|
/ \
|
|
|
|
---A---B A'--B'--C'
|
|
|
|
|
|
|
|
If you reverted the revert in such a case as in the previous example:
|
|
|
|
|
|
|
|
---o---o---o---M---x---x---W---x---x---Y---*
|
|
|
|
/ \ /
|
|
|
|
---A---B A'--B'--C'
|
|
|
|
|
|
|
|
where Y is the revert of W, A' and B' are rerolled A and B, and there may
|
|
|
|
also be a further fix-up C' on the side branch. "diff Y^..Y" is similar
|
|
|
|
to "diff -R W^..W" (which in turn means it is similar to "diff M^..M"),
|
|
|
|
and "diff A'^..C'" by definition would be similar but different from that,
|
|
|
|
because it is a rerolled series of the earlier change. There will be a
|
|
|
|
lot of overlapping changes that result in conflicts. So do not do "revert
|
|
|
|
of revert" blindly without thinking..
|
|
|
|
|
|
|
|
---o---o---o---M---x---x---W---x---x
|
|
|
|
/ \
|
|
|
|
---A---B A'--B'--C'
|
|
|
|
|
|
|
|
In the history with rebased side branch, W (and M) are behind the merge
|
|
|
|
base of the updated branch and the tip of the mainline, and they should
|
|
|
|
merge without the past faulty merge and its revert getting in the way.
|
|
|
|
|
|
|
|
To recap, these are two very different scenarios, and they want two very
|
|
|
|
different resolution strategies:
|
|
|
|
|
|
|
|
- If the faulty side branch was fixed by adding corrections on top, then
|
|
|
|
doing a revert of the previous revert would be the right thing to do.
|
|
|
|
|
|
|
|
- If the faulty side branch whose effects were discarded by an earlier
|
|
|
|
revert of a merge was rebuilt from scratch (i.e. rebasing and fixing,
|
|
|
|
as you seem to have interpreted), then re-merging the result without
|
|
|
|
doing anything else fancy would be the right thing to do.
|
|
|
|
(See the ADDENDUM below for how to rebuild a branch from scratch
|
|
|
|
without changing its original branching-off point.)
|
|
|
|
|
|
|
|
However, there are things to keep in mind when reverting a merge (and
|
|
|
|
reverting such a revert).
|
|
|
|
|
|
|
|
For example, think about what reverting a merge (and then reverting the
|
|
|
|
revert) does to bisectability. Ignore the fact that the revert of a revert
|
|
|
|
is undoing it - just think of it as a "single commit that does a lot".
|
|
|
|
Because that is what it does.
|
|
|
|
|
|
|
|
When you have a problem you are chasing down, and you hit a "revert this
|
|
|
|
merge", what you're hitting is essentially a single commit that contains
|
|
|
|
all the changes (but obviously in reverse) of all the commits that got
|
|
|
|
merged. So it's debugging hell, because now you don't have lots of small
|
|
|
|
changes that you can try to pinpoint which _part_ of it changes.
|
|
|
|
|
|
|
|
But does it all work? Sure it does. You can revert a merge, and from a
|
|
|
|
purely technical angle, git did it very naturally and had no real
|
|
|
|
troubles. It just considered it a change from "state before merge" to
|
|
|
|
"state after merge", and that was it. Nothing complicated, nothing odd,
|
|
|
|
nothing really dangerous. Git will do it without even thinking about it.
|
|
|
|
|
|
|
|
So from a technical angle, there's nothing wrong with reverting a merge,
|
|
|
|
but from a workflow angle it's something that you generally should try to
|
|
|
|
avoid.
|
|
|
|
|
|
|
|
If at all possible, for example, if you find a problem that got merged
|
|
|
|
into the main tree, rather than revert the merge, try _really_ hard to
|
|
|
|
bisect the problem down into the branch you merged, and just fix it, or
|
|
|
|
try to revert the individual commit that caused it.
|
|
|
|
|
|
|
|
Yes, it's more complex, and no, it's not always going to work (sometimes
|
|
|
|
the answer is: "oops, I really shouldn't have merged it, because it wasn't
|
|
|
|
ready yet, and I really need to undo _all_ of the merge"). So then you
|
|
|
|
really should revert the merge, but when you want to re-do the merge, you
|
|
|
|
now need to do it by reverting the revert.
|
|
|
|
|
|
|
|
ADDENDUM
|
|
|
|
|
|
|
|
Sometimes you have to rewrite one of a topic branch's commits *and* you can't
|
|
|
|
change the topic's branching-off point. Consider the following situation:
|
|
|
|
|
|
|
|
P---o---o---M---x---x---W---x
|
|
|
|
\ /
|
|
|
|
A---B---C
|
|
|
|
|
|
|
|
where commit W reverted commit M because it turned out that commit B was wrong
|
|
|
|
and needs to be rewritten, but you need the rewritten topic to still branch
|
|
|
|
from commit P (perhaps P is a branching-off point for yet another branch, and
|
|
|
|
you want be able to merge the topic into both branches).
|
|
|
|
|
|
|
|
The natural thing to do in this case is to checkout the A-B-C branch and use
|
|
|
|
"rebase -i P" to change commit B. However this does not rewrite commit A,
|
|
|
|
because "rebase -i" by default fast-forwards over any initial commits selected
|
|
|
|
with the "pick" command. So you end up with this:
|
|
|
|
|
|
|
|
P---o---o---M---x---x---W---x
|
|
|
|
\ /
|
|
|
|
A---B---C <-- old branch
|
|
|
|
\
|
|
|
|
B'---C' <-- naively rewritten branch
|
|
|
|
|
|
|
|
To merge A-B'-C' into the mainline branch you would still have to first revert
|
|
|
|
commit W in order to pick up the changes in A, but then it's likely that the
|
|
|
|
changes in B' will conflict with the original B changes re-introduced by the
|
|
|
|
reversion of W.
|
|
|
|
|
|
|
|
However, you can avoid these problems if you recreate the entire branch,
|
|
|
|
including commit A:
|
|
|
|
|
|
|
|
A'---B'---C' <-- completely rewritten branch
|
|
|
|
/
|
|
|
|
P---o---o---M---x---x---W---x
|
|
|
|
\ /
|
|
|
|
A---B---C
|
|
|
|
|
|
|
|
You can merge A'-B'-C' into the mainline branch without worrying about first
|
|
|
|
reverting W. Mainline's history would look like this:
|
|
|
|
|
|
|
|
A'---B'---C'------------------
|
|
|
|
/ \
|
|
|
|
P---o---o---M---x---x---W---x---M2
|
|
|
|
\ /
|
|
|
|
A---B---C
|
|
|
|
|
|
|
|
But if you don't actually need to change commit A, then you need some way to
|
|
|
|
recreate it as a new commit with the same changes in it. The rebase commmand's
|
|
|
|
--no-ff option provides a way to do this:
|
|
|
|
|
|
|
|
$ git rebase [-i] --no-ff P
|
|
|
|
|
|
|
|
The --no-ff option creates a new branch A'-B'-C' with all-new commits (all the
|
|
|
|
SHA IDs will be different) even if in the interactive case you only actually
|
|
|
|
modify commit B. You can then merge this new branch directly into the mainline
|
|
|
|
branch and be sure you'll get all of the branch's changes.
|
|
|
|
|
|
|
|
You can also use --no-ff in cases where you just add extra commits to the topic
|
|
|
|
to fix it up. Let's revisit the situation discussed at the start of this howto:
|
|
|
|
|
|
|
|
P---o---o---M---x---x---W---x
|
|
|
|
\ /
|
|
|
|
A---B---C----------------D---E <-- fixed-up topic branch
|
|
|
|
|
|
|
|
At this point, you can use --no-ff to recreate the topic branch:
|
|
|
|
|
|
|
|
$ git checkout E
|
|
|
|
$ git rebase --no-ff P
|
|
|
|
|
|
|
|
yielding
|
|
|
|
|
|
|
|
A'---B'---C'------------D'---E' <-- recreated topic branch
|
|
|
|
/
|
|
|
|
P---o---o---M---x---x---W---x
|
|
|
|
\ /
|
|
|
|
A---B---C----------------D---E
|
|
|
|
|
|
|
|
You can merge the recreated branch into the mainline without reverting commit W,
|
|
|
|
and mainline's history will look like this:
|
|
|
|
|
|
|
|
A'---B'---C'------------D'---E'
|
|
|
|
/ \
|
|
|
|
P---o---o---M---x---x---W---x---M2
|
|
|
|
\ /
|
|
|
|
A---B---C
|