46

Is there a way to test if two diffs or patches are equivalent?

Let's say you have the following git commit history, where features F and G are cleanly rebaseable to E:

     G
    /
A--B--C--D--E
 \
  F

Due to limitations in our current deployment process, we have the following, somewhat related graph (it's not version controlled)

              G'
             /
------------E'
             \
              F'

F' and G' will ultimately be applied to the head E', in some to be determined order, so it would end up like

------------E'--G'--F'

Is there a way to test that the diff from E' to G' is the same as the patch produced by the git commit of G from B?

I fully realize that in an ideal world, revision control would solve this, and we're getting there, but that's not where we are currently.

You could essentially play both patches on separate checkouts and compare the outputs, but that seems kind of clunky. And comparing the diffs themselves, I'm assuming, wouldn't work because line numbers could change. Even if G' and F' were rebased to E', the patch for F' would ultimately be applied to G', making the diff context of the patch different.

Mark Canlas
  • 9,385
  • 5
  • 41
  • 63
  • 3
    Did you try a diff on the diffs? – Jaffa Dec 20 '11 at 01:35
  • Gnu diff has a command line switch for specifying regex for lines to be ignored when generating the diff. – holygeek Dec 20 '11 at 01:37
  • For batches, `git patch-id` and `git cherry` (also see `--cherry*` options for git log) might be used as fast answer, but it is too strict and can consider some minor changes as important. Diff of diffs is the way to go when you need certain answer – max630 Aug 11 '16 at 09:18

4 Answers4

56
diff <(git show COMMIT1SHA) <(git show COMMIT2SHA)
mrbrdo
  • 7,968
  • 4
  • 32
  • 36
  • 3
    Can you add some context as to how this answers the question? – Chrismas007 Jan 23 '15 at 19:48
  • 2
    It compares (diffs) two diffs (in this case the diffs of two different commits). – mrbrdo Feb 20 '15 at 20:21
  • 1
    It makes a diff of the actual changes that the commits made, as well as the metadata of the commits (sha, author, date, commit message etc). I have used this to check if two commits in separate branches introduce the same change, to be certain that I can use either of them for a cherry-pick. – Amedee Van Gasse Jul 03 '15 at 07:15
  • 24
    For anyone wondering what those `<(...)` marks are, the google keyword is _process substitution_. See http://www.tldp.org/LDP/abs/html/process-sub.html – Linus Arver Jun 07 '16 at 21:59
  • If anyone wants to compare two diffs that both span across multiple commits, you can use the following variation: `diff <(git diff -w sha1 sha2) <(git diff -w sha3 sha4)`. It's a bit hard to read if they actually differ -- I mainly use it to make sure they are the same. – Daniel Waltrip Apr 04 '18 at 02:46
  • 3
    I've used this approach in the past and found it pretty weak. However, today I was inspired to add the --ignore-matching-lines option to skip useless hunks where the diff is only showing differences in line numbers (which are an unavoidable distraction when comparing to otherwise very similar commits. So try a variant like this: % diff -U3 --ignore-matching-lines="^@@ " <(git show b7f9f919) <(git show 8ce6b2af) – Ted Apr 23 '18 at 13:14
  • One (small?) downside of this approach is that it depends on process substitution which isn't available in all shells. It works in bash and zsh, but neither in dash nor tcsh. – Uwe Kleine-König Apr 19 '23 at 13:07
46

Since git 2.19 there is an extra tool for that: git range-diff

So you probably want:

git range-diff E..G E'..G'

This should work even for more than one patch in each range.

Uwe Kleine-König
  • 3,426
  • 1
  • 24
  • 20
  • Should this be `git range-diff E..G E'..G'`? – mkrieger1 May 23 '21 at 10:15
  • 2
    @mkrieger1 Indeed, I didn't notice that there is an `E'`. With a recent enough git you can also do: `git range-diff G^! G'^!`, then the actual base doesn't matter. I'll correct my answer. Thanks for the hing – Uwe Kleine-König May 25 '21 at 05:45
  • What does single quotation marks mean here `E'..G'` ? :thinking: – Lajos Sep 01 '21 at 15:07
  • 1
    @Lajos The same as in the original question: `E'` is a variant of `E` (e.g. rebased). So it's just a name for a revision that you have to adapt when working on an actual repository. – Uwe Kleine-König Sep 28 '21 at 10:23
  • There was a suggestion to use `git range-diff B..G E'..G'` instead. Note that both variants are equivalent. While `E` isn't an ancestor of `G`, the syntax is still fine and the range contains just the commit `G`. – Uwe Kleine-König Apr 19 '23 at 13:02
4

For the benefit of the reader, here is an update to the answer of @mrbrdo with a little tweak:

  • Adds git sdiff alias for easy access of this. sdiff stands for show diff.
  • Ignore annotated tag headers using the ^{} suffix.
  • Allow diff options to be used like -u etc.

Run this once in each of your accounts in which you use git:

git config --global alias.sdiff '!'"bash -c 'O=(); A=(); while x=\"\$1\"; shift; do case \$x in -*) O+=(\"\$x\");; *) A+=(\"\$x^{}\");; esac; done; g(){ git show \"\${A[\$1]}\" && return; echo FAIL \${A[\$1]}; git show \"\${A[\$2]}\"; }; diff \"\${O[@]}\" <(g 0 1) <(g 1 0)' --"

Afterwards you can use this:

git sdiff F G

Please note that this needs bash version 3 or above.

Explained:

  • git config --global alias.sdiff adds an alias named git sdiff into the global ~/.gitconfig.

  • ! runs the alias as shell command

  • bash -c we need bash (or ksh), as <(..) does not work in dash (aka. /bin/sh).

  • O=(); A=(); while x="$1"; shift; do case $x in -*) O+=("$x");; *) A+=("$x^{}");; esac; done; separates options (-something) and arguments (everything else). Options are in array O while arguments are in array A. Note that all arguments get ^{} attached, too, which skipps over annotations (such that you can use annotated tags).

  • g(){ git show "${A[$1]}" && return; echo FAIL ${A[$1]}; git show "${A[$2]}"; }; creates a helper function, which does git show for the first argument. If that fails, it outputs "FAIL first-argument" and then outputs the second argument. This is a trick to reduce the overhead in case something fails. (A proper error managment would be too much.)

  • diff "${O[@]}" <(g 0 1) <(g 1 0) runs diff with the given options against the first argument and the second argument (with said FAIL-error fallback to the other argument to reduce the diff).

  • -- allows to pass diff-options (-something) to this alias/script.

