revert-a-faulty-merge.txt 10.6 KB
Newer Older
1 2 3 4 5 6 7 8 9
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>
10 11 12 13
Content-type: text/asciidoc

How to revert a faulty merge
============================
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

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
33
               /
34 35 36 37 38 39
       ---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?).
40
IOW, `"diff W^..W"` is similar to `"diff -R M^..M"`.
41 42 43 44 45

Such a "revert" of a merge can be made with:

    $ git revert -m 1 M

46
After the developers of the side branch fix their mistakes, the history
47 48 49
may look like this:

 ---o---o---o---M---x---x---W---x
50
               /
51 52 53 54 55 56
       ---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
57
changes made in A or B will be in the result, because they were reverted
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
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
84
               /
85 86 87 88 89 90 91 92
       ---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
93
changed) be equivalent to not having W or Y at all in the history:
94 95

 ---o---o---o---M---x---x-------x----
96
               /
97 98 99 100 101 102
       ---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-------*
103
               /                       /
104 105 106 107 108 109 110 111 112 113
       ---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
114
               /                 \
115 116 117 118 119
       ---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---*
120
               /                 \         /
121 122
       ---A---B                   A'--B'--C'

123
where Y is the revert of W, A' and B' are rerolled A and B, and there may
124 125 126
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,
127 128 129 130 131
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
132
               /                 \
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
       ---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.
149 150
   (See the ADDENDUM below for how to rebuild a branch from scratch
   without changing its original branching-off point.)
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166

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
167
purely technical angle, Git did it very naturally and had no real
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
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.
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235

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
236
recreate it as a new commit with the same changes in it.  The rebase command's
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
--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