Skip to content

Instantly share code, notes, and snippets.

@smileyborg
Last active August 16, 2023 04:00
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save smileyborg/913fe3221edfad996f06 to your computer and use it in GitHub Desktop.
Save smileyborg/913fe3221edfad996f06 to your computer and use it in GitHub Desktop.
Two scripts that can be used to detect evil merges in Git. See http://stackoverflow.com/questions/27683077
#!/bin/bash
# A shell script to provide a meaningful diff output for a merge commit that can be used to determine whether the merge was evil.
# The script should be run from outside the git repository, with two arguments:
# 1 - the directory of the git repository
# 2 - the SHA for the merge commit to inspect
# The script will output one file:
# - the merge redone fresh without any conflicts resolved, diff'ed to the actual merge
output_file="diff.txt"
if [ "$#" -ne 2 ]
then
echo "ERROR: This script must be run with exactly two arguments: the directory for the Git repo, and the SHA for the merge commit to inspect." >&2
echo "Usage: $0 GIT_REPO_DIR MERGE_COMMIT_SHA" >&2
exit 1
fi
# Store the current HEAD so we can put the repository back into the original state when finished
if ! original_head=$(git -C $1 symbolic-ref --short -q HEAD) # get the branch name, if possible
then
original_head=$(git -C $1 rev-parse HEAD) # detached HEAD, get the commit SHA
fi
# Perform the merge again, without resolving conflicts. Then diff the result with the actual merge commit we're inspecting.
git -C $1 checkout $2~ &>/dev/null
git -C $1 -c merge.conflictstyle=diff3 merge --no-ff $2^2 --no-commit &>/dev/null
git -C $1 add $(git -C $1 status -s | cut -c 3-) &>/dev/null
git -C $1 commit --no-edit &>/dev/null
git -C $1 diff HEAD..$2 > $output_file
# Put the repository back in the original state
git -C $1 checkout $original_head &>/dev/null
#!/bin/bash
# A shell script to provide a meaningful diff output for a merge commit that can be used to determine whether the merge was evil.
# The script should be run from outside the git repository, with two arguments:
# 1 - the directory of the git repository
# 2 - the SHA for the merge commit to inspect
# The script will output two files:
# - the merge redone fresh with conflicts resolved using the first parent, diff'ed to the actual merge
# - the merge redone fresh with conflicts resolved using the second parent, diff'ed to the actual merge
output_fileA="diffA.txt"
output_fileB="diffB.txt"
if [ "$#" -ne 2 ]
then
echo "ERROR: This script must be run with exactly two arguments: the directory for the Git repo, and the SHA for the merge commit to inspect." >&2
echo "Usage: $0 GIT_REPO_DIR MERGE_COMMIT_SHA" >&2
exit 1
fi
# Store the current HEAD so we can put the repository back into the original state when finished
if ! original_head=$(git -C $1 symbolic-ref --short -q HEAD) # get the branch name, if possible
then
original_head=$(git -C $1 rev-parse HEAD) # detached HEAD, get the commit SHA
fi
# Perform the merge again, resolving conflicts using the version from the first parent.
# Then diff the result with the actual merge commit we're inspecting.
git -C $1 checkout $2~ &>/dev/null
git -C $1 merge --no-ff --no-edit -s recursive -Xours $2^2 &>/dev/null
git -C $1 diff HEAD..$2 > $output_fileA
# Perform the merge again, resolving conflicts using the version from the second parent.
# Then diff the result with the actual merge commit we're inspecting.
git -C $1 checkout $2~ &>/dev/null
git -C $1 merge --no-ff --no-edit -s recursive -Xtheirs $2^2 &>/dev/null
git -C $1 diff HEAD..$2 > $output_fileB
# Put the repository back in the original state
git -C $1 checkout $original_head &>/dev/null
@smileyborg
Copy link
Author

Difference between the two scripts:

  • detect_evil_merge.sh outputs a single diff, the merge redone fresh without any conflicts resolved, diff'ed to the actual merge
  • detect_evil_merge2.sh outputs two diffs, the merge redone fresh twice, once resolving conflicts using each parent, diff'ed to the actual merge

Interpreting the output:

  • Additions + in the diff are lines that the actual merge commit added, as compared to the fresh (automatic) merge(s)
  • Deletions - in the diff are lines that the actual merge commit removed, as compared to the fresh (automatic) merge(s)
  • Suspicious (potentially evil) changes that happened in the actual merge will appear as additions or deletions that are unrelated to the changes required to resolve merge conflicts.

Some tips:

  • Either script will do the job, it's just personal preference on which way you find it easier to understand how the conflicts were resolved. You don't need to run both scripts to detect an evil merge.
  • Don't place the scripts inside of your git repo directory, since the scripts run git commands that will be affecting the repo state. That's why the git repo directory is taken as the first argument. Similarly, you should run these scripts with a clean working directory in your git repo!
  • You can modify the output file paths/names in each script if you like.

@dougpagani
Copy link

What do you mean by "evil" here? Possibly what I call "dirty merges"? That is to say, merges where changesets are snuck-in amidst the parents' changesets?

@maxim-belkin
Copy link

What do you mean by "evil" here? Possibly what I call "dirty merges"? That is to say, merges where changesets are snuck-in amidst the parents' changesets?

$ man gitglossary | grep "evil merge"
       evil merge
           An evil merge is a merge that introduces changes that do not appear in any parent.

@maxim-belkin
Copy link

@smileyborg, I'd like to suggest a few changes:

  1. combine the two scripts into one, so that functionality of the second script is engaged by passing an argument
  2. Rename this new script to something like git-is-evil-merge, so that it can be engaged as git is-evil-merge when placed into a dir in PATH
  3. Add a new git-detect-evil-merges script that would iterate over commits in a branch or a range of commits (commitA..commitB) detecting evil merge commits. I think a lot of folks would benefit from such a repo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment