git revert a merge: determine which parent is which (-m 1 vs -m 2)
Asked Answered
P

1

14

I am trying to revert a merge, but I don't know whether to use git revert -m 1 <merge commit's sha> or git revert -m 2 <merge commit's sha>. How do I find out which parent is -m 1 and which parent is -m 2?

Putrid answered 13/4, 2017 at 14:52 Comment(0)
U
28

Well, the super-short answer is that it's always -m 1. :-) But this deserves some explanation:

  • The parents are ordered, and commands like git log and git show show the order:

    commit c13c783c9d3d7d3eff937b7bf3642d2a7fe32644
    Merge: 3f7ebc6ec 39ee4c6c2
    

    so here 3f7ebc6ec is parent #1 and 39ee4c6c2 is parent #2.

  • The suffix ^ operation takes these same values:

    $ git rev-parse c13c783c9d3d7d3eff937b7bf3642d2a7fe32644^1
    3f7ebc6ece46f1c23480d094688b8b5f24eb345c
    

    (and of course ...^2 would be the other one).

  • Programs that draw the graph, including git log --graph, will show you how these connect.

  • But most importantly, the first parent of any merge is the commit that was current when you made the merge.

In particular, this means that if you are on branch main and run git merge sidebranch, the commit you make now (if all goes well) or eventually (if you must resolve the merge by hand) has, as its first parent, the previous tip of the main branch. Its second parent is therefore the tip of sidebranch.

Suppose, then, that we have this to start with:

...--o--*--A--B------C   <-- main
         \
          D--E--F--G--H   <-- sidebranch

when we run git merge. The common base commit is *, and Git makes a new merge commit M by doing, in essence:

  1. git diff * C (what did we change?)
  2. git diff * H (what did they change?)

and then combining these two sets of changes and applying those to *, giving us this final result:

...--o--*--A--B------C--M   <-- main
         \             /
          D--E--F--G--H   <-- sidebranch

Now, if everything changed in A-B-C is completely independent of everything in changed in D-E-F-G-H, it would not matter too much precisely how Git did the revert, as long as it kept the A-B-C changes while ditching the D-E-F-G-H changes.

But what if B is mostly the same as F, i.e., both B and F fix a bug? In that case, we don't want to undo the shared changes from B and F, that Git took one copy of. This is where the -m 1 part comes in.

When git revert goes to undo some changes, it run its own git diff. The git diff it runs compares the commit you want to revert, to its parent. For any ordinary non-merge commit, this is easy: compare B vs A, or E vs D, or whatever, to see what happened, and back it out. With a merge commit, though, it's not obvious which parent to compare to (except that it kind of is :-) ). The first parent here is C, so let's see what we get if we run:

git diff C M

The changes between C and M are those we picked up by adding the changes from D-E-F-G-H to the changes we already had in A-B-C, if we compare M vs *. In other words:

  • If B and F overlap 100%, the changes in C-vs-M are D-E-G-H: everything except the overlap. So we wind up reverting just those.

  • If F has more changes in it than B, the changes in C-vs-M are D-E-(some-of-F)-G-H: We wind up reverting those changes, but not the ones in B.

  • If F has fewer changes in it than B, the changes in C-vs-M are just D-E-G-H again, and we wind up reverting just those.

Since the first parent is C, and we want to back out the D-E-F-G-H changes (excluding any we already had via A-B-C), we want -m 1 in this revert.

Unskillful answered 13/4, 2017 at 19:35 Comment(2)
This is a very well thought out answer. Thank you!Anthraquinone
beautiful answer! how could conflicts arise when reverting, in your examples?Farceur

© 2022 - 2024 — McMap. All rights reserved.