两个分支之间的 git diff 与合并期间的更改不匹配



我有两个分支,master和feature。 如果我这样做:

git diff --name-only master..feature

我得到了一长串文件,其中一些是源代码,所以没有被 .gitignore 排除

但是,当我尝试将功能合并到主控中时:

git checkout master
git merge feature

在合并过程中,我只在 master 中更改了一个文件。

为什么会这样?

另一个有趣的事情是,如果我尝试反向并将母版合并到功能中,则会删除在功能分支中创建的文件。

我该如何解决此问题并避免将来出现此问题?

这不是错误。

请考虑以下简单示例。 假设有一个名为example.txt的文件。 在分支X中,它写道:

This is
quite
a file.

在分支 Y 中,它写道:

This is
not
a file.

合并分支 X 和 Y 的结果应该是什么? 具体来说,您希望在名为example.txt的文件中显示哪些内容

我未能向您提供哪些信息(如果有)? 在回答这个问题之前,您还需要了解什么?

(在继续阅读之前,请尝试提出答案。

Git 是关于提交,而不是文件

在我们继续之前,让我们注意你在 Git 中处理的存储单位是提交,而不是文件。 确实,提交包含文件,但这里的一般想法是它是一个包交易:提交具有所有文件的完整快照。 如果我们进行一些起始提交:

git checkout somebranch

并将一个大文件bigfile.py拆分为两个较小的文件,small1.pysmall2.py完全删除bigfile.py然后提交,与旧提交相比,新提交缺少bigfile.py添加了两个较小的文件。 当我们签出旧提交时,我们只有三个文件中的一个 - 大文件 - 当我们签出新提交时,我们只有三个文件中的两个。 这是一个一揽子交易:你可以选择一个文件的提交,或者两个文件的提交,但你永远不会同时得到大文件和一个小文件,或者所有三个文件,或者其他组合。

尽管如此,提交包含文件,这在以后我们开始合并时会很重要。 但除了包含文件(这是它们的主要数据:每个文件的快照(就像您提交时它的外观一样)之外,每个提交都包含一些元数据或有关提交的信息。 这包括您在输出中看到的内容git log例如,提交人员的姓名和电子邮件地址,以及日期和时间戳。1

在所有这些元数据中,Git 在每次提交中存储一些早期提交的原始哈希 ID。 大多数提交只存储一个较早的提交哈希 ID。 这些哈希 ID 也是提交的"真实名称":它们是 Git 实际查找每个提交的方式。 提交存储在一个大的键值数据库中,提交的哈希 ID 是键,提交的内容是值。

每次提交都存储了上一个提交的哈希 ID,我们最终会得到一个很好的简单线性提交链。 如果我们使用大写字母来代表每个哈希 ID,我们会得到如下所示的绘图:

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

其中H是链中最后一个提交的哈希 ID。 在提交H中,Git 存储了之前提交G的实际哈希 ID。 在提交G中,Git 存储了更早的提交F的哈希 ID,等等。

这些链允许 Git 向后工作,从最新的提交返回到早期的提交。 这些Git 存储库中的历史记录,因此这些链对于使用 Git 至关重要。 而且,由于每个提交都存储一个完整快照,我们必须让 Git比较两个提交以查看更改的内容。 如果我们让 Git 将G中的快照与H中的快照进行比较 ,例如,这告诉我们当我们G进行H时我们更改了什么。

所以,这就是git log所做的:它从最新的提交(例如H)开始,打印出哈希 ID 和元数据,如果我们使用-p来获取补丁,提取GH(到临时内存区域)并比较两个提交的快照以找出更改的内容, 并告诉我们这一点。 然后,在显示提交H后,Git 向后移动一步以提交G:它打印出哈希 ID 和元数据,如果我们使用-p,则比较F-vs-G。 打印出G后,git log再往后退一步,F,依此类推。

(换句话说,Git是向后工作的。 我不会在这里强调这一点,但它解释了很多关于 Git 的信息,一旦你意识到这一点。


1如果你使用git log --pretty=fuller,你会看到每个提交实际上有两个:作者提交者。 每个都由三元组组成:姓名,电子邮件,时间戳。 如今,通常两者都是相同的,除了挑选提交,其中保留了原始提交的作者,提交者是执行挑选的人,提交者时间戳是挑选操作的时间。

>分支名称只是帮助我们找到

提交为了使上述工作,我们必须以某种方式知道链中最后一个提交的哈希ID。 我们需要将该哈希 ID 提供给 Git,因为 Git 最终只能通过其哈希 ID找到提交。 我们可以写下这些哈希ID,把它们记在纸上,或者白板上,或者其他东西。 但它们真的很大很丑,很难正确输入。 另外,我们有一台电脑。 为什么不让计算机为我们记住哈希 ID? 我们可以将第二个数据库添加到我们的 Git 存储库中:它将保存名称,例如masterdevelopfeature,并使用这些名称记住上次(最近、最有用、无论如何)提交的哈希 ID。

这就是分支名称:它是名称数据库中的一个条目。 实际名称扩展了一点:master真的很refs/heads/masterfeature真的很refs/heads/feature。 这为其他类型的名称留出了空间,例如标签名称:v2.1真的很refs/tags/v2.1。 但特别是对于分支名称,它们都持有提交哈希 ID(每个一个),并且该哈希 ID 是我们将考虑为"在分支上"的最后一次提交的 ID。

如果我们只有一个分支,一切都很容易:

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

在这里,分支名称master是唯一的名称,它保存着我们最近提交的哈希 ID,提交H。 因此,名称master指向链末尾的提交。 这让我们(和 Git)访问提交H. 向后提交H点以提交G,这让我们(和 Git)可以访问它;再次向后提交G点,依此类推。

如果我们现在创建一个新的分支名称,例如feature,我们可以选择任何现有的提交来指向这个新名称。 但是,大多数情况下,我们会选择正在使用的提交:H,通过master。 所以我们会得到:

...--F--G--H   <-- feature, master

现在我们遇到了一个问题。 我们使用哪个分支名称? 请记住,我们将添加一个特殊名称HEAD,并将其附加到这两个分支名称之一。 让我们将HEAD附加到feature— 如有必要,通过运行git checkout feature— 并绘制它:

...--F--G--H   <-- feature (HEAD), master

我们仍在使用提交H,但现在我们使用它是因为名称feature

现在让我们以通常的方式创建一个新的提交:修改一些文件,甚至可能创建新文件和/或删除现有文件,并根据需要使用git add和/或git rm来更新它们,并git commit结果。 无需过多担心所有细节,这让 Git 保存了一个新的快照,添加了一些元数据,并将集合写出为新的提交。 新提交获得一个新的、唯一的哈希 ID——看起来随机且不可预测,因为它取决于我们提交提交的确切时间——但我们只称其为提交I。 新提交将向后指向现有提交H

I
/
...--F--G--H

一旦新提交存在,甚至在我们回到能够运行更多命令之前,Git 现在会做它最后一个特殊技巧:它将新提交的哈希 ID 写入当前分支名称,即HEAD附加到的分支名称中。 由于这是feature,我们得到:

I   <-- feature (HEAD)
/
...--F--G--H   <-- master

提交H在提交I之前出现,但它仍然是master分支上的最后一次提交。 提交Ifeature上的最后一次提交,但通过H提交的提交也在feature

现在让我们继续对feature再做一个承诺:

I--J   <-- feature (HEAD)
/
...--F--G--H   <-- master

然后运行git checkout master. 这将使我们的HEAD远离feature,并将其附加到master。 它还将更新我们的工作区,以便我们使用提交H的内容,而不是提交J的内容:我们所有的文件现在都匹配H,而不是J。 我们所做的任何更新和快照到IJ中都安全地存储在那里,IJ,但现在它们已经从我们的视野中消失了,因为我们已经承诺H

I--J   <-- feature
/
...--F--G--H   <-- master (HEAD)

我们现在可以创建另一个新的分支名称,例如feature2,并附加HEAD

I--J   <-- feature
/
...--F--G--H   <-- feature2 (HEAD), master

然后在feature2上进行两个新提交:

I--J   <-- feature
/
...--F--G--H   <-- master

K--L   <-- feature2 (HEAD)

或者,我们可以继续直接在master上进行这些提交:

I--J   <-- feature
/
...--F--G--H

K--L   <-- master (HEAD)

就图形本身而言——提交集之间有向后箭头(这里画成线条,因为文本中可用的箭头图形很差)——没关系:我们不能更改任何现有的提交(永远),但我们总是可以添加新的提交,无论哪种方式,我们最终都会得到这组提交。 这只是哪些名称找到这些提交的问题。 但是 Git 允许我们随时创建、销毁或移动分支名称。 提交不会更改;只是我们用来查找它们的名称可能不同。

合并

是时候回答上面的问题了:缺少什么?

当我们在 Git 中合并一些提交时,这都是关于组合工作。 这个想法是,在某些系列的提交(也许I-J)中,有人做了一些工作,而某人(可能是其他人)在其他一系列的提交(K-L)中做了一些工作。 这给了我们这个:

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

K--L   <-- br2

由于提交的性质——它们永远不会改变——我们可以从这张图中看出,这两行工作从一个共同的起点开始,即提交H。 从视觉上看,很容易看出,J中的所有东西都是H的后裔,L也是如此。 它们也是G的后裔,但H"更好",因为它"更接近"端点提交。

现在,我们已经知道 Git 可以比较两个快照,例如GH,或IJ. 如果 Git 可以轻松地将H直接与J进行比较怎么办? 好吧,它可以;如果我们让 Git 这样做,我们就会发现从HJ有什么不同。 这就是某人在顶线上所做的工作。 所以这些是br1的变化.

同样,如果我们让 Git 将H中的内容与L中的内容进行比较,我们将找出某人在底线上做了哪些工作。 无论文件是什么不同,以及我们使用什么规则来更改H中的文件内容到L中的内容,这就是某人在br2上所做的。

这也告诉我们缺少什么。 为了合并example.txt,我们不仅需要两个端点文件(例如,一个在第 2 行说quite,另一个在第 2 行说not——还需要文件的基本副本example.txt的基本副本是提交H中文件的副本。 提交H是两个提示提交的合并基础,每个文件的副本是我们找出更改内容的方式。

如果基本副本显示:

This is
quite
a file.

然后我们知道仍然说quite的那一行没有任何变化,而在说not的那一行中改变了。

如果基本副本显示:

This is
not
a file.

然后我们知道仍然说not的那一行没有任何变化,而在说quite的那一行中改变了。

如果基本副本没有第 2 行 — 如果它读取,则完整:

This is
a file.

然后我们有一个合并冲突,因为两个人都做了一个改变:都添加了第 2 行,但他们添加了不同的第 2 行。

这对您的案件意味着什么

如果两个分支提示提交(以名称master找到的提交和以名称feature找到的分支提示提交)不同,那只是告诉我们它们是不同的。 Git 提出的配方,将更改一个提交以使其与另一个提交匹配,只是告诉我们如何将一个提示提交更改为另一个提示提交。

如果这两个分支提示提交之间的合并基提交是第三次提交,2我们需要知道第三次提交中的内容,因为这就是git merge如何找出master中更改的内容以及feature中更改的内容。然后,merge 命令将尝试合并这两更改,将合并的更改应用于合并基中的任何内容。

正如博士所评论的那样,您可以将三点表示法与git diff命令一起使用:

git diff master...feature

例如。 这有 Git:

  • 找到两个提示提交之间的合并基(我们称之为$B);
  • 运行相当于git diff $B feature

它告诉你feature上发生了什么变化,关于这个合并基础。 如果随后运行相同的命令,并交换两个名称:

git diff feature...master

Git 将找到相同两个提示提交的合并基础,3然后 diff$Bvsmaster:这显示了master上发生了哪些变化。

同样,git merge对这些情况所做的是:4

  • 运行两个差异,将输出保存在临时区域中;
  • 获取每个文件的合并基础版本;
  • 如果可能的话,合并差异;和
  • 组合的差异应用于文件的合并基本版本。

如果一切顺利,git merge将根据结果进行合并提交。 合并提交与常规非合并提交没有太大区别:它仍然具有所有文件的快照(由上面的合并过程构建)和一些元数据。 合并提交的特殊之处在于它将两个分支提示提交都列为其父级,以便 Git 可以沿着两个分支返回(现在通过合并提交将它们合并为一个"分支":这暴露了单词"branch"中的一个缺陷;请参阅">分支"到底是什么意思?)。


2这里有一些堕落的案例。 特别是,如果合并基是两个分支提示提交之一,我们要么有一个简单的"可快进"的情况,要么就没有什么可以合并的了。 但是,鉴于您发布的内容,您一定不能遇到这些情况之一。

3如果只有一个合并基提交(通常是这种情况),则两个分支提示提交的列出顺序并不重要。 但是,对于某些复杂的提交图,可能有两个或多个合并基提交。 在这里,情况变得相当模糊。 直到最近,git diff命令才很好地处理这个问题;git merge处理得更好,但它仍然很棘手。

4这个描述对你如何进行合并、图形的形状等做出了很多假设,并且在其他方面与git merge内部实际做的事情相比大大简化。 这个想法是捕捉总体目标,而不涉及一些更棘手的机制。 例如,这忽略了合并如何处理重命名文件的情况。