I usually check the content of a commit by "git diff commit^!".
(Why? The usual command is git show <commit>
. Note that git diff commit^!
is not officially supported, and for merge commits, its accidental behavior changed in Git 2.28.)
However, when I apply it to the initial commit, I see [something less useful]
The place to find the (short) explanation of any revision syntax is the gitrevisons documentation, which says this about the ^!
suffix:
The r1^! notation includes commit r1 but excludes all of its parents.
By itself, this notation denotes the single commit r1.
Unfortunately, this explanation is too short. Feeding a revision to git rev-parse
is more helpful in this case:
$ git rev-parse HEAD
b5101f929789889c2e536d915698f58d5c5c6b7a
$ git rev-parse HEAD^1
a562a119833b7202d5c9b9069d1abb40c1f9b59a
From these two outputs, we can see that HEAD
represents hash ID b5101f929789889c2e536d915698f58d5c5c6b7a
, and its first parent is a562a119833b7202d5c9b9069d1abb40c1f9b59a
. This is what we need to make sense of:
$ git rev-parse HEAD^!
b5101f929789889c2e536d915698f58d5c5c6b7a
^a562a119833b7202d5c9b9069d1abb40c1f9b59a
The output here means, in effect:
- start at
b5101f929789889c2e536d915698f58d5c5c6b7a
...
- but stop at
a562a119833b7202d5c9b9069d1abb40c1f9b59a
.
(The ^
in front of the hash ID means "not": commit B, but not commit A.)
Compare this to:
$ git rev-parse HEAD^..HEAD
b5101f929789889c2e536d915698f58d5c5c6b7a
^a562a119833b7202d5c9b9069d1abb40c1f9b59a
It's the same output!
What this means for git diff
is a little tricky and subtle, because we're talking about git rev-parse
here and not git diff
. But in fact, when you run:
git diff <something>
Git internally hands the <something>
to git rev-parse
. So if you type in:
git diff HEAD^..HEAD
git diff
internally hands the whole string HEAD^..HEAD
to the internal version of git rev-parse
and gets the "stop at" and "start at" IDs. If you type in:
git diff HEAD^!
git diff
internally hands the whole string HEAD^!
to the internal version of git rev-parse
and gets the same "stop at" and "start at" IDs.
The same holds when you use a commit hash for any commit that has one parent: the ^!
suffix generates a "not" for the parent hash, and a "use" for the hash.
But, when you find a root commit—in my Git repository for Git there are a bunch, so I will just take the first one:
$ git rev-list --max-parents=0 HEAD | head -1
0ca71b3737cbb26fbf037aa15b3f58735785e6e3
—when we give this hash ID with a hat-bang suffix to git rev-parse
, we get:
$ git rev-parse 0ca71b3737cbb26fbf037aa15b3f58735785e6e3^!
0ca71b3737cbb26fbf037aa15b3f58735785e6e3
That is, git rev-parse
said yes to the commit, and no to all of its parents, but there aren't any parents, so it said no to nothing at all!
If you feed this to git diff
—just one commit hash—the git diff
command thinks: ah, you want to compare the given commit to whatever is in your work-tree right now. So that's the diff you get.
Merge commits
Note that the ^!
suffix produces "not"s for all the parents of a commit. For a merge commit, there are at least two (and usually exactly two) parents:
$ git show -s a562a11983
commit a562a119833b7202d5c9b9069d1abb40c1f9b59a
Merge: 7fa92ba40a ad6f028f06
Author: Junio C Hamano ...
So:
$ git rev-parse a562a11983^!
a562a119833b7202d5c9b9069d1abb40c1f9b59a
^7fa92ba40abbe4236226e7d91e664bbeab8c43f2
^ad6f028f067673cadadbc2219fcb0bb864300a6c
When you give git diff
a revision specifier that expands to three or more commits, it sometimes does something different.
In Git versions predating 2.28, git diff <rev>^!
sort of accidentally compares the commit itself to the first parent.1 The git show
command will instead produce a combined diff by default, which in this case shows nothing at all because combined diffs are designed to show you where merges might have conflicted.
It's possible that the rev^!
syntax to git diff
behavior will change again in a future Git release (2.39 or later), either to restore the old behavior or just to reject this entirely if there are multiple parents.
1The reason for this is that, while git rev-parse
shows the positive ref first, then all the negative refs—i.e.,
a562a119833b7202d5c9b9069d1abb40c1f9b59a
^7fa92ba40abbe4236226e7d91e664bbeab8c43f2
^ad6f028f067673cadadbc2219fcb0bb864300a6c
as shown above—the internal rev parser that git diff
calls winds up storing them into an array in this order:
^7fa92ba40abbe4236226e7d91e664bbeab8c43f2
^ad6f028f067673cadadbc2219fcb0bb864300a6c
a562a119833b7202d5c9b9069d1abb40c1f9b59a
Pre-Git-2.28, this selected the first entry as the second commit and the last entry as the first commit. In Git 2.28 and later, this winds up running a combined-diff with the revisions in the listed order, which isn't very useful. A combined diff needs the first revision to be a positive reference. The git show
command handles this properly when you pass it just the single commit.