42

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,207
  • 5
  • 40
  • 62
  • 3
    Did you try a diff on the diffs? – Geoffroy 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

3 Answers3

53
diff <(git show COMMIT1SHA) <(git show COMMIT2SHA)
mrbrdo
  • 7,638
  • 4
  • 30
  • 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
  • 18
    For anyone wondering what those ` – 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 – 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="^@@ " – Ted Apr 23 '18 at 13:14
44

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,304
  • 1
  • 22
  • 19
  • 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
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
  • 8,551
  • 3
  • 52
  • 57