As detailed by Linus ( https://mirrors.edge.kernel.org/pub/software/scm/git/docs/howto/revert-a-faulty-merge.txt):
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.
Okay, so that explains why revert is not an effective strategy, but what can we do? Let's consider the following:
p---Q---r---s---M---t---u--- dev
\ /
A---B---C-------D---E feature
- ABC is feature/release branch work
- M is the bad merge, were the changes from AB was discarded, but C was preserved
- DE is later work on
feature
- the rest are unrelated changes on the mainline branch
dev
So long as M exists on dev, it assumes the history of ABC has been integrated, even though the deltas of AB are missing. To recover them without changing the history of dev, we need to recreate the deltas in an alternate history (ie new commit IDs).
If there are only a few commits, you could individually cherrypick each one onto dev as cherrypicking copies data into a new commit ID. This however doesn't scale well to large or complicated branch histories.
The next option is to use rebase --no-ff to recreate a new feature branch from which the lost changes can be merged.
git checkout E
git rebase --no-ff Q
Which creates the following:
A'--B'--C'-------D'--E' feature-fixed
/ \
p---Q---r---s---M---t---u---M2 dev
\ /
A---B---C--------D---E feature
The orginal merge M likely only became a problem due to merge conflicts. One problem with this approach is that not only do you have to correctly resolve the original conflict in ABC, but now you have a new possible source of conflict in DE and TU to contend with. In an emergency situation this can be hairy to figure out what's going on.
Preferred Solution:
p---Q---r---S-------M---t---u-----M3 dev
\ \ / /
A---B---\---C----D---E / feature
\ \ /
----M2---------- fix
A simpler strategy, using tools you're likely familiar with, is to correctly recreate the merge using a squash commit (M2). This creates a new commit ID (new history), and thus the deltas from AB can be successfully integrated back into the mainline. This method also gates off possible sources of merge conflict, allow you to first correct the error and then deal with upstream changes.
Method:
Branch from dev before the bad merge (M) landed.
git checkout -b fix S
You now have a clean slate from which you can perform a corrected merge. The squash flag will condense these changes into a single commit, but more importantly it will generate a new commit ID
git merge --squash C
Likely at this point you will need to resolve conflicts, however M2 now represents all the data M should have originally contained. You can now merge this to dev as you normally would
git checkout dev
git merge fix
Again, merge conflicts might arise, but after this point (M3) you have recovered your missing data. You are now free to proceed as per normal, eg you're free to merge DE from feature into dev or any of your other usual operations. This also means no other team members need to reset their local dev branch as it will recover when they next perform a pull.