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 合并到 feature 中,在 feature 分支中创建的文件将被删除。
我该如何解决这个问题并避免以后再出现这个问题?
这不是错误。
考虑以下简单示例。假设有一个名为 example.txt
的文件。在分支 X 中,它显示为:
This is
quite
a file.
在分支 Y 中,它显示为:
This is
not
a file.
合并分支 X 和 Y 的结果应该是什么?具体来说,您希望在名为 example.txt
?
的文件中显示什么 内容
我没有给你什么信息,如果有的话?在 回答 这个问题之前,您还需要了解什么?
(在继续阅读之前尝试想出一个答案。)
Git 是关于提交,而不是文件
在继续之前,请注意您在 Git 中处理的存储单元是 commit,而不是文件。确实提交 包含 文件,但这里的一般想法是它是一揽子交易:提交有 完整快照 of all 个文件。如果我们开始提交:
git checkout somebranch
并将一个大文件 bigfile.py
拆分为两个较小的文件 small1.py
和 small2.py
以及 删除 bigfile.py
完全然后提交,新提交 lacks bigfile.py
and adds 这两个较小的文件,与旧提交相比。当我们检出旧提交时,我们只有三个文件中的一个——大文件——而当我们检出新提交时,我们只有三个文件中的两个。这是一揽子交易:您可以选择一个文件的提交,或者两个文件的提交,但是您永远不会同时获得大文件和 一个 小文件,或者所有三个文件,或其他一些组合。
仍然,提交 包含 文件,这在我们开始合并时很重要。但除了包含文件——这是它们的主要数据:每个 文件的快照(按照你提交时它出现的方式)——每个提交包含一些元数据,或有关提交的信息。这包括您在 git log
输出中看到的内容:提交人的姓名和电子邮件地址,以及 date-and-time-stamp,例如。1
在所有这些元数据中,Git 在每次提交中存储一些早期提交的原始 哈希 ID。大多数提交只存储一个较早的提交哈希 ID。这些哈希 ID 也是提交的“真实名称”:它们是 Git 实际上 找到 每个提交的方式。提交存储在一个大key-value database中,以提交的哈希ID为键,提交的内容为值。
随着每个提交存储 前一个 提交的哈希 ID,我们最终得到一个很好的简单线性提交链。如果我们用大写字母代表每个哈希 ID,我们得到的图如下所示:
... <-F <-G <-H
其中 H
是链中 last 提交的哈希 ID。在提交 H
中,Git 存储了早期提交 G
的实际哈希 ID。在提交 G
、Git 中存储了 still-earlier 提交 F
的哈希 ID,依此类推。
这些链允许 Git 向后工作,从 最新的 提交回到更早的提交。这些 是 Git 存储库中的历史,因此这些链对于使用 Git 至关重要。而且,由于每个提交都存储了一个 完整快照 ,我们必须 Git 比较 两个提交以查看发生了什么变化。例如,如果我们 Git 将 G
中的快照与 H
中的快照进行比较,这会告诉我们在 制作 时我们更改了什么 H
来自 G
.
所以,这就是 git log
所做的:它从最近的提交(例如 H
)开始,打印出哈希 ID 和元数据,如果我们使用 -p
获取补丁,提取 G
和 H
(到临时内存区域)并比较两个提交的快照以找出更改的内容,并向我们展示。然后,显示提交 H
,Git 向后移动一步提交 G
:它打印出哈希 ID 和元数据,如果我们使用 -p
,比较 F
-对-G
。打印出 G
、git log
后退一步到 F
,依此类推。
(换句话说,Git 工作 向后 。我不会在这里多强调这一点,但它解释了很多关于 Git,一旦你意识到这一点。)
1如果你使用git log --pretty=fuller
,你会看到每个提交实际上有两个:author和一个 提交者 。每个都由三元组组成:姓名、电子邮件、时间戳。这些天通常两者都是相同的,除了 cherry-picked 提交,其中保留原始提交的作者,提交者是执行 cherry-pick 的人,提交者时间戳是时间cherry-pick 动作。
分支名称只是帮助我们 查找 提交
为了完成上述工作,我们有以某种方式知道链中 last 提交的哈希 ID。我们需要将该哈希 ID 提供给 Git,因为 Git 最终只能 找到 通过其哈希 ID 提交。我们可以写下这些哈希 ID,将它们记在纸上、白板上或其他东西上。但它们又大又丑,而且很难正确输入。另外,我们有一台 计算机 。为什么不让 computer 为我们记住哈希 ID?我们可以将第二个数据库添加到我们的 Git 存储库:它将包含 names,例如 master
或 develop
或 feature
,以及使用这些名称,记住 last(最近的,最有用的,无论如何)提交的哈希 ID。
这就是分支名称:它是名称数据库中的一个条目。实际名称扩展了一点:master
实际上是 refs/heads/master
,feature
实际上是 refs/heads/feature
。这为其他类型的名称留出了空间,例如标签名称:v2.1
实际上是 refs/tags/v2.1
。但特别是对于分支名称,它们都持有 提交哈希 ID——每个一个——并且该哈希 ID 是我们提交的 last 的 ID会考虑“在分支上”。
如果我们只有一个分支,一切都很简单:
...--F--G--H <-- master
在这里,分支名称 master
是唯一的名称,它包含我们最近提交的哈希 ID,提交 H
。所以 name master
指向 链末端的提交。这让我们(和 Git)可以访问提交 H
。提交 H
向后指向提交 G
,这让我们(和 Git)可以访问它; commit G
points backwards again, 依此类推。
如果我们现在创建一个 new 分支名称,例如 feature
,我们可以选择任何现有的提交来使用这个新名称 point-to .不过,大多数情况下,我们会选择我们正在使用的提交:H
,通过 master
。所以我们会得到:
...--F--G--H <-- feature, master
现在我们有问题了。我们使用的是哪个分支名称?为了记住,我们将添加一个特殊名称 HEAD
,并将其附加到这两个分支名称之一。让我们将 HEAD
附加到 feature
——如有必要,通过 运行ning git checkout feature
——然后绘制:
...--F--G--H <-- feature (HEAD), master
我们仍在使用提交 H
,但现在我们使用它是因为名称 feature
。
现在让我们以通常的方式创建一个新的提交:修改一些文件,甚至可能创建新的 and/or 删除现有的,然后使用 git add
and/or git rm
根据需要更新它们,然后 git commit
结果。无需过多担心所有细节,这 Git 保存了一个新快照,添加了一些元数据,并将集合作为新提交写出。新提交获得一个新的、唯一的哈希 ID——某种东西 random-looking,并且不可预测,因为它取决于我们进行提交的 确切时间 ——但我们只需调用它提交 I
。新提交将向后指向现有提交 H
:
I
/
...--F--G--H
一旦新的提交 存在 ,甚至在我们恢复能够 运行 更多命令之前,Git 现在执行它的最后一个特殊技巧:它将新提交的哈希 ID 写入 当前分支名称 ,即 HEAD
是 attached-to。因为那是 feature
,我们得到:
I <-- feature (HEAD)
/
...--F--G--H <-- master
提交 H
就在提交 I
之前,但它仍然是 master
分支上的 last 提交。提交 I
是 feature
上的 最后一个 提交,但 H
之前的提交也在 feature
上。
现在让我们继续在 feature
上再提交一次:
I--J <-- feature (HEAD)
/
...--F--G--H <-- master
然后是运行git checkout master
。这将使我们的 HEAD
远离 feature
并将其附加到 master
。它还将更新我们的工作区,以便我们使用提交 H
的内容,而不是提交 J
的内容:我们所有的文件现在匹配 H
,而不是 J
.我们对 I
和 J
所做的任何更新和快照都安全地存储在那里,在 I
和 J
中,但它们从我们的 视图中消失了 现在,因为我们已经提交 H
out:
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)
就 图本身 而言——提交集与它们之间的 backwards-pointing 箭头(此处绘制为线,因为文本中可用的箭头图形是差)—没关系:我们不能 更改 任何 现有 提交(永远),但我们总是可以添加新的提交,并且无论哪种方式,我们最终都会得到这组提交。这只是 names find 这些提交的问题。但是 Git 允许我们随时创建、销毁或移动 分支名称 。提交不会改变;只是我们用来 find 的 names 可能不同
正在合并
是时候回答上面的问题了:缺少什么?
当我们合并 Git 中的一些提交时,这就是合并工作。这个想法是,某人在某些提交系列(I-J
中)做了一些工作,而另一个人(可能是其他人)在其他一些提交系列中(K-L
)做了一些工作。这给了我们这个:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
由于提交的性质——它们永远不会改变——我们可以从这张图中看出,这两行工作从共同开始点,即提交H
。从视觉上很容易看出 J
中的所有内容都是从 H
继承而来的,L
也是如此。它们也是 G
的后代,但是 H
“更好”,因为它“更接近” end-point 提交。
现在,我们已经知道 Git 可以比较两个快照,如 G
和 H
,或 I
和 J
。如果 Git 可以轻松地将 H
直接与 J
进行比较会怎么样?好吧,它 可以; 如果我们 Git 这样做,我们会发现 与 H
有什么不同到 J
。这是 某人在第一行所做的工作。所以这些是 br1
.
中的 变化
同样,如果我们 Git 将 H
中的内容与 L
中的内容进行比较,我们将在底线上找出某人所做的工作。无论文件有何不同,无论我们使用什么规则将 H
中的文件内容 更改为 L
中的文件内容,这就是有人在 br2
上所做的].
这也告诉我们缺少了什么。为了合并 example.txt
,我们不仅需要两个 end-point 文件——例如,一个在第 2 行说 quite
,另一个在第 2 行说 not
——还有文件的 基本副本 。 example.txt
的基础副本是提交 H
中文件的副本。提交 H
是两个提示提交的 合并基础 ,每个文件的副本是我们找出 更改的内容 的方式。
如果基本副本说:
This is
quite
a file.
然后我们知道 没有任何变化 在仍然说 quite
的那一行,并且在说 not
.[=170 的那一行改变了一行=]
如果基本副本说:
This is
not
a file.
然后我们知道 没有任何变化 在仍然说 not
的那一行,并且在说 quite
.[=170 的那一行改变了一行=]
如果基本副本没有第 2 行——如果它完整地读取:
This is
a file.
然后我们有一个合并冲突,因为两个人都做了一个改变:都添加了一个line-2,但是他们添加了不同的第 2 行。
这对您的情况意味着什么
如果两个分支提示提交 - 一个由名称 master
找到的,一个由名称 feature
找到的 - 不同,这只是告诉我们它们是不同的。 Git 提出的配方,将更改一个提交以使其匹配另一个提交,只是告诉我们如何将一个提示提交更改为另一个提示提交。
如果这两个 branch-tip 提交之间的 merge base 提交是一些 third 提交,2 我们需要知道第三次提交中的内容,因为 那是 git merge
如何弄清楚 master
中发生了什么变化以及 feature
。然后,合并命令将尝试合并那些两组更改,将合并的更改应用于合并基础中的任何内容。
作为phd commented,你可以在git diff
命令中使用triple-dot表示法:
git diff master...feature
例如。这有 Git:
- 找到两个 tip 提交之间的合并基础(我们称之为
$B
);然后
- 运行相当于
git diff $B feature
它告诉您 feature
上关于此合并基础的更改。如果您然后 运行 相同的命令,但两个名称互换:
git diff feature...master
Git 将找到相同的两个 tip 提交的合并基础,3 然后 diff $B
vs master
:这表明你在 master
.
发生了什么变化
同样,git merge
对这些情况的作用是:4
- 运行 两者 diffs,将输出保存在临时区;
- 获取每个文件的merge base版本;
- 如果可能的话,合并差异;和
- 将 combined 差异应用于文件的合并基础版本。
如果一切顺利,git merge
将从结果中进行合并提交。合并提交与常规 non-merge 提交没有太大区别:它仍然具有所有文件的快照(由上面的合并过程构建)和一些元数据。合并提交的特殊之处在于它列出了 both branch-tip 提交作为其父项,因此 Git 可以 g返回两个分支(现在通过合并提交合并为一个“分支”:这暴露了“分支”一词的缺陷;参见What exactly do we mean by "branch"?)。
2这里有一些退化的情况。特别是,如果合并基础是两个分支提示提交之一,我们要么有一个简单的“fast-forward-able”案例,要么没有要合并的东西。不过,鉴于您发布的内容,您一定没有这些情况之一。
3如果只有一个合并基础提交——通常是这种情况——两个分支尖端提交的列出顺序无关紧要。对于一些复杂的提交图,但是,可能有两个或更多合并基础提交。在这里,画面变得相当模糊。 git diff
命令直到最近才处理得很好; git merge
处理得更好,但仍然很棘手。
4这个描述对你如何进行合并、图形的形状等做出了很多假设,并且在其他方面大大简化了 git merge
内部确实如此。这个想法是为了抓住总体目标,而不是进入一些更棘手的机制。例如,这忽略了合并如何处理重命名文件的情况。
我有两个分支,master 和 feature。 如果我这样做:
git diff --name-only master..feature
我得到一长串文件,其中一些是源代码,所以没有被 .gitignore
排除但是,当我尝试将功能合并到 master 中时:
git checkout master
git merge feature
在合并过程中,我在 master 中只更改了一个文件。
为什么会这样?
另一个有趣的事情是,如果我尝试反向将 master 合并到 feature 中,在 feature 分支中创建的文件将被删除。
我该如何解决这个问题并避免以后再出现这个问题?
这不是错误。
考虑以下简单示例。假设有一个名为 example.txt
的文件。在分支 X 中,它显示为:
This is
quite
a file.
在分支 Y 中,它显示为:
This is
not
a file.
合并分支 X 和 Y 的结果应该是什么?具体来说,您希望在名为 example.txt
?
我没有给你什么信息,如果有的话?在 回答 这个问题之前,您还需要了解什么?
(在继续阅读之前尝试想出一个答案。)
Git 是关于提交,而不是文件
在继续之前,请注意您在 Git 中处理的存储单元是 commit,而不是文件。确实提交 包含 文件,但这里的一般想法是它是一揽子交易:提交有 完整快照 of all 个文件。如果我们开始提交:
git checkout somebranch
并将一个大文件 bigfile.py
拆分为两个较小的文件 small1.py
和 small2.py
以及 删除 bigfile.py
完全然后提交,新提交 lacks bigfile.py
and adds 这两个较小的文件,与旧提交相比。当我们检出旧提交时,我们只有三个文件中的一个——大文件——而当我们检出新提交时,我们只有三个文件中的两个。这是一揽子交易:您可以选择一个文件的提交,或者两个文件的提交,但是您永远不会同时获得大文件和 一个 小文件,或者所有三个文件,或其他一些组合。
仍然,提交 包含 文件,这在我们开始合并时很重要。但除了包含文件——这是它们的主要数据:每个 文件的快照(按照你提交时它出现的方式)——每个提交包含一些元数据,或有关提交的信息。这包括您在 git log
输出中看到的内容:提交人的姓名和电子邮件地址,以及 date-and-time-stamp,例如。1
在所有这些元数据中,Git 在每次提交中存储一些早期提交的原始 哈希 ID。大多数提交只存储一个较早的提交哈希 ID。这些哈希 ID 也是提交的“真实名称”:它们是 Git 实际上 找到 每个提交的方式。提交存储在一个大key-value database中,以提交的哈希ID为键,提交的内容为值。
随着每个提交存储 前一个 提交的哈希 ID,我们最终得到一个很好的简单线性提交链。如果我们用大写字母代表每个哈希 ID,我们得到的图如下所示:
... <-F <-G <-H
其中 H
是链中 last 提交的哈希 ID。在提交 H
中,Git 存储了早期提交 G
的实际哈希 ID。在提交 G
、Git 中存储了 still-earlier 提交 F
的哈希 ID,依此类推。
这些链允许 Git 向后工作,从 最新的 提交回到更早的提交。这些 是 Git 存储库中的历史,因此这些链对于使用 Git 至关重要。而且,由于每个提交都存储了一个 完整快照 ,我们必须 Git 比较 两个提交以查看发生了什么变化。例如,如果我们 Git 将 G
中的快照与 H
中的快照进行比较,这会告诉我们在 制作 时我们更改了什么 H
来自 G
.
所以,这就是 git log
所做的:它从最近的提交(例如 H
)开始,打印出哈希 ID 和元数据,如果我们使用 -p
获取补丁,提取 G
和 H
(到临时内存区域)并比较两个提交的快照以找出更改的内容,并向我们展示。然后,显示提交 H
,Git 向后移动一步提交 G
:它打印出哈希 ID 和元数据,如果我们使用 -p
,比较 F
-对-G
。打印出 G
、git log
后退一步到 F
,依此类推。
(换句话说,Git 工作 向后 。我不会在这里多强调这一点,但它解释了很多关于 Git,一旦你意识到这一点。)
1如果你使用git log --pretty=fuller
,你会看到每个提交实际上有两个:author和一个 提交者 。每个都由三元组组成:姓名、电子邮件、时间戳。这些天通常两者都是相同的,除了 cherry-picked 提交,其中保留原始提交的作者,提交者是执行 cherry-pick 的人,提交者时间戳是时间cherry-pick 动作。
分支名称只是帮助我们 查找 提交
为了完成上述工作,我们有以某种方式知道链中 last 提交的哈希 ID。我们需要将该哈希 ID 提供给 Git,因为 Git 最终只能 找到 通过其哈希 ID 提交。我们可以写下这些哈希 ID,将它们记在纸上、白板上或其他东西上。但它们又大又丑,而且很难正确输入。另外,我们有一台 计算机 。为什么不让 computer 为我们记住哈希 ID?我们可以将第二个数据库添加到我们的 Git 存储库:它将包含 names,例如 master
或 develop
或 feature
,以及使用这些名称,记住 last(最近的,最有用的,无论如何)提交的哈希 ID。
这就是分支名称:它是名称数据库中的一个条目。实际名称扩展了一点:master
实际上是 refs/heads/master
,feature
实际上是 refs/heads/feature
。这为其他类型的名称留出了空间,例如标签名称:v2.1
实际上是 refs/tags/v2.1
。但特别是对于分支名称,它们都持有 提交哈希 ID——每个一个——并且该哈希 ID 是我们提交的 last 的 ID会考虑“在分支上”。
如果我们只有一个分支,一切都很简单:
...--F--G--H <-- master
在这里,分支名称 master
是唯一的名称,它包含我们最近提交的哈希 ID,提交 H
。所以 name master
指向 链末端的提交。这让我们(和 Git)可以访问提交 H
。提交 H
向后指向提交 G
,这让我们(和 Git)可以访问它; commit G
points backwards again, 依此类推。
如果我们现在创建一个 new 分支名称,例如 feature
,我们可以选择任何现有的提交来使用这个新名称 point-to .不过,大多数情况下,我们会选择我们正在使用的提交:H
,通过 master
。所以我们会得到:
...--F--G--H <-- feature, master
现在我们有问题了。我们使用的是哪个分支名称?为了记住,我们将添加一个特殊名称 HEAD
,并将其附加到这两个分支名称之一。让我们将 HEAD
附加到 feature
——如有必要,通过 运行ning git checkout feature
——然后绘制:
...--F--G--H <-- feature (HEAD), master
我们仍在使用提交 H
,但现在我们使用它是因为名称 feature
。
现在让我们以通常的方式创建一个新的提交:修改一些文件,甚至可能创建新的 and/or 删除现有的,然后使用 git add
and/or git rm
根据需要更新它们,然后 git commit
结果。无需过多担心所有细节,这 Git 保存了一个新快照,添加了一些元数据,并将集合作为新提交写出。新提交获得一个新的、唯一的哈希 ID——某种东西 random-looking,并且不可预测,因为它取决于我们进行提交的 确切时间 ——但我们只需调用它提交 I
。新提交将向后指向现有提交 H
:
I
/
...--F--G--H
一旦新的提交 存在 ,甚至在我们恢复能够 运行 更多命令之前,Git 现在执行它的最后一个特殊技巧:它将新提交的哈希 ID 写入 当前分支名称 ,即 HEAD
是 attached-to。因为那是 feature
,我们得到:
I <-- feature (HEAD)
/
...--F--G--H <-- master
提交 H
就在提交 I
之前,但它仍然是 master
分支上的 last 提交。提交 I
是 feature
上的 最后一个 提交,但 H
之前的提交也在 feature
上。
现在让我们继续在 feature
上再提交一次:
I--J <-- feature (HEAD)
/
...--F--G--H <-- master
然后是运行git checkout master
。这将使我们的 HEAD
远离 feature
并将其附加到 master
。它还将更新我们的工作区,以便我们使用提交 H
的内容,而不是提交 J
的内容:我们所有的文件现在匹配 H
,而不是 J
.我们对 I
和 J
所做的任何更新和快照都安全地存储在那里,在 I
和 J
中,但它们从我们的 视图中消失了 现在,因为我们已经提交 H
out:
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)
就 图本身 而言——提交集与它们之间的 backwards-pointing 箭头(此处绘制为线,因为文本中可用的箭头图形是差)—没关系:我们不能 更改 任何 现有 提交(永远),但我们总是可以添加新的提交,并且无论哪种方式,我们最终都会得到这组提交。这只是 names find 这些提交的问题。但是 Git 允许我们随时创建、销毁或移动 分支名称 。提交不会改变;只是我们用来 find 的 names 可能不同
正在合并
是时候回答上面的问题了:缺少什么?
当我们合并 Git 中的一些提交时,这就是合并工作。这个想法是,某人在某些提交系列(I-J
中)做了一些工作,而另一个人(可能是其他人)在其他一些提交系列中(K-L
)做了一些工作。这给了我们这个:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
由于提交的性质——它们永远不会改变——我们可以从这张图中看出,这两行工作从共同开始点,即提交H
。从视觉上很容易看出 J
中的所有内容都是从 H
继承而来的,L
也是如此。它们也是 G
的后代,但是 H
“更好”,因为它“更接近” end-point 提交。
现在,我们已经知道 Git 可以比较两个快照,如 G
和 H
,或 I
和 J
。如果 Git 可以轻松地将 H
直接与 J
进行比较会怎么样?好吧,它 可以; 如果我们 Git 这样做,我们会发现 与 H
有什么不同到 J
。这是 某人在第一行所做的工作。所以这些是 br1
.
同样,如果我们 Git 将 H
中的内容与 L
中的内容进行比较,我们将在底线上找出某人所做的工作。无论文件有何不同,无论我们使用什么规则将 H
中的文件内容 更改为 L
中的文件内容,这就是有人在 br2
上所做的].
这也告诉我们缺少了什么。为了合并 example.txt
,我们不仅需要两个 end-point 文件——例如,一个在第 2 行说 quite
,另一个在第 2 行说 not
——还有文件的 基本副本 。 example.txt
的基础副本是提交 H
中文件的副本。提交 H
是两个提示提交的 合并基础 ,每个文件的副本是我们找出 更改的内容 的方式。
如果基本副本说:
This is
quite
a file.
然后我们知道 没有任何变化 在仍然说 quite
的那一行,并且在说 not
.[=170 的那一行改变了一行=]
如果基本副本说:
This is
not
a file.
然后我们知道 没有任何变化 在仍然说 not
的那一行,并且在说 quite
.[=170 的那一行改变了一行=]
如果基本副本没有第 2 行——如果它完整地读取:
This is
a file.
然后我们有一个合并冲突,因为两个人都做了一个改变:都添加了一个line-2,但是他们添加了不同的第 2 行。
这对您的情况意味着什么
如果两个分支提示提交 - 一个由名称 master
找到的,一个由名称 feature
找到的 - 不同,这只是告诉我们它们是不同的。 Git 提出的配方,将更改一个提交以使其匹配另一个提交,只是告诉我们如何将一个提示提交更改为另一个提示提交。
如果这两个 branch-tip 提交之间的 merge base 提交是一些 third 提交,2 我们需要知道第三次提交中的内容,因为 那是 git merge
如何弄清楚 master
中发生了什么变化以及 feature
。然后,合并命令将尝试合并那些两组更改,将合并的更改应用于合并基础中的任何内容。
作为phd commented,你可以在git diff
命令中使用triple-dot表示法:
git diff master...feature
例如。这有 Git:
- 找到两个 tip 提交之间的合并基础(我们称之为
$B
);然后 - 运行相当于
git diff $B feature
它告诉您 feature
上关于此合并基础的更改。如果您然后 运行 相同的命令,但两个名称互换:
git diff feature...master
Git 将找到相同的两个 tip 提交的合并基础,3 然后 diff $B
vs master
:这表明你在 master
.
同样,git merge
对这些情况的作用是:4
- 运行 两者 diffs,将输出保存在临时区;
- 获取每个文件的merge base版本;
- 如果可能的话,合并差异;和
- 将 combined 差异应用于文件的合并基础版本。
如果一切顺利,git merge
将从结果中进行合并提交。合并提交与常规 non-merge 提交没有太大区别:它仍然具有所有文件的快照(由上面的合并过程构建)和一些元数据。合并提交的特殊之处在于它列出了 both branch-tip 提交作为其父项,因此 Git 可以 g返回两个分支(现在通过合并提交合并为一个“分支”:这暴露了“分支”一词的缺陷;参见What exactly do we mean by "branch"?)。
2这里有一些退化的情况。特别是,如果合并基础是两个分支提示提交之一,我们要么有一个简单的“fast-forward-able”案例,要么没有要合并的东西。不过,鉴于您发布的内容,您一定没有这些情况之一。
3如果只有一个合并基础提交——通常是这种情况——两个分支尖端提交的列出顺序无关紧要。对于一些复杂的提交图,但是,可能有两个或更多合并基础提交。在这里,画面变得相当模糊。 git diff
命令直到最近才处理得很好; git merge
处理得更好,但仍然很棘手。
4这个描述对你如何进行合并、图形的形状等做出了很多假设,并且在其他方面大大简化了 git merge
内部确实如此。这个想法是为了抓住总体目标,而不是进入一些更棘手的机制。例如,这忽略了合并如何处理重命名文件的情况。