git - Git 是否会考虑时间来避免分支“竞赛”?还是合并顺序很重要?
问题描述
假设中午在分支 A 上创建了一个文件:
A
在 1,您从 A 创建一个分支 B,并将文件更改为:
A
B
C
在2,其他人从A创建C,并将文件更改为:
A
B
C
D
在 3 处,他们将 C 合并到 A 中,所以 A 现在是:
A
B
C
D
在 4 时,有人在 A 上创建了一个分支 D,并将文件更改为:
A
在5,创建分支的原始人将B合并到A中,这意味着A上的文件是:
A
B
C
最后,在 6 处,分支 D 合并到 A 中,留下,作为最终结果(?):
A
我的问题是 Git 知道如何将其视为合并冲突吗?它看时间吗?或者只是具有共同祖先的三向差异,如果它在词汇上“去”它去?
在此示例中,分支 D 正在破坏 B,即使它是在 B 提交之后从 A 创建的(在 B 中,但在合并回 A 之前)。如果是这种情况,如果最后两次提交不是 B->A 然后 D->A,而是相反(首先是 D->A,然后是 B->A),您会得到不同的结果。
解决方案
对于 Git,时间并不重要。只有 生命 图是重要的。 (当然,图表本身是随着时间构建的,所以人们可以争辩说时间在某种程度上确实很重要,但请继续阅读。)
假设中午在分支 A 上创建一个文件 [包含一行阅读
A
]
分支也并不重要,不是你想象的那样。分支是否重要取决于您所说的分支是什么意思(请参阅我们所说的“分支”到底是什么意思?)。请记住,每个提交都由其唯一的哈希 ID 标识,具有:
- 所有源文件的完整快照;
- 作者姓名、电子邮件和时间戳;
- 提交者姓名、电子邮件和时间戳;
- 父提交的哈希 ID;和
- 日志消息。
这是上面的粗体项——提交父项的哈希 ID——在这里很重要,因为它形成了图表。作者和提交者这两个时间戳本质上是任意的:您可以在提交时覆盖一个或两个。(一旦提交,它们就是提交数据的一部分,不能更改。任何提交的任何部分都不能更改:最多,您可以制作该提交的副本,更改某些项目,并获得一个新的提交,具有一个新的和不同的唯一哈希 ID,并说服每个人开始使用新的提交来代替旧的提交。)
让我们给每个提交一个唯一的C<number>
ID(我通常使用大写字母,但您在这里调用了分支、、、A
和,B
所以让我们继续进行提交)。C
D
C1
C7
同时,一个分支名称——请参阅我们所说的“分支”到底是什么意思?再次——只是指向一个特定提交的指针,具有特殊属性,当我们git checkout
分支并进行新提交时,Git 会更新名称以指向我们刚刚进行的新提交。同时,我们刚刚创建的新提交作为其父提交存储了先前提交的哈希 ID。所以,假设我们有一个名为 的分支A
,指向一些现有的 commit C1
,它有一些我们不会引入的父级:
... <-C1 <-- A
分支名称A
包含 的哈希 ID C1
。您git checkout A
因此C1
成为当前提交并A
成为当前分支:
...--C1 <-- A (HEAD)
(将连接器从提交给他们的父母绘制为箭头即将成为一个问题,所以我--
改用这里)。然后,您创建一个新文件,git add
它,并进行新的 commit C2
。这使得名称A
标识 commit C2
,其父级是C1
:
...--C1--C2 <-- A (HEAD)
在 1,您从 A 创建一个分支 B ...
(通过,例如:git checkout -b B A
。)
现在你有:
...--C1--C2 <-- A, B (HEAD)
...并将文件更改为[读取三行
A
B
和C
]
假设你运行git add
and git commit
,你现在有一个新的提交,并B
识别它;新提交的父级是C2
:
...--C1--C2 <-- A
\
C3 <-- B (HEAD)
在 2 时,其他人从 A 中创建了 C ...
(例如,git checkout -b C A
)。所以让我们画出:
...--C1--C2 <-- A, C (HEAD)
\
C3 <-- B
并将文件更改为 [四行,
A
B
C
并且D
,按此顺序]
假设通常的添加和提交,我们得到 commit C4
,C
现在指向:
C4 <-- C (HEAD)
/
...--C1--C2 <-- A
\
C3 <-- B
在 3 处,他们将 C 合并到 A
此时,准确找出他们使用哪些命令来执行此操作变得很重要,因为 commitC4
严格在 commit 之前C2
。这意味着:
git checkout A
git merge C
将进行快进合并,结果是:
C4 <-- A (HEAD), C
/
...--C1--C2
\
C3 <-- B
但是,如果他们使用git merge --no-ff C
,Git 将执行真正的合并(尽管这很简单),结果是:
C4 <-- C
/ \
...--C1--C2----C5 <-- A (HEAD)
\
C3 <-- B
其中新文件的内容(C2
存储在 commitC5
中)与 中的同名文件的内容匹配C4
。通过对一般规则的明显简化,我们稍后将看到一个简单的合并只是采用“图形方式稍后”提交的内容。
无论哪种方式,名称A
都指向文件具有三行形式的提交,但有一种方式A
指向与.不同的提交C
。
在 4,有人从 A 创建了一个分支 D
即,git checkout -b D A
。要绘制它,我们需要知道从两个图表中的哪一个重新开始。不过暂时没关系,所以现在,我将选择具有快进合并的那个:
C4 <-- A, C, D (HEAD)
/
...--C1--C2
\
C3 <-- B
并将文件更改为[再次仅一行
A
]
$ git checkout -b D A
Switched to a new branch 'D'
$ echo A > file
$ git add file
$ git commit -m c7
[D 5724954] c7
1 file changed, 3 deletions(-)
我打电话给这个是C7
为了留出一些空间,跳过了一些提交 ID,因为我们将暂时停止绘制它;但现在我们有:
C7 <-- D (HEAD)
/
C4 <-- A, C
/
...--C1--C2
\
C3 <-- B
5、原来创建分支的人将B合并到A
这一次,无论输入的是哪个提交图结构,都无法进行快进合并。我们得到了一个真正的合并,这不是一个简单的合并。
现在让我们探索实际的合并算法
在这一点上,我将我的演示存储库稍微倒带并重做快进合并,以便回到第一种情况:
$ git checkout A
Switched to branch 'A'
$ git reset --hard 6626cd2
HEAD is now at 6626cd2 c2
$ git merge C
Updating 6626cd2..7af3a02
Fast-forward
file | 3 +++
1 file changed, 3 insertions(+)
$ git log --all --decorate --oneline --graph
* 5724954 (D) c7
* 7af3a02 (HEAD -> A, C) c4
| * 5915b1d (B) c3
|/
* 6626cd2 c2
* 80e22c8 (master) initial
或以我喜欢的格式:
C7 <-- D
/
C4 <-- A (HEAD), C
/
...--C1--C2
\
C3 <-- B
我们C4
在索引和工作树中有提交,并且我们HEAD
附加到 name A
。我们现在运行git merge B
或git merge <hash of C3>
- 哪个并不重要。Git 查看此图以找到最低公共祖先节点,在这种情况下显然是 commit C2
。
合并算法现在实际上运行两个git diff
命令:
git diff --find-renames <hash-of-C2> <hash-of-C4> # what we changed
git diff --find-renames <hash-of-C2> <hash-of-C3> # what they changed
假设“我们”( C2
-vs- C4
) 只更改了一个文件,则此处发现的更改将是:在文件末尾添加三行 , 。B
C
D
同样,为“他们的”工作找到的更改是在文件末尾添加两行 ,B
和。C
Git 的工作就是将这两个变化结合起来。但它们有冲突:不可能只添加两行,同时添加三行。所以 Git 因合并冲突而停止:
$ git checkout A
Already on 'A'
$ git merge B
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.
因此,您的下一个陈述是一个问题:
意味着在 A 上的文件是:
A B C
因为正如我们所见,在我们修复合并冲突之前,这不是文件中的内容:
$ cat file
A
<<<<<<< HEAD
B
C
D
||||||| merged common ancestors
=======
B
C
>>>>>>> B
(我已merge.conflictStyle
设置为diff3
,在此处生成该||||||| merged common ancestors
部分,尽管在这种情况下它仍然是空的)。
当然,进行合并的人可以按照您建议的方式设置内容,或者使用-X ours
或-X theirs
选择两个更改之一优先。但是默认情况下是合并冲突,在这种情况下,无论正确的定义在您的情况下如何,都取决于您(人类)来正确解决它。我们A
B
C
在这里选择:
$ git checkout --theirs file
$ cat file
A
B
C
$ git add file
$ git commit -m c5
[A eec968d] c5
该图现在看起来像这样:
C4 <-- C
/ \
...--C1--C2 C5 <-- A (HEAD)
\ /
C3 <-- B
而file
in commit的内容C5
就是我们选择的。
如果我们强制进行微不足道的合并怎么办?为什么微不足道的合并如此微不足道?
我们可以再次重置我们的条件并回到我们允许A
快进之前:
$ git reset --hard HEAD~2
HEAD is now at 6626cd2 c2
$ git log --all --decorate --oneline --graph
* 5724954 (D) c7
* 7af3a02 (C) c4
| * 5915b1d (B) c3
|/
* 6626cd2 (HEAD -> A) c2
* 80e22c8 (master) initial
以我的方式重绘这张图,我们回到:
C7 <-- D
/
C4 <-- C
/
...--C1--C2 <-- A (HEAD)
\
C3 <-- B
让我们利用git merge --no-ff C
这段时间,探索一下常用算法的作用。我们找到两个提示提交,C2
和C4
,并找到它们的合并基础——最不共同的祖先,即C2
。然后我们做两个差异:
git diff --find-renames C2 C2 # what we changed (nothing!)
git diff --find-renames C2 C4 # what they changed
然后,我们将“无”与他们改变的任何东西结合起来。这种结合的结果当然只是它们的变化;这些应用于C2
,结果是一个提交,其内容与以下内容匹配C4
:
$ git merge --no-ff C -m c5
Merge made by the 'recursive' strategy.
file | 3 +++
1 file changed, 3 insertions(+)
$ git log --all --decorate --oneline --graph
* b47cf02 (HEAD -> A) c5
|\
| | * 5724954 (D) c7
| |/
| * 7af3a02 (C) c4
|/
| * 5915b1d (B) c3
|/
* 6626cd2 c2
* 80e22c8 (master) initial
注意C4
和C5
匹配的内容:
$ git diff 7af3a02 b47cf02
$
并且图表现在正如预期的那样(尽管 Git 的绘图很难阅读):
C7 <-- D
/
C4 <-- C
/ \
...--C1--C2----C5 <-- A (HEAD)
\
C3 <-- B
如果我们现在运行git merge B
,我们必然要求真正的合并——没有办法将名称A
“向前”滑动到C3
——但再次出现冲突:
$ git merge B
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.
$ cat file
$ cat file
A
<<<<<<< HEAD
B
C
D
||||||| merged common ancestors
=======
B
C
>>>>>>> B
合并冲突与之前的冲突相同,因为三个输入内容也相同。
让我们再次按照您建议的方式解决这个问题,使用三行--theirs
版本(我将使用一个不需要git add
它的快捷方式作弊):
$ git checkout MERGE_HEAD -- file
$ git commit -m c6
[A 2e66e76] c6
$ cat file
A
B
C
(“作弊”是git checkout MERGE_HEAD
从 commit 中提取文件C3
,指向哪个B
点,而不是从索引槽 3 中提取它。这会清除三个冲突的索引槽条目,用已解决的零槽条目替换它们,这样我们就是准备提交结果。)
现在我们有这个图表:
C7 <-- D
/
C4 <-- C
/ \
...--C1--C2----C5--C6 <-- A (HEAD)
\ /
\ /
\ /
C3 <-- B
回到正在使用的命令
最后,在 6 点,分支 D 被合并到 A ...
要做到这一点,我们必须HEAD
附加到A
——它已经是——并且我们运行git merge D
or git merge <hash of C7>
。让我们通过找到提交的合并基础来预测会发生什么,C6
并C7
沿着图形连接向后追溯到最佳(“最低”)共同祖先。这一次,是 commit C4
。file
里面又是什么C4
?C
让我们使用名称指向它的事实来查看它:
$ git show C:file
A
B
C
D
$
因此,Git 会将 的内容与<hash-of-C4>:file
的内容进行比较<hash-of-C6>:file
,以查看我们更改了什么——我们不会为重命名检测或其他文件而烦恼,因为没有要检测的重命名,也没有对其他文件的更改:
$ git diff C:file A:file
diff --git a/file b/file
index 8422d40..b1e6722 100644
--- a/file
+++ b/file
@@ -1,4 +1,3 @@
A
B
C
-D
所以我们改变的是删除 final D
。
另外,Git 会将 的内容与<hash-of-C4>:file
的内容进行比较<hash-of-C7>:file
,以查看它们发生了什么变化,因此:
$ git diff C:file D:file
$ git diff C:file D:file
diff --git a/file b/file
index 8422d40..f70f10e 100644
--- a/file
+++ b/file
@@ -1,4 +1 @@
A
-B
-C
-D
他们删除了三行。这些更改应该是冲突的。让我们看看我们是否正确:
$ git merge D
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.
$ cat file
A
<<<<<<< HEAD
B
C
||||||| merged common ancestors
B
C
D
=======
>>>>>>> D
我们确实是正确的:共同祖先(保存在工作树中,因为我有diff3
我的冲突风格设置)有三行,而HEAD
版本保留其中两行,但他们的 ( D
) 版本删除了所有三行。
(我们可以对由快进产生的稍微简单的图进行相同的练习,但结果是相同的:我们在同一组线上遇到了合并冲突。关键是找到合并基础和两个尖端提交,并将基数与每个分支尖端进行比较。在这种情况下,最终合并的输入具有以下图表:
C7 <-- D
/
C4 <-- C
/ \
...--C1--C2 C5 <-- A (HEAD)
\ /
C3 <-- B
where与更复杂的图中C5
具有相同的手动构建内容,并且和是相同的,合并基础仍然是.)C6
C4
C7
C4
递归合并
在对iBug 的回答的评论中,您询问了递归合并。当有多个最低共同祖先时,就会发生这些情况。在简单的树数据结构中,只有一个 LCA,但在有向图中,可能不止一个。参见Michael A Bender、Martín Farach-Colton、Giridhar Pemmasani、Steven Skiena 和 Pavel Sumazin。树和有向无环图中的最低共同祖先。Journal of Algorithms, 57(2):75–94, 2005.两个正式定义;但通常,当您进行“纵横交错”合并时,这些会发生在图形导向的版本控制系统(例如 Git 和 Mercurial)中。例如,假设我们从这张图开始:
o--A <-- branch1
/
...--o--*
\
o--B <-- branch2
我们现在git checkout branch1 && git merge branch2
,然后当成功时,我们有:
o--A---M1 <-- branch1
/ /
...--o--* /
\ /
o--B <-- branch2
我们立即运行git checkout branch2 && git merge branch1~1
(或等效的,例如git merge <hash of commit A>
)来生成:
o--A---M1 <-- branch1
/ \ /
...--o--* X
\ / \
o--B---M2 <-- branch2
如果我们现在在两个分支上进行更多提交,我们可能会有,例如:
o--A---M1--C <-- branch1
/ \ /
...--o--* X
\ / \
o--B---M2--D <-- branch2
我们现在要问:哪个提交是或者是两个分支的提示的最低共同祖先,提交C
和D
?从 开始C
并向后工作,我们发现 commit B
through M1
,它可以从D
through到达M2
,所以它是一个 LCA。但是我们也可以在直接路径上找到提交,并且A
可以D
通过.M2
使用他们的任何一个定义,或者我喜欢的更简单的定义,包括计算跳数(但是当有很多输入时,这不像诱导子图方法那样有效),我们发现提交A
和B
都是提交的LCAC
和D
. 这就是 Git-s resolve
和-s recursive
策略不同的地方。
在 下-s resolve
,Git 简单地(显然)随机选择一个祖先,并将其用作两个差异的合并基础。在 下-s recursive
,Git 找到所有LCA 并将它们全部用作合并基础。为此,Git 会合并所有 LCA,就好像你已经运行过一样git merge <lca1> <lca2>
,然后——如果有更多 LCA——<code>git merge <resulting commit> <lca3>,以此类推。即使存在冲突,也会进行每次提交:Git 只是简单地获取冲突的合并,加上冲突标记,然后从中进行提交。
(合并任何一对 LCA 本身可能需要合并多个 LCA。如果是这样,Git 会递归地合并它们:因此得名。)
最终提交是临时的,因为它没有名称来保留它,它被用作比较分支提示的合并基础。当内部合并有冲突时,这会产生非常混乱的结果和/或合并冲突。不过,在大多数情况下,它运作良好。它给出的结果与大多数情况相似-s resolve
,而对于结果不同的少数情况,它们往往更好。
推荐阅读
- java - 从给定的字符串数组创建多个数组
- c# - 意外的 String.Compare() 导致 .NET 5
- c++ - 在 C++ 中使用模板传递具有 N 个元素的数组
- python - 为什么所有的测试用例都没有通过?回文程序
- javascript - 如何“强制”Rails 读取 JS 文件,以便函数可用?
- poloniex - Poloniex websocket API 不断断开连接
- angular - Angular httpClient 无法读取未定义的属性未定义
- nginx - Webmin 保存配置文件失败:配置无效:nginx:
- flutter - 每当我运行我的 Flutter 应用程序时,我都会收到一个 null 错误
- reactjs - 如何在反应日历中格式化日期?