I've created a simple git repo to illustrate my question, available on GitHub here: https://github.com/smileyborg/EvilMerge
Here's an illustration of the repo history:
master A---B---D---E-----G-----I \ / \ / another_branch ----C \ / \ / another_branch2 F---H
(In the actual repo on GitHub, D
is 4a48c9
, and I
is 48349d
.)
D
is a "simple" evil merge, where the merge commit "correctly" resolves a merge conflict, but also makes an unrelated "evil" change that did not exist in either parent. It is possible to discover the "evil" part of this merge by using git show -c
on this commit, as the output includes ++
and --
(as opposed to single +
and -
) to indicate the changes that did not exist in either parent (see this answer for context).
I
is a different kind of evil merge, where the merge commit "correctly" resolves a merge conflict (caused by changes from F
to file.txt
that conflict with changes from G
), but also "evilly" discards the changes made to a completely different file file2.txt
(effectively undoing the changes from H
).
How can you know that I
is an evil merge? In other words, what command(s) can you use to discover that I
not only manually resolves a conflict, but also fails to merge changes that it should have?
As pointed out by René Link below, it is hard (perhaps impossible) to define a generic set of criteria to identify an "evil merge". However, much like Supreme Court Justice Stewart said about pornography, evil merges are something you know when you see.
So perhaps a better question to ask is this: what git command(s) can you use on a merge commit to get a diff output of all novel changes introduced solely in the merge commit itself. This diff should include:
D
)I
)The goal here is to be able to have a human look at this output and know whether the merge was successful or (accidentally or maliciously) "evil" without having to re-review all the previously-reviewed changes (e.g. F
and H
) that are being integrated in the merge.
For simple text files, Git uses an approach known as the longest common subsequence algorithm to perform merges and to detect merge conflicts. In its simplest form, Git find the longest set of lines in common between your changed file and the common ancestor.
An evil merge is a merge that introduces changes that do not appear in any parent.
To see the beginning of the merge conflict in your file, search the file for the conflict marker <<<<<<< . When you open the file in your text editor, you'll see the changes from the HEAD or base branch after the line <<<<<<< HEAD .
The easiest thing to do would be to diff the results of your conflict resolution with a merge that auto-resolves conflicts without human intervention. Any automatic resolutions will be ignored, since they will be resolved in exactly the same way.
I see two ways of visualizing the possible "evil" resolutions. If you are making this into a script add &> /dev/null
to the end of all lines that you do not care to see output.
1) Use two separate diffs, one that favors the first parent, and a second that favors the second parent.
MERGE_COMMIT=<Merge Commit> git checkout $MERGE_COMMIT~ git merge --no-ff --no-edit -s recursive -Xours $MERGE_COMMIT^2 echo "Favor ours" git diff HEAD..$MERGE_COMMIT git checkout $MERGE_COMMIT~ git merge --no-ff --no-edit -s recursive -Xtheirs $MERGE_COMMIT^2 echo "Favor theirs" git diff HEAD..$MERGE_COMMIT
2) Diff against the results of the conflicted merge with the conflicts still in.
MERGE_COMMIT=<Merge Commit> git checkout $MERGE_COMMIT~ git -c merge.conflictstyle=diff3 merge --no-ff $MERGE_COMMIT^2 --no-commit git add $(git status -s | cut -c 3-) git commit --no-edit git diff HEAD..$MERGE_COMMIT
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With