Search code examples
gitgit-mergegit-merge-conflict

Revert merge commit resolved with 'ours' strategy


A colleague of mine merge our develop branch back into the feature branch with '--strategy=ours', or with a series of operation that effectively resulted in discarding develop changes.

I was trying to find a way to revert the merge commit to bring the changes back but I was not able to find a simple recipe for it:

  • git revert -m 1 merge-sha1 doesn't works since there is no change against the first parent
  • git revert -m 2 merge-sha1 does bring develop changes into our branch but it seems to also get rid of a lot of changes which were not in develop that we wanted to keep.

I searched online and found some articles about situation like these or similar but couldn't figure out the right way to cleanly resolve this mess. In the end we made a new branch from right before the faulty merge and started from scratch. How could this situation be resolved in a clean way?

EDIT:

To better explain:

--A--B--C--D--E--F--G--H--I
         \           \
          D'--E'--F'--M--G'

M is the faulty merge, that discarded all changes from D to G. With the first option (-m 1), nothing happens. With the second options (-m 2), the revert seems to remove changes happened in D'-F' together with adding the removed changes.

Sorry, forgot to add that git reset is not an option since the merge has already been pushed to the remote repo and git push --force is not allowed.


Solution

  • ... I searched online and found some articles about situation like these or similar but couldn't figure out the right way to cleanly resolve this mess. In the end we made a new branch from right before the faulty merge and started from scratch. How could this situation be resolved in a clean way?

    That actually is the right / clean way—though you don't need a new branch per se, as branch names are not all that important to Git.

    [given this graph]

    --A--B--C--D--E--F--G--H--I   <-- br1
             \           \
              D'--E'--F'--M--G'   <-- br2
    

    [where] M is the faulty merge, that discarded all changes from D to G ...

    All revert options work by comparing M to one of its two parents. Since M was run with --ours, its tree matches either F or F' (whichever of these is the first parent):

    I'm not quite sure why you labeled some commits with prime-marks (D'-E'-F' etc) but if M should have been a real merge, rather than --ours, you need to re-run the merge. You can do this any way you like but the easiest is to check out either of its parents, here G and F' respectively, and then run git merge with the hash ID of its other parent. In general I'd recommend keeping the first- and second-parent-ness properties of the two, so I would do:

    git checkout <hash-of-G>
    git merge <hash-of-F'>
    

    Finish the merge as usual and, if Git itself doesn't commit it, commit it:

                          _________
                         /         \
    --A--B--C--D--E--F--G--H--I     \   <-- br1
             \           \          M2   <-- HEAD
              D'--E'--F'--M--G'     /   <-- br2
                       \___________/
    

    Then, depending on what you want to see, either cherry-pick G' and forcibly reset br2 to point to the result:

                          _________
                         /         \
    --A--B--C--D--E--F--G--H--I     \   <-- br1
             \           \          M2--G"   <-- br2
              D'--E'--F'--M--G'     /
                       \___________/
    

    (but this is a non-fast-forward case that requires all other users of br2 to adjust), or you can just drop a temporary name (tag or branch) here, git checkout br2, git merge the temporary name, and then delete the temporary name:

                          _________
                         /         \
    --A--B--C--D--E--F--G--H--I     \   <-- br1
             \           \          M2--M3   <-- br2
              D'--E'--F'--M--G'-----/---/
                       \___________/
    

    Note that the merge base when producing merge M3 is both F' and G so git merge will, by default, do a recursive merge of those two to use as a merge base; it will then merge both M2 and G' against that virtual merge base to produce M3. You can use git merge -s resolve to pick one of F' or G at random as the merge base, which may be less messy if there are merge conflicts in making the virtual merge base. If you made M2 correctly —for some definition of "correctly" anyway—the result of -s resolve will be the same.

    In the end, Git doesn't care how you get to your final commit graph and the snapshots that are in those commits. All Git really cares about is preserving the commits (which preserves the graph). Branch names just serve to find the tip commits: the entry points into the graph. Various individual commands, such as git diff and git cherry-pick, compare specific commits, perhaps finding them from the graph, and will do interesting things from there, so you might want specific commits to have specific contents so make these as interesting as possible.

    Meanwhile, git merge itself just looks at the graph to find the merge base, then does two diffs (from merge base to each tip commit) to find changes, then combine the changes. There are some exceptions of course: -s ours makes the new mergecommit with the same graph links as a true merge, but completely ignores one of the two branch tips and therefore doesn't have to bother finding a merge base, it just re-uses the tree from the then-current commit. And, as noted in passing above, -s recursive finds all the merge bases. Usually there's only one, so there's nothing special about this, but there are ways to get more than one merge base, for which -s recursive just merges the merge bases to produce a new (temporary) commit to use as the merge base. (After finishing the merge-as-a-verb process, it releases the temporary commit to be reclaimed by Git's usual garbage collection process.)