首页 > 解决方案 > 提交差异缺失/冲突相关?

问题描述

我团队的成员最近报告了他们的代码更改(已删除行),这些更改未显示在任何提交中,但仍在代码库中处于活动状态。从技术上讲,他们在功能分支中有代码,但该代码随后进入最终分支,缺少一些行。

使用普通的 git 命令搜索提交(git -S 'somexpr' ...,甚至git log -u搜索输出),我能够找到添加行的位置。但是在没有线条的活动分支中,我找不到这些线条后来再次消失。

我不是 git 专家,但我模糊地阅读了一些关于 git 默认情况下不显示合并差异的内容,所以我也尝试了选项-C--cc. 没有成功。

但是,使用git blame --reverse HASHWHERELINEEXISTS filename它,我能够找到该行最后出现的哈希前缀。然后使用git log手动搜索,我能够在日志中找到提交。在单独检查该提交和之前的提交时,我仍然无法在线条消失的情况下获得差异。

这让我怀疑这些行可能作为冲突解决的一部分消失了,并且这些差异通常不会在任何地方显示。

我想我终于设法强制 git 显示实际的差异(包括冲突解决或其他)。“技巧”基本上是执行git diff HASHFROMREVERSEBLAME..HASBEFORETHATONE(其中那些 HASH.. 值是指从上面提到的 git log 输出中复制粘贴)。

这留下了问题:

标签: git

解决方案


您的诊断是正确的:

我模糊地阅读了一些关于 git 默认情况下不显示合并差异的内容......

具体来说,git log -p遍历提交图(见下文),但当它遇到合并时,默认情况下不会打扰显示差异。您想要的是-m,可能与--first-parent. 请参阅下面的详细信息。

我还尝试了这些选项-C--cc...

-C选项在这里无关紧要(它被传递给差异引擎,它的意思是“查找整个文件副本”,它有其他用途但对您的问题没有任何好处)。(-c小写)和--cc(两个破折号和两个小写c字母)选项是相关的,但没有帮助,我们将在下面看到。

关于提交本身的知识

在 Git 中,每次提交:

  • 包含 Git 知道的所有文件的快照,采用特殊的、只读的、仅限 Git 的、压缩的和去重的格式;
  • 包含一些元数据:关于提交本身的信息;和
  • 由提交的哈希 ID 编号,看起来是随机的(不是,但没有简单的方法来预测它们并且它们没有排序)。

Git 通过它们的哈希 ID 号查找这些东西(提交和其他内部 Git 对象),因此您需要向 Git 提供提交号以使其执行任何有用的操作。然而,提交数字对人类没有用处。所以我们通常不使用它们——好吧,除非在特殊情况下,例如使用剪切和粘贴。相反,我们使用names。特别是我们倾向于使用分支名称远程跟踪名称master,如. 分支名称通过保存该提交的编号来标识一个特定的提交。origin/master

分支名称的特殊之处在于它始终保存该分支上最后一次提交的哈希 ID。这似乎没什么用——知道最后一次提交的哈希 ID 有什么好处,而不知道之前提交的哈希 ID?——直到我们提到,在其元数据中,每个提交都存储一个提交的提交号犯罪。Git 将此称为提交的父级。

这意味着我们可以绘制提交,早期的提交向左,后来的提交向右,如下所示:

... <-F <-G <-H

在这里,每个大写字母代表一些看起来很随机的哈希 ID。 特别是代表链中最后一次提交H的哈希 ID 。一旦我们找到了 commit ,我们就可以使用 commit的元数据(包含 commit 的哈希 ID)让 Git 找到 commit 。Commit反过来将较早提交的哈希 ID 存储在元数据中,因此从,我们可以移回较早的 commit ,依此类推。我们所需要的只是链中最后一次提交的哈希 ID——而这正是分支名称所包含的内容。HHGGGFGF

因此,我们可以更简单地将其绘制为:

...--F--G--H   <-- master

分支名称master为我们提供了哈希 ID,以便 Git 可以找到 commit H。我们省略了提交之间的箭头,因为我们知道一旦进行了新的提交,其中的任何内容都不会改变——任何文件或任何元数据都不会改变——而且 Git向后工作,从子节点到父节点,以查找提交.

为了进行的提交,Git 将:

  • 写出文件(以特殊的只读去重复 Git 格式);

  • 写出适当的元数据,包括提交的人的姓名和电子邮件地址,以及“现在”作为日期和时间戳——在这种情况下,H哈希 ID 作为新的提交——给我们:

     ...--F--G--H   <-- master
                 \
                  I
    
  • 最后,既然提交I存在——提交的创建为其分配了新的唯一哈希 ID——<code>git commit 将使名称 master保持其新的哈希 ID:

     ...--F--G--H
                 \
                  I   <-- master
    

没有理由在图纸中保留扭结;我们现在可以写:

...--H--I   <-- master

如何git log显示你提交

跳过一堆重要的细节,我们稍后会谈到其中一个,git log做的是向后遍历提交。通过从类似 的名称开始master,我们——或者无论如何 Git——可以找到最新的提交,例如 commit I。这包含 Git 在我们提交时知道的每个文件的完整快照I

接下来,Git 退回一个提交,到H. 这当然也有每个文件的完整快照。所以 Git 本质上提取了两个提交,并比较了两个提交中的所有文件。这是一种形式git diff,运行方式为git log -p比较任意两个提交。在这里,两个提交是I(walk 中的当前一个)和H(它的父级)。

