让 git 在命令行中显示大型提交中单个文件发生的更改,而无需更改 HEAD



假设我在一个具有许多分支的大型存储库中有一些提交。

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因此BC之间的任何更改都是感兴趣的。 向git show命令添加一个或多个路径名会将差异限制为所选文件中更改的内容。

合并提交略有不同

当应用于合并提交时,整个想法会失败。 合并提交是具有两个或多个父提交的提交。 除了有两个或多个父级之外,合并提交与任何其他提交一样:它具有快照和元数据。

通常,我们通过运行git merge来获得合并提交。 (还有其他方法可以进行合并提交,git merge并不总是进行合并提交,因此这里没有保证一对一的对应关系,但这是进行合并的常用方法。 顺便说一下,请注意,名词形式,合并,指的是合并提交,可能由git merge;要合并的动词形式是git merge用来提供快照内容的过程。

合并过程实际上至少涉及三个提交。 如果我们有两个不同的分支,我们可以通过绘制我们得到的设置来了解典型的合并是如何发生的:

I--J   <-- br1
/
...--G--H

K--L   <-- br2

在这里,两个分支名称br1br2分别选择提交JL。 与所有提交一样,提交JL具有快照和父级:

  • 提交J的(单个)父级是提交I。 提交I当然也有快照和父级;它的父级是提交H
  • 提交L的(单个)父级是提交K,其父级是提交H

所以两个分支都是从一堆共享提交中衍生出来的,以提交H结束。 提交HG以及之前的任何内容都在两个分支上,但由于提交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 --continuegit commit以指示我们修复了合并冲突并提供了正确的最终快照。 无论哪种方式,Git 都会使这个新的提交M- 但不是仅链接回JL,而是新快照链接回JL,如下所示:

I--J
/    
...--G--H      M   <-- br1 (HEAD)
    /
K--L   <-- br2

显示合并提交

鉴于git show需要差异提交,它应该差异哪些提交? 如果我们从 合并提交M,我们可以将其快照与J中的快照进行比较,或者与L中的快照进行比较。 结果会有所不同,具体取决于我们选择的父母。

Git 的默认答案不是只选择一个。 相反,Git 可以:

  • 比较JM以及
  • 比较LM

然后以一种相当奇特的方式组合这两个差异。 有两种不同的方法可以组合差异。 一个称为组合分,另一个称为密集组合差分。 它们非常相似,并且它们都有一个奇怪的功能:它们完全丢弃与任何父项完全匹配的任何文件。因此,例如,如果M中的core/library_foo/foo.cpp匹配core/library_foo/foo.cppJcore/library_foo/foo.cppLgit show根本不会显示它。

(我怀疑组合差异背后的目标是展示如何解决合并冲突。 不幸的是,当通过获取--ours--theirs文件来解决合并冲突时,用于显示组合差异的算法完全无法做到这一点,即使这是一个错误。 但这只是猜测;我们实际拥有的是代码。 行为如下所述:仅当合并结果与所有父项不同时才显示差异。

如何解决此问题

有几种简单的方法可以解决此问题:

  • 您可以直接使用git diff

    例如,假设您要将提交5643f6a7c与其第一个父级进行比较。 然后git diff 5643f6a7c^ 5643f6a7c将完成这项工作。 实际上,通过直接使用git diff,您可以准确选择要使用的父项。

  • 您可以使用git show -m.

    -m标志指示git show"虚拟拆分"合并。 假设合并M有父JL,如上例所示。 然后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^

最新更新