Some of this question is about Git Extensions, the graphical user interface. I have not used that and cannot answer anything about that. The other part of this question, though, is what these diffs mean / show, and that's basic command-line Git.
Remember, every commit in Git holds a full snapshot of all of your files. For regular (non-merge) commits, it's easy enough to see how this works. Every commit has a parent commit, and hence commits are strung together, one after another, in a line that we can draw:
...---F--G--H <-- master
or:
* 9fadedd637 (master) a commit message subject
|
* 3bab5d5625 some other commit message subject
|
* 1c56d6f57a this commit does something or other
|
:
as shown by git log --graph
for instance (the above output is simulated and not quite what --graph
would show, but close enough).
To view a commit, we can either check it out, so that we have all the files in a work-tree where we can see them; or we can ask Git to show the commit by comparing it to its parent. To see commit H
, at the tip of master
, we have Git extract both G
and H
to a temporary area (in memory) and compare them.
Some files in G
match their counterparts in H
. Git says nothing at all about these files. Some files don't match; for those, Git shows us a recipe–a diff listing—by which we could change the file in G
so that it would match the copy in H
.
Merge commits have more than one parent. When drawing them with newer commits towards the right, as I usually do on StackOverflow, we get something like this:
I--J
/ \
...--G--H M--N--...
\ /
K--L
Commit M
is a merge, with parents J
and L
.
A merge is like any other commit: it holds a full snapshot, not some set of changes. But we can view it as changes, by comparing the snapshot in M
to the snapshot in J
:
git diff <hash-of-J> <hash-of-M>
does exactly that, for instance.
We can also view it as changes by comparing L
vs M
:
git diff <hash-of-L> <hash-of-M>
Either diff will show changes. The changes they'll show are different, though.
Since M
is a merge commit, what we'll see, when looking at J
-vs-M
, is the changes we brought in from the work done along the bottom row, in commits K
and L
. If we compare L
-vs-M
, we'll see the changes we brought in from the work done along the top row, in commits I
and J
. Note that it's possible that L
is a cherry-pick of I
, for instance, so that the work is duplicated. In this case, the merge took only one copy of these changes, not two copies, so we won't see that in either of these two diffs.
A combined diff is something else entirely. It is, in my opinion, an attempt to be useful that is not as successful as one might like. What a Git combined diff does is, first, run each of the individual diffs—in this case, J
vs M
and L
vs M
—and then throw out from this diff any file that's not changed at all in the other diff.
That is, suppose that in H
, we had four files, all of which are also in I
, J
, K
, L
, and M
, and these properties hold:
same.txt
is the same in all six commits.
top-only.txt
gets modified in I
and/or J
, but not in K
and/or L
.
bot-only.txt
gets modified in K
and/or L
, but not in I
and/or J
.
both.txt
gets modified in J
and also in L
.
We will also assume that M
is not an evil merge (see Evil merges in git?). That is, H
-vs-M
, if we run that diff, will not show any changes that were not acquired through I
, J
, K
, or L
. Moreover, we'll assume that the changes to both.txt
did not overlap at all, so that the merge combined both changes: no conflicts, no other issues.
Obviously, diffing J
vs M
shows nothing for same.txt
: it was not touched in any of the five commits that come after H
.
Diffing J
vs M
does not show top-only.txt
because the copy in M
matches the copy in J
. Changes to this file happened in I
and/or J
, i.e., comparing H
with J
shows changes, but those changes to the H
version of top-only.txt
, resulting in the J
version of the top-only.txt
, are applied to the H
version to get the M
version.
Diffing L
vs M
does not show bot-only.txt
for the same reason. Again, the M
copy of bot-only.txt
matches the L
copy.
Only for both.txt
are there differences between J
and M
and differences between L
and M
.
If we ask Git for a combined diff view of M
, Git will now:
- diff
J
vs M
: two files are changed: top-only.txt
and both.txt
; and
- diff
L
vs M
: two files are changed: bot-only.txt
and both.txt
.
So the list has three files changed, one in both diffs and one only in one each. The combined diff now throws out the only-in-one-diff files, leaving only the one file both.txt
.
At this point, you have an option of two different kinds of combined diff:
git diff --cc
(the default: note two dashes and two c
s): this prints nothing.
git diff -c
(note one dash and one c
): this shows a diff for both.txt
.
The --cc
one actually makes sense. The idea here is to show you where there was a merge conflict. Since there wasn't a merge conflict, it does not show anything. (For a case where it does show something, see the combined diff format section of the git diff
documentation.)
The -c
one makes sense too, except for one thing: it doesn't show you top-only.txt
, nor bot-only.txt
. What we really need here, but Git does not have, is a way to compare the merge result snapshot with the merge base snapshot. That is, we might like to view H
vs M
. There is no built in operation to do this, despite the fact that I think there probably should be.
The output from git show -c
, which runs this sort of combined diff, on a test repository I made here, is:
commit 54627dbf51ab9b27c8e5486e4755651688dd5d21 (HEAD -> merge)
Merge: da92a96 e77c224
[snip]
diff --combined both.txt
index 5d51eed,506867f..1f3f391
--- a/both.txt
+++ b/both.txt
@@@ -2,10 -2,10 +2,11 @@@ this fil
will be
modified in
both branches.
+here is the top-branch change
The changes
will be
combined into
one file
in the merge result.
+ here is the bottom-branch change
We can do it manually. See the script below (named git-show-merge
so I can run it as git show-merge
). This basically finds the merge base from the parents of the specified merge (or HEAD
as the default), then runs the desired diff.
Git also has the -m
option, available for git show
and git log
both. This flag effectively "splits" the merge into separate "virtual commits". Each one is a single-parent commit, whose single parent is one of the parents from the actual merge. Git's internal difference engine is then good with diff-ing that commit as an ordinary diff, to show you which files it changed.
In this case, then, git show -m <hash-of-M>
will diff J
-vs-M
first, then L
-vs-M
second, and show you both diffs.
It looks like Git Extensions does not have the -m
option but does have the option to show either the J
-vs-M
diff, or the L
-vs-M
diff, or—if you select it—one of the combined-diff formats, probably the --cc
variant.
Note that git log -p
defaults to not even attempting to show a merge. Use an explicit -c
or --cc
option to force git log
to show a combined diff.
#! /bin/sh
USAGE="[commit] the merge commit to show - default is HEAD"
. git-sh-setup
case $# in
0) set HEAD;;
1) ;;
*) usage;;
esac
commit=$1
hash=$(git rev-parse ${commit}^{commit}) || exit
set -- $(git rev-parse $hash^@)
[ $# -ge 2 ] || die "$commit ($hash) is not a merge"
set -- $(git merge-base --all $@)
case $# in
0) die "parents of ${commit} are unrelated";;
1) ;;
*) echo "warning: more than one merge base, diffing against $1";;
esac
git diff $1 $hash