对于相同的文件,差异代码根本不做任何事情。这些文件没什么好说的。对于不同的文件,diff 代码会提出一些更改,这些更改会将左侧 ( H) 提交副本更改为右侧 ( I) 提交副本。这就是你看到的差异。(对于全新的文件或被删除的文件,您也会在此处看到适当的配方。该-C选项告诉 Git:如果右侧文件是全新的,请查看它是否实际上全部或部分从某个现有文件复制在左侧提交中。

这对于这些普通的、简单的、单父提交来说很好,但它不适用于合并。

合并提交略有不同

当您使用git merge进行真正的合并时,合并提交:

  • 像往常一样保存快照;和
  • 像往常一样有元数据,除了
  • 在元数据中,合并提交包含多个先前提交编号。

这是最后一个事实,非常简单,它使提交成为合并提交。

我们可以像这样绘制一个合并提交:

...--I--J
         \
          M   <-- branch
         /
...--K--L

此合并提交有两个父级。大多数合并提交看起来像这样,尽管 Git 也支持他们所谓的章鱼合并,其中有三个或更多父级。

当我们在提交遍历过程中遇到合并提交时,git log必须变得更加复杂。默认情况下,它会以某种顺序遍历传入提交的两条腿,从Mback 到L,但也从Mback 到J. 您可以使用该--first-parent标志告诉图形遍历代码仅查看数字上的第一个父级,在 Git 中,它是您在运行时所在分支的提交git merge。(章鱼合并的另一个父级或多个父级是您合并的另一个或多个提交。)

但是git diff代码有问题。您无法真正将合并提交的快照与任一父级进行比较并获得一些明智的结果。同时,您不能同时将合并提交与所有父级进行比较……除非——好吧,这里的事情变得有点奇怪。

git log其本身而言,它的解决方案是根本不费心显示差异。不幸的是,这个解决方案完全隐藏了不正确的合并,这就是为什么你不能以这种方式找到错误的合并。

对于git show,它显示了一个提交的日志消息和一个补丁,默认的解决方案是使用--cc模式,这导致我们在 Git中对组合 diff的定义有些特殊。-c选项还产生组合差异,组合方法略有不同。但是由于组合差异的一个特殊功能,这两种方法对于您的特定问题都是无用的。

Git 的 combine diff 完全省略了一些文件

当 Git 产生组合差异时,它的作用是:

  • 对于合并提交子节点的每个父节点,进行快速比较以查找相同文件和不同文件。(由于是内部存储格式,加上文件去重,这部分速度很快。)

  • 对于在孩子中与同一文件的任何父母的副本完全相同的任何文件,根本不要说任何东西

  • 对于与每个父级中的相同文件不同的子级中的每个文件,对每个父级运行差异。然后显示该差异的一部分。(确切地说,您看到的部分取决于您是否使用-c--cc,但它们都非常相似。)

由于您的案例涉及意外进行合并提交的人使用与其父母之一完全相同的文件,而不是从父母双方进行更改,因此根据定义,组合差异将直接跳过该文件。所以这就是使组合差异在这里无用的原因。

-m选项_

-m选项 - 两者都可用 -git log 告诉 git showGit 假装,只是为了不同的目的,一个合并提交是 N 个单独的提交,其中 N 是父级的数量。也就是说,给定:

...--I--J
         \
          M   <-- branch
         /
...--K--L

git log命令仍将覆盖M,返回J,然后从J to I , and so on; and will still also cover L , and K , and so on, as needed, in some order. But while showing M` 本身,Git 将假装存在两个单独的提交,如下所示:

J--M1

L--M2

因此运行两个 git diff命令,一个将快照 inJ与快照 in进行比较,另一个将快照 in与 inM1进行比较(当然,两个“M”快照本身就是快照)。JM2M

在这两个差异之一中,您关心的文件根本不会改变。另一方面,您会看到应该执行的某些行(例如 fromL到)已M2更改为与 in 匹配J。这向您显示了错误的提交,以及谁做了它。

为什么很多这并不重要

除了教育做出错误提交的人之外,此时您唯一能做的就是进行新的提交以纠正文件。1 谁做出这个更正的提交并不重要。从字面上看,所有先前的提交都无法更改。因此,只需进行修复,提交它,然后继续前进。

历史改写

如果你愿意,你可以做人们所说的“历史改写”。在这里,我们进行了一系列提交:

...--I--J
         \
          M--N--O--P   <-- branch
         /
...--K--L

有问题的地方,比如 at M,然后进行一系列新的提交:

...--I--J
         \
          M'-N'-O'-P'   <-- replacement-branch
         /
...--K--L

旧的提交继续存在,但由于我们通过name找到它们,所以我们现在要做的就是让每个人将 name 交换replacement-branch为 name branch,反之亦然。然后(坏)提交将在新名称下找到,新(好)提交将在旧名称下找到。

历史重写的问题是我们必须说服每个拥有存储库克隆的人更改他们的分支名称。虽然提交是共享的——你将一个 Git 连接到另一个 Git,而没有提交的 Git 通常会从另一个 Git 获取这些提交——Git 存储库的每个副本都有自己的私有分支名称。所以使用“错误”分支的每个人都必须更新他们的私有分支名称,以便它使用新的和改进的提交。获得这些提交很容易,但默认情况下会将新的和改进的提交与旧的错误提交合并,这正是你不想要的。

尽管如此,如果没有太多的历史记录,并且很少有提交错误的克隆,那么历史记录重写技巧可能是一个不错的选择。但是,当涉及合并时,这并不是微不足道的——而且不值得在这里写下来(还有其他 StackOverflow 的答案涵盖了这类事情)。


推荐阅读