Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Git: Revert a merge with accidentally discarded files, after it's been pushed

An inexperienced user makes changes release and then merges to dev. Oh no, a merge conflict! The merge status shows their own conflicted files, as well as all other fixes from other team members. Being a paranoid and cautious user, they say:

Those aren't my files, did I accidentally alter them? Aha, I should discard them!

The merge commit now contains only their changes, discarding all other changes in the branch (possibly a large amount of work). They push dev upstream, and the error escapes detection until another team member notices something is amiss.

Most guidance suggests:

  1. If the branch hasn't been pushed yet: git reset
  2. Revert the merge: git revert -m 1 <commitId>. However this only reverts data (aka only the hapless user's changes), it doesn't undo the history. Any future attempts to merge will ignore the lost changes because history implies they have already been integrated.
  3. Rewrite history, rebase or reset followed by git push origin -f. This means the rest of the team has to synchronise to the forced dev branch. If the team is large, or the error wasn't caught quickly, or elaborate CI tooling is present - this becomes a very painful exercise.

Imho this strikes me a critical oversight in the design of git. There is very little tooling to identify or recover from this situation. git diff doesn't show discarded changes, and a git revert doesn't undo those discarded changes. Is there a better way to prevent and fix this problem?

like image 276
Peter Cardwell-Gardner Avatar asked Sep 02 '25 15:09

Peter Cardwell-Gardner


1 Answers

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.

like image 169
Peter Cardwell-Gardner Avatar answered Sep 05 '25 06:09

Peter Cardwell-Gardner



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!