git diff <first_commit>^<exclamation-point> 是什么意思?

3

我通常通过 "git diff commit^!" 命令来检查提交内容。然而,当我对初始提交应用此命令时,我看到了来自不同提交的混合更改,而我本以为它应该是提交的最初拷贝。能否有人帮助我从语义上理解这个问题?

顺便说一句,我知道有很好的答案可以显示如何从stackoverflow问题40883798显示初始化提交的差异。


如果您想查看提交的特定内容,可以使用 git show <commit> 命令。它只会显示该提交中所做的更改。 - Rmahajan
插入符号 (^) 表示“父级”。初始提交没有父级。 - evolutionxbox
我发现 StackOverflow 在标题中吃掉了 "!"。我放了其他东西来突出它,希望这样可以。 - torek
2个回答

7
我通常通过“git diff commit^!”来检查提交内容。
(为什么?通常的命令是git show <commit>。请注意,git diff commit^!不受官方支持,并且对于合并提交,其意外行为在Git 2.28中发生了变化。)
但是,当我将其应用于初始提交时,我看到的是[一些不太有用的东西]。
任何修订语法(简短)说明的地方都可以在gitrevisons文档中找到,其中关于^!后缀的内容如下:
“r1^!”表示包括提交“r1”,但不包括其所有父提交。单独使用这个符号表示单个提交“r1”。
不幸的是,这个解释过于简短。在这种情况下,将修订提交给git rev-parse更有帮助。
$ git rev-parse HEAD
b5101f929789889c2e536d915698f58d5c5c6b7a
$ git rev-parse HEAD^1
a562a119833b7202d5c9b9069d1abb40c1f9b59a

从这两个输出中,我们可以看到 HEAD 代表哈希 ID b5101f929789889c2e536d915698f58d5c5c6b7a,它的第一个父提交是 a562a119833b7202d5c9b9069d1abb40c1f9b59a。这就是我们需要理解的内容。
$ git rev-parse HEAD^!
b5101f929789889c2e536d915698f58d5c5c6b7a
^a562a119833b7202d5c9b9069d1abb40c1f9b59a

这里的输出实际上意味着:
  • b5101f929789889c2e536d915698f58d5c5c6b7a开始...
  • 但停止于a562a119833b7202d5c9b9069d1abb40c1f9b59a

(散列ID前面的^表示“不包括”:提交B,但不包括提交A。)

与此相比:

$ git rev-parse HEAD^..HEAD
b5101f929789889c2e536d915698f58d5c5c6b7a
^a562a119833b7202d5c9b9069d1abb40c1f9b59a

输出结果相同!

对于git diff,这意味着有点棘手和微妙,因为我们在这里讨论的是git rev-parse而不是git diff。但实际上,当你运行以下命令时:

git diff <something>

Git内部将<something>交给git rev-parse。因此,如果您输入:

git diff HEAD^..HEAD

git diff 命令会将整个字符串 HEAD^..HEAD 传递给内部版本的 git rev-parse,并获取“停止于”和“开始于”的 ID。如果您键入:

git diff HEAD^!

git diff会将整个字符串HEAD^!传递给内部版本的git rev-parse,并获得相同的“停止于”和“开始于”ID。

当您使用具有一个父级的任何提交哈希时,情况也是如此:^!后缀为父哈希生成“非”,为哈希生成“使用”。

但是,当您找到一个根提交时,在我的Git存储库中有很多个,因此我将只取第一个:

$ git rev-list --max-parents=0 HEAD | head -1
0ca71b3737cbb26fbf037aa15b3f58735785e6e3

当我们将带有感叹号后缀的哈希ID this 提供给git rev-parse时,会得到:

$ git rev-parse 0ca71b3737cbb26fbf037aa15b3f58735785e6e3^!
0ca71b3737cbb26fbf037aa15b3f58735785e6e3

也就是说,git rev-parse 对该提交说“是”,对其所有父提交说“否”,但是没有任何父提交,因此它什么也没有拒绝!

如果你将这个提交哈希值输入git diffgit diff 命令会认为:啊,你想比较给定的提交和当前工作树中的内容。所以你得到的就是这个差异。

合并提交

请注意,^! 后缀为提交的所有父提交生成“不”操作。对于合并提交,至少有两个(通常恰好两个)父提交:

$ git show -s a562a11983
commit a562a119833b7202d5c9b9069d1abb40c1f9b59a
Merge: 7fa92ba40a ad6f028f06
Author: Junio C Hamano ...

所以:
$ git rev-parse a562a11983^!
a562a119833b7202d5c9b9069d1abb40c1f9b59a
^7fa92ba40abbe4236226e7d91e664bbeab8c43f2
^ad6f028f067673cadadbc2219fcb0bb864300a6c

当你给git diff一个扩展到三个或更多提交的修订说明符时,它有时会做一些不同的事情。
在Git 2.28之前的版本中,git diff <rev>^!意外地将提交本身与第一个父提交进行比较。1相反,git show命令默认会生成一个合并差异,在这种情况下,合并差异不显示任何内容,因为合并差异旨在显示可能存在冲突的合并位置。
在将来的Git发布(2.39或更高版本)中,rev^!语法对git diff行为可能会再次更改,无论是恢复旧行为还是拒绝多个父提交。
1原因是,虽然git rev-parse首先显示正引用,然后是所有负引用。
a562a119833b7202d5c9b9069d1abb40c1f9b59a
^7fa92ba40abbe4236226e7d91e664bbeab8c43f2
^ad6f028f067673cadadbc2219fcb0bb864300a6c

如上所示——git diff调用的内部rev解析器会按照以下顺序将它们存储到数组中:

^7fa92ba40abbe4236226e7d91e664bbeab8c43f2
^ad6f028f067673cadadbc2219fcb0bb864300a6c
a562a119833b7202d5c9b9069d1abb40c1f9b59a

在Git 2.28之前,这将选择第一个条目作为第二个提交,并将最后一个条目作为第一个提交。在Git 2.28及更高版本中,这将以列出顺序的修订版运行合并差异,这并不是很有用。组合差异需要第一个修订版是正引用。当您仅传递单个提交时,git show命令会正确处理此问题。


0

当你运行git diff并传递一个提交时,git会显示相对于该提交的当前工作树中的更改。

来自git文档

git diff [] [--] […​]此形式用于查看您在工作树中相对于命名的提交所做的更改。您可以使用HEAD将其与最新提交进行比较,或者使用分支名称将其与不同分支的末端进行比较。

插入符号(^)是对该提交的第一个父级的引用。类似地,您可以引用第二个(^2),第三个(^3)等。

通过运行git diff <firstCommit>^,您语义上要求“给我当前工作目录和第一个提交的第一个父级之间的更改”。第一个提交的父级不存在,因此它在语义上没有意义。

在git 2.17.1中运行这样的命令会产生错误:

$> git diff 9f3f6c8e4b1dea1de25febbb8248a6c430966236^
$> fatal: ambiguous argument '9f3f6c8e4b1dea1de25febbb8248a6c430966236^': unknown revision or path not in the working tree.

他使用的是 ^!,而不仅仅是 ^ - torek
问题标题是“git diff <first_commit>^是什么”,但还算公平。 - danielorn
是的,似乎stackoverflow吃掉了“!”字符。我无法将其添加到标题中! - torek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接