Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Git silently reverted commit without explanation

Tags:

git

A developer asked where his fix to a TR had gone. After examining the branch, it was indeed not there. I assumed he had just forgotten to forward merge the fix, as is fairly common. After looking, the fix had indeed been committed with commit 'A', committed at a point 2 in the timeline.

In parallel, another branch was working on another TR. This branch was branched before 'A' was committed, and thus it was not included.

After a while, this parallel branch was merged into the main branch with commit 'B' (merge commit). Similar to:

O
| \
A  |
| /
B

Despite not having touched the same files as 'A', the merge silently reverted the changes in 'A' without conflict, in fact, running

git blame

after the merge shows 'A' never happened.

However, running

git --contains

shows us that 'A' definitely happened. So the changes were all silently reverted, without a commit.

After attempting the merge between the same two parents, all changes in 'A' are indeed overridden. Furthermore, the original merge was done via a PR on ADS, so there is no chance of any crazy flags being used in the final merge. The PR shows indeed that the files will be changed, which was overlooked on the developers' part.

What could have caused Git to get the merge so wrong? Something earlier in the history? (Remember, the files in 'A' had not been touched by any commit that made up the second parent in 'B').

like image 776
gwow12345 Avatar asked May 16 '26 15:05

gwow12345


1 Answers

(Note: this is not a complete answer, and as such, might deserve to be a comment—but I need formatting and lots of space...)

Despite not having touched the same files as 'A', the merge silently reverted the changes in 'A' without conflict ...

This is ... hard to believe—this sort of thing is usually user-error—but:

After attempting the merge between the same two parents, all changes in 'A' are indeed overridden

this suggests that it indeed is happening.

The most obvious candidate that would allow for this is that there are differences in both A and some other commit after O but before B, and there is a .gitattributes that defines a merge driver for these files. This merge driver is in charge of doing actual merge work, and if it does that work by ignoring the copy of the file in A in favor of the copy in the other commit.

As max630 suggests in a comment, it may also be a good idea to check whether O is a simple, single merge base, or whether Git is computing a synthetic commit to use as a merge base via the recursive strategy.

That is, given:

O---A---M
 \     /
  X---Y

then git merge-base --all A Y will produce the hash ID of commit O, but in more complex cases, there might be multiple merge bases. The -s recursive (default) merge strategy will first merge the merge bases, then use the resulting temporary commit1 as the merge base for A and Y. If you find that there are two or more merge base commits, you can merge them yourself to see what the resulting temporary commit was (see also footnote 1) and use that to figure out what happened.

If there is only a single merge base, then a merge driver that used the files from Y, for some reason, would explain the result in commit M. But the merge driver defined in .gitattributes is run only if all three copies of the files (in O, A, and Y) differ, so if the copies on O and Y match, Git should have taken the copies from A.

Other than that, if some other merge strategy were used, that could explain the result, but when you re-do the merge, you supply a new merge strategy every time.2 So I think that rules out this possibility.


1The recursive strategy really does make temporary commits for each of these intermediate merge bases, but it does not leave you any good way to find the hash IDs of these temporary commits. They're not in any ref name or any reflog. The git gc code will delete them once they're past loose-object-expiry time.

2If you don't supply -s strategy, Git picks one: recursive by default for a two-commit merge, octopus by default for a 3-or-more-commit merge.

like image 127
torek Avatar answered May 19 '26 04:05

torek