Bugs:

  • Number of arguments is not checked. Everything behind the 2nd argument is ignored, and if you give it too few, you just see FAIL or nothing at all.

  • Errors act a bit strange and clutter the output. If you do not like this, run it with 2>/dev/null (or change the script appropriately).

  • It does not return an error if something breaks.

  • You probably want to feed this into a pager by default.

Please note that it is easy to define some more aliases like:

git config --global alias.udiff '!git sdiff -u'
git config --global alias.bdiff '!git sdiff -b'

git config --global alias.pager '!pager() { cd "$GIT_PREFIX" && git -c color.status=always -c color.ui=always "$@" 2>&1 | less -XFR; }; pager'
git config --global alias.ddiff '!git pager udiff'

I hope this does not need to be explained further.

For more aliases like that, perhaps have a look at my GitHub repo

Tino
  • 9,583
  • 5
  • 55
  • 60
0

I was looking for an answer and while diff <(git show XYZ) <(git show ABC) works, it still shows the commit ID in the diff and you need to account for that with the result. With git diff-tree you can do this:

diff -q \
   <(git diff-tree XYZ --no-commit-id --cc) \
   <(git diff-tree ABC --no-commit-id --cc)

This compares only the two diffs and you don't have to account for the commit ID being diffed.

waterkip
  • 1
  • 2