3

I usually check the content of a commit by "git diff commit^!". However, when I apply it to the initial commit, I see a mix of changes from different commits afterwards, while I assume it should be the initial copy of the commit. Could someone help me to understand it semantics-wise?

BTW, I'm aware there is good answers how to show the diff of init commit from stackoverflow question 40883798.

torek
  • 448,244
  • 59
  • 642
  • 775
ywu
  • 131
  • 2
  • 6

2 Answers2

7

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.

torek
  • 448,244
  • 59
  • 642
  • 775
0

When you run git diff and pass a commit, git will display the changes in your current working tree relative to that commit

From the git docs

git diff [] [--] […​] This form is to view the changes you have in your working tree relative to the named . You can use HEAD to compare it with the latest commit, or a branch name to compare with the tip of a different branch.

The Caret (^) is a reference to the first parent of that commit. In a similar fashion you can refer to the second (^2), third (^3) and so on.

By running git diff <firstCommit>^ you are semantically asking for "Give me the changes between my current working directory and the parent first parent of the first commit". The Parent of the first commit does not exists, so it does not semantically make sense.

In git 2.17.1 running running such a command yields an error:

$> git diff 9f3f6c8e4b1dea1de25febbb8248a6c430966236^
$> fatal: ambiguous argument '9f3f6c8e4b1dea1de25febbb8248a6c430966236^': unknown revision or path not in the working tree.
danielorn
  • 5,254
  • 1
  • 18
  • 31