As 阿米尔回答了 https://stackoverflow.com/a/59532577/1256452(正确),你会遇到合并冲突one的两个合并。您将必须采取一些措施来处理合并冲突。做什么取决于你。不过值得指出的是,why你会得到one合并冲突,不是两个,以及为什么会发生合并冲突。
Git 的合并其实并不是branches。是关于commits。 Git 的大部分实际上都是关于提交的,并且git merge
这里没有什么不同。
让我们在这里注意一下提交是什么以及它的作用。每个提交都有两部分:data—所有文件的已保存快照及其metadata,或有关提交的信息。
-
保存的快照非常简单:如果您克隆存储库并使用--no-checkout
,你有一个空的工作树(没有可编辑或可用的副本any文件,尚未)。然后你选择一些提交(任何地方的任何提交)并告诉 Git 检查该特定提交(可能是通过其原始哈希 ID)。现在,您拥有了所有文件的副本,无论是谁进行了该提交,它们的外观都是一样的。
一般来说,这就是git checkout
或 Git-2.23 中的新功能git switch
命令的用途是:你选择一些提交并说获取该提交中的所有文件。他们进入你的工作树 or 工作树您可以在其中看到它们并对其进行处理。您还可以将其他未 Git 化的文件放入您的工作树中,例如编译文件或输出或其他文件。这些文件仍然存在未追踪的(我不会在这里详细讨论,但它们不在 Git 中,它们只是位于您的工作树中,除非/直到you删除它们)。
-
The metadata提交中记录了诸如是谁制作的信息——他们的姓名和电子邮件地址——以及他们制作的时间。这就是你看到的东西git log
output.
不过,元数据的一部分是专门针对 Git 本身的。每个提交都有一个唯一的哈希 ID,每个 Git 都同意这一点:一个哈希 ID 用于that提交,并且绝不用于任何其他提交。因此,很容易判断您是否有一些提交:您只需将其哈希 ID 提供给 Git,并且您的 Git 要么拥有它,在这种情况下,它就有that提交,或者你的 Git 没有它,在这种情况下你必须找到其他有它的 Git 帽子。
无论如何,每个提交都会将其前一个或另一个提交的哈希 ID 存储为元数据的一部分。parent犯罪。大多数提交只有一个。合并提交有点特殊,因为它们有two父母(或更多,但大多数只有两个)。
这些父提交(或合并时的父提交)是 Git 存储历史记录的方式。每一个快照只是一个快照,但每一个快照也都在说:我之前的快照是______(用哈希 ID 填写空白)。对于合并,这是first父母。对于常规提交,它是唯一的父级(因此也是第一个父级)。因此,通过返回每个提交的第一个父级,Git 可以追溯到一段时间内发生的情况。放两个快照:左边是旧快照,右边是新快照,然后比较它们。有什么不同? https://en.wikipedia.org/wiki/Spot_the_difference这种差异告诉你发生了什么:旧的和新的之间发生了什么变化。
一旦您了解了有关提交的信息,我们只需要添加一件事即可使分支正常工作。在 Git 中,一个分店名称记录的哈希IDlatest我们希望将其称为“分支的一部分”。主要就是这样——这就是分支名称对我们和 Git 的作用。它记录了last犯罪。提交本身记录了历史。
因此,给定一系列提交,在一个只有三个提交和一个分支名称的非常小的存储库中,我们有,例如:
A <-B <-C <--master
The last提交是C
。我们有名字master
来存储它的实际哈希 ID——这实际上是一些大而丑陋的随机字母和数字字符串,我们永远无法猜到。犯罪C
本身存储了早期提交的哈希IDB
,使得C指着 B
;并承诺B
存储早期提交的哈希 IDA
.
Commit A
很特别:它根本不指向回,因为这是第一次提交,并且不能指向更早的提交。这就是 Git 知道的方式stop回去:当它不能时。
给定一个 Git 存储库,我们可以进入并查找所有提交,并查看哪些是最后提交,但拥有一个可以快速找到它们的名称会更快。当我们开始拥有多个分支机构时,这一点也变得很重要。让我们从一个大约有八次提交的小型存储库开始:
...--G--H <-- master
现在让我们添加一个新分支。我们将从新的开始name also选择提交H
。我们需要一种方法来了解我们正在使用哪个分支,所以我们将附加特殊名称HEAD
分支名称之一:
...--G--H <-- master, feature1 (HEAD)
现在我们将添加一个新的提交,它会获取一些新的随机哈希 ID,我们将调用它I
:
I <-- feature1 (HEAD)
/
...--G--H <-- master
当我们添加一个new提交,Git自动更新分支名称指向新的提交。哪个分支名称会更新?唯一的那个HEAD
被附加到。其他人都留在原地。
现在所有提交都通过H
are on both分支,并提交I
是*仅在feature1
。让我们再次提交,然后创建一个新分支feature2
选择提交H
,然后开始使用那个:
I--J <-- feature1
/
...--G--H <-- master, feature2 (HEAD)
现在让我们添加两个提交feature2
:
I--J <-- feature1
/
...--G--H <-- master
\
K--L <-- feature2 (HEAD)
现在,假设在提交中I
or J
,我们创建了一个new file test1
,尚未提交H
。假设在提交中K
or L
, we also创建了一个名为的新文件test1
.
Merging
We're now going to merge the two features into master
, one at a time. For no obvious reason,1 we will use the --no-ff
option:
git checkout master
git merge --no-ff feature1
为了达成这个。
When we git checkout master
,我们将 Git 定向到:
- 提取由名称标识的提交
master
—commit H
——到我们的工作树(以及 Git 的index,我们不会在这里讨论);和
- 设置我们的工作树来匹配,这意味着removing文件
test1
,这是在提交中L
— 有一个已保存的具有该名称的快照文件 — 但不在提交中H
.
所以,现在我们有:
I--J <-- feature1
/
...--G--H <-- master (HEAD)
\
K--L <-- feature2
我们准备好了git merge --no-ff feature1
.
Git 现在发现三个提交,不只是两个。感兴趣的三个提交是:
-
我们目前的承诺,HEAD
。这真的很容易找到,因为HEAD
附加到分支名称并且分支名称指向提交,因此Git找到提交H
.
-
我们命名的另一个提交。这也很简单:我们说过要合并feature1
。名字feature1
识别提交J
。 (看图就知道了!)
-
The merge base. The merge base is defined by the commit graph, formed by the inter-connections from one commit to another. While we won't go into all the details, you can think of this as the best shared commit, i.e., the best commit that's on both branches. Starting from J
—as found by name feature1
—we work backwards; and starting from H
, as found by master
, we also work backwards. When some commit is on both branches, that's a shared commit. The newest such commit—with newest not being properly defined here, but in most cases it's obvious—is usually the best commit.2
在这种情况下,合并基础显然是提交的H
itself.
1The merge I'll do here is the kind you would get on GitHub, using its "merge pull request" button. From the Git command line, you get more options. The --no-ff
option forces command-line Git to make a real merge, instead of using its short-cut "fast forward not-really-a-merge" option.
2Technically, what Git is doing is finding the Lowest Common Ancestor (LCA) in a directed graph. In a tree, there is always one well-defined LCA, but Git's commit graph is not necessarily a single tree: it's just a Directed Acyclic Graph or DAG. Two commits may have no LCA, or may have more than one LCA, and merge does different things for these cases.
合并,第 2 部分
找到合并基础后,Git 现在运行two of its 比较两个提交并查看发生了什么变化运营。比较 #1 将合并基础与--ours
承诺,即HEAD
。所以 Git 会这样做:
git diff --find-renames <hash-of-H> <hash-of-H> # what we changed on master
显然,承诺H
与提交相同H
。一切都没有改变!
然后,Git 进行第二次比较,看看“他们”(我们)在另一侧更改了什么:
git diff --find-renames <hash-of-H> <hash-of-J> # what they changed on feature1
那么 merge 的作用是combine这两组变化。当我们更改了某些文件而他们没有更改时,Git 会接受我们的更改。当他们更改了某些文件而我们没有更改时,Git 会接受他们的更改。这些组合的更改将应用于合并基础快照。这样,我们就可以保留我们所有的工作并添加他们的工作,但无论我们和他们做了什么不同的更改某些文件或多个文件,Git 会显示合并冲突.
在这种情况下,--ours
diff 完全是空的:我们没有改变任何东西。所以无论“他们”是什么——实际上,我们feature1
—did,Git 接受这些更改。这包括添加新文件test1
。这种合并进展顺利,因此 Git 自行进行了新的合并提交。
The 第一个父母新的合并提交是我们当前的提交,H
, on master
。新合并提交的第二个父级是他们的提交J
, on feature1
。我们可以画出这一点——这里的图画并没有正确显示第一次提交和第二次提交,但如果需要的话我们可以记住它,或者向 Git 询问父母双方的情况,看看哪一个是第一个,或者其他什么。
结果如下:
I--J <-- feature1
/ \
...--G--H------M <-- master (HEAD)
\
K--L <-- feature2
注意如何不other分支名称已更改:我们仍在master
,并且它已经移动到指向M
, and feature1
仍然命名提交J
and feature2
仍然命名提交L
.
冲突的合并
如果我们现在运行另一个git merge
——这次与feature2
—Git 将再次定位三个提交:
- 我们的和他们的承诺都是承诺
M
and L
, 当然。
- 合并基础是最好的shared commit.
看图。两者都有哪些提交master
and feature2
?提交G-H-I-J-M
都在master
—H
in two方式,直接从第一个父母M
,并间接地从J
to I
to H
通过第二个父母M
-因此G
是否有两种方式存在,等等,但我们真正关心的是H
and G
在那儿。
同时,feature2
结束于L
,回到K
,然后返回到H
。所以承诺H
and G
两者都是共享的。犯罪H
is the best不过,有一个。那么,合并基础再次提交H
.
Git 将再次运行两个git diff
s,都与--find-renames
(检查重命名的文件)并且都来自H
到两个分支尖端。所以 Git 将比较以下快照H
反对其中的一个M
,看看我们改变了什么。
What did我们改变,从H
to M
?嗯,在M
,我们添加了通过比较得到的所有变化H
vs J
。所以我们的任何文件changed in feature1
改变于M
。但我们还添加了一个新文件,test1
,在任一I
or J
,所以这个变更集说添加全新文件test1
.
当我们比较时H
vs L
,这也说添加一个全新的文件test1
. So 两个变更集都说要添加新文件.
Git 将这种冲突称为添加/添加冲突.在工作树中,Git 只是将两个文件的全部内容作为冲突留给您。你必须以某种方式解决这个冲突。你如何去做取决于你。无论您选择放入文件中的内容test1
,您现在可以运行:
git add test1
Git 会假设文件中的内容test1
is the correct该冲突的解决方案。
请务必编辑该文件!如果不这样做,它就只有冲突标记,Git 认为这就是正确答案!可能不是。
一旦解决了所有冲突,并确保合并结果正确(例如,您已经完成了需要做的任何测试),您可以通过运行以下任一命令来安全地完成合并:
git merge --continue
or:
git commit
(The git merge --continue
只是确保您仍在完成合并,然后运行git commit
对于你来说,所以他们最终会做同样的事情——除非你已经完成或终止了合并,也就是说。)
Git 现在将制作another合并提交;我们称之为提交N
,并像这样画它:
I--J <-- feature1
/ \
...--G--H------M--N <-- master (HEAD)
\ /
K-----L <-- feature2
第一个父母是N
is M
,以及第二个父母N
is L
。现在有three方式从N
to H
, and all图中的提交已开启master
.
现在可以安全删除这些名字了feature1
and feature2
因为 Git 可以找到这些提交——包括J
and L
——从提交向后退N
。你不have删除名称,如果您想保留查找提交的能力J
and L
直接、快速,但他们不再必要的,就像合并操作之前一样。