假设我在一个具有许多分支的大型存储库中有一些提交。
projectname:some/branch* λ git log --oneline -n4 --graph
* 742b5fd1a (HEAD -> some/branch) Added many bugs
| * 16963a72a (TAG: Release_9, upstream/version_B) Release Test fix
| * 5643f6a7c (tag: RELEASE_8) Fixed bug
| * e31f00146 (tag: RELEASE_7) Fixed race condition
我想查看RELEASE_8(5643f6a7c
)core/library_foo/foo.cpp
发生的更改,而无需签出RELEASE_8。我无法签出,因为存储库的大小非常大,因此签出需要很长时间(许多分钟)。此提交还更改了几百个文件,因此希望将其限制为单个文件。
我希望执行以下操作,但两者都不起作用(它们只是显示 git 提交消息而没有文件的文本差异,或者给我错误)。此文件确实更改了该提交,基于在崇高合并和 gitk 中查看提交。
git show 5643f6a7c core/library_foo/foo.cpp
git show 5643f6a7c:core/library_foo/foo.cpp
git show 5643f6a7c -- core/library_foo/foo.cpp
git log 5643f6a7c core/library_foo/foo.cpp
git log 5643f6a7c:core/library_foo/foo.cpp
git log 5643f6a7c -- core/library_foo/foo.cpp
由于此示例是在私有存储库上完成的,因此我不得不调整文件路径、提交哈希、提交消息、标记名称和分支名称。这是针对最近的 git (2.29.0)。
根据评论,事实证明提交5643f6a7c
实际上是一个合并提交。git show
命令对合并提交具有特殊处理,最终显示此文件没有差异。
git show
最终什么都不显示的原因在细节上有点复杂,但与以下内容有关:
- 都包含一个完整快照:每个文件的副本,与您(或任何人)提交时的形式相同。
- 每个提交还包含有关提交本身的信息。 我们称之为提交的元数据,以将其与主数据(快照)区分开来。 例如,这包括提交者的姓名和电子邮件地址。 它还包括提交的父提交的原始哈希 ID。
它是父链接,从提交到其前身提交或提交,允许 Git 向你显示差异。 大多数提交只有一个父级。 鉴于提交C
遵循较早的提交B
,并且两个提交都有快照,Git 可以简单地提取到临时区域(在内存或其他区域中)两个快照,然后比较它们。 对于每个相同的文件,Git 什么也没说。 对于每个不同的文件,Git 会打印文件名和配方。 应用更改配方,这将转换文件副本,因为它出现在提交B
中,使其与文件副本在提交C
中显示的副本相匹配。
这个方法,用于将父提交B
更改为子提交C
,是差异的一种形式。 此差异是git show
在从提交C
打印元数据的选定(和格式化)部分后显示的内容。git show
命令可以做到这一点,因为只有一个较早的提交:commitB
先于提交C
因此B
和C
之间的任何更改都是感兴趣的。 向git show
命令添加一个或多个路径名会将差异限制为所选文件中更改的内容。
合并提交略有不同
当应用于合并提交时,整个想法会失败。 合并提交是具有两个或多个父提交的提交。 除了有两个或多个父级之外,合并提交与任何其他提交一样:它具有快照和元数据。
通常,我们通过运行git merge
来获得合并提交。 (还有其他方法可以进行合并提交,git merge
并不总是进行合并提交,因此这里没有保证一对一的对应关系,但这是进行合并的常用方法。 顺便说一下,请注意,名词形式,合并,指的是合并提交,可能由git merge
;要合并的动词形式是git merge
用来提供快照内容的过程。
合并过程实际上至少涉及三个提交。 如果我们有两个不同的分支,我们可以通过绘制我们得到的设置来了解典型的合并是如何发生的:
I--J <-- br1
/
...--G--H
K--L <-- br2
在这里,两个分支名称br1
和br2
分别选择提交J
和L
。 与所有提交一样,提交J
和L
具有快照和父级:
- 提交
J
的(单个)父级是提交I
。 提交I
当然也有快照和父级;它的父级是提交H
。 - 提交
L
的(单个)父级是提交K
,其父级是提交H
。
所以两个分支都是从一堆共享提交中衍生出来的,以提交H
结束。 提交H
和G
以及之前的任何内容都在两个分支上,但由于提交H
是最后一次这样的提交,因此它也是执行合并工作的最佳提交 -合并操作的合并部分。
为了将我们在分支br1
中所做的任何工作与任何人在分支br2
中所做的任何工作合并,我们git checkout br1
选择提交J
作为我们当前的提交:
I--J <-- br1 (HEAD)
/
...--G--H
K--L <-- br2
然后运行git merge br2
告诉 Git 找到提交L
并开始合并过程。 Git 知道我们在J
,找到L
,并使用每个提交的连接来找到最好的共享提交,H
,用作 Git 所说的合并基础。
由于每次提交都包含一个快照,因此H
保留一个快照。 通过将H
中的快照与J
中的快照进行比较,Git 可以弄清楚我们在br1
中更改了什么。 同样,通过将H
中的快照与L
中的快照进行比较,Git 可以找出它们在br2
中更改了什么。 然后,Git 可以组合这些更改,这是合并、合并为动词过程的核心。 通过将组合更改应用于从提交H
拍摄的快照,Git保留了我们的工作,但也添加了他们的工作。
如果这个合并过程一切顺利,Git 可以自己创建一个新的合并提交M
。 如果没有,Git 会停止并让我们提供正确的最终快照。 我们修复了所有冲突,在需要时使用git add
,并运行git merge --continue
或git commit
以指示我们修复了合并冲突并提供了正确的最终快照。 无论哪种方式,Git 都会使这个新的提交M
- 但不是仅链接回J
或仅L
,而是新快照链接回J
和L
,如下所示:
I--J
/
...--G--H M <-- br1 (HEAD)
/
K--L <-- br2
显示合并提交
鉴于git show
需要差异提交,它应该差异哪些提交? 如果我们从 合并提交M
,我们可以将其快照与J
中的快照进行比较,或者与L
中的快照进行比较。 结果会有所不同,具体取决于我们选择的父母。
Git 的默认答案不是只选择一个。 相反,Git 可以:
- 比较
J
与M
,以及 - 比较
L
与M
然后以一种相当奇特的方式组合这两个差异。 有两种不同的方法可以组合差异。 一个称为组合差分,另一个称为密集组合差分。 它们非常相似,并且它们都有一个奇怪的功能:它们完全丢弃与任何父项完全匹配的任何文件。因此,例如,如果M
中的core/library_foo/foo.cpp
匹配core/library_foo/foo.cpp
J
或core/library_foo/foo.cpp
L
,git show
根本不会显示它。
(我怀疑组合差异背后的目标是展示如何解决合并冲突。 不幸的是,当通过获取--ours
或--theirs
文件来解决合并冲突时,用于显示组合差异的算法完全无法做到这一点,即使这是一个错误。 但这只是猜测;我们实际拥有的是代码。 行为如下所述:仅当合并结果与所有父项不同时才显示差异。
如何解决此问题
有几种简单的方法可以解决此问题:
您可以直接使用
git diff
。例如,假设您要将提交
5643f6a7c
与其第一个父级进行比较。 然后git diff 5643f6a7c^ 5643f6a7c
将完成这项工作。 实际上,通过直接使用git diff
,您可以准确选择要使用的父项。您可以使用
git show -m
.-m
标志指示git show
"虚拟拆分"合并。 假设合并M
有父J
和L
,如上例所示。 然后git show -mhash-of-M
将显示两个差异。 第一个将J
,第一个父母,与M
进行比较。 第二个将L
,第二个父母,与M
进行比较。您可以使用
git show --first-parent -m
.和以前一样,
-m
选项拆分合并,但这一次,--first-parent
选项禁止显示除第一个父级差异之外的所有差异。
第一个父概念利用了这样一个事实,即在所有情况下,由git merge
生成的合并提交按顺序写出父项:
- 新合并提交的第一个父级是我们运行
git merge
时当前提交的提交。 对于我们的示例,这是提交J
,因为我们在运行git merge
之前运行了git checkout br1
。
其余的父项 - 是我们在命令行上命名的父项,按我们命名的顺序排列。
没有标志来挑选任何特定的父级,但我们可以使用帽子后缀符号,如5643f6a7c^
,这样做:5643f6a7c^2
表示第二个父项,5643f6a7c^3
表示第三个父项,依此类推。 帽子后面的数字是父母的选择。 但是,无论如何,大多数合并提交只有两个父项,因此^1
和^2
是最明智的后缀。 如果您省略了数字(如5643f6a7c^
),则表示第一个父母。
请注意,~
后缀,如5643f6a7c~4
,表示重复^1
多次。 因此,在使用5643f6a7c~4
时,我们返回到提交的第一个父级,然后返回到该提交的第一个父级,依此类推。 (这依赖于这样一个事实,即提交几乎总是至少有一个父级。 对于这种特殊情况,没有特别的理由使用~
后缀,但如果它更容易键入,您可以使用5643f6a7c~
或5643f6a7c~1
代替5643f6a7c^
。