繁体   English   中英

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

[英]git diff between two branches not matching the changes during a merge

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

git diff --name-only master..feature

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

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

git checkout master
git merge feature

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

为什么会这样?

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

我该如何解决这个问题并在将来避免这个问题?

那不是错误。

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

This is
quite
a file.

在分支 Y 中,它显示:

This is
not
a file.

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

如果有的话,我没有给你什么信息? 回答这个问题之前,您还需要知道什么?

(在继续阅读之前尝试想出一个答案。)

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

在我们讨论 go 之前,让我们注意您在 Git 中处理的存储单元是提交,而不是文件。 提交确实包含文件,但这里的一般想法是它是 package 交易:提交具有所有文件的完整快照 如果我们采取一些开始提交:

git checkout somebranch

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

尽管如此,提交还是包含文件,这在我们稍后进行合并时会很重要。 但除了包含文件——这是它们的主要数据:每个文件的快照(按照你提交时的显示方式)——每个提交都包含一些元数据,或关于提交的信息。 这包括您在git log output 中看到的内容:提交人的姓名和 email 地址,以及日期和时间戳,例如。 1

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

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

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

其中H是链中最后一次提交的 hash ID。 在提交H中,Git 存储了之前提交G的实际 hash ID。 在提交G中,Git 存储了之前提交F的 hash ID,依此类推。

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

所以,这就是git log所做的:它从最新的提交开始(例如H ),打印出 hash ID 和元数据,如果我们使用-p来获取补丁,提取GH (到临时 ZCD6981B4957F08CDE2 area) 并比较两个提交的快照以找出发生了什么变化,并向我们展示。 然后,在显示提交H后,Git 后退一步提交G :它打印出 hash ID 和元数据,如果我们使用-p ,则比较F -vs- G 打印出G后, git log后移一步到F ,依此类推。

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


1如果您使用git log --pretty=fuller ,您会看到每个提交实际上有两个:作者提交者。 每一个都由一个三元组组成:名称、email、时间戳。 这些天通常两者都是相同的,除了樱桃挑选的提交,原始提交的作者被保留,提交者是挑选樱桃的人,提交者时间戳是樱桃的时间-选择动作。


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

为了完成上述工作,我们必须以某种方式知道链中最后一个提交的 hash ID。 We need to give that hash ID to Git, because Git can only find commits by their hash IDs, in the end. 我们可以写下这些 hash ID,将它们记在纸上、白板上或其他东西上。 但它们真的又大又丑,而且很难正确输入。 另外,我们有一台电脑 为什么不让计算机为我们记住 hash ID? 我们可以将第二个数据库添加到我们的 Git 存储库中:它将包含名称,例如masterdevelopfeature ,并且使用这些名称,请记住最后(最近,最有用,无论如何)提交的 hash ID。

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

如果我们只有一个分支,一切都很简单:

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

在这里,分支名称master是唯一的名称,它包含我们最近提交的 hash 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

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

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

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

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

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

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

让我们提前 go 并再次提交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)

或者,我们可以提前 go 并直接在master上进行这些提交:

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

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

合并

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

当我们在 Git 中合并一些提交时,这就是合并工作 这个想法是某人,在某些提交系列(也许是IJ )中做了一些工作,而某人——可能是其他人——在其他一些提交系列( KL )中做了一些工作。 这给了我们这个:

          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找到,一个由 name feature找到——是不同的,这只是告诉我们它们是不同的。 Git 提出的配方,将更改一个提交以使其匹配另一个提交,只是告诉我们如何将一个提示提交更改为另一个提示提交。

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

正如phd 所评论的,您可以将三点符号与git diff命令一起使用:

git diff master...feature

例如。 这有 Git:

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

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

git diff feature...master

Git 将找到相同的两个提示提交的合并基础, 3然后 diff $B vs master :这向您展示了master上的变化。

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

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

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


2这里一些退化的情况。 特别是,如果合并基础是两个分支提示提交之一,我们要么有一个简单的“可快速前进”的情况,要么没有什么可合并的。 但是,鉴于您发布的内容,您一定不能遇到这些情况之一。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM