Date: Fri, 19 Dec 2008 00:45:19 -0800 From: Linus Torvalds , Junio C Hamano 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: Alan 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. 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.