git:重新认识Merging

merger允许将两个(或多个)提交合并成一个单独的合并提交,同时将这两个提交中合并的所有不同文件合并在一起。如果存在合并冲突,需要逐个解决。其中一个提交是当前提交的远端的合并称为fast-forward merges ,不会创建合并提交。当你从远程源pull时,实际上是在一步中进行了fetch merge - 尽管通常情况下,你可以在进行fast-forward merges

为了本文的目的,我将创建三个分支,分别称为“Alex”“Bob”“master”,以记录Alice和Bob。每个分支都将有一个名为Hello的文件,其中包含文本“Hello, my name is Alex”,以及一个AlexsFile、BobsFile等。

1
2
3
4
5
6
7
8
(master) $ git ls-tree Alex
100644 blob ada4c4c4f33cd190fe40769d5ca9826adb9fb7ce AlexsFile
100644 blob ca4eef2f4e3f1fe92028176cb547b590a08c2259 Hello
(master) $ git ls-tree Bob
100644 blob eea826732acee08a8cf83445e3b98cf58f11ce5c BobsFile
100644 blob ca4eef2f4e3f1fe92028176cb547b590a08c2259 Hello
(master) $ git ls-tree master
100644 blob ca4eef2f4e3f1fe92028176cb547b590a08c2259 Hello

合并策略

当你创建一个合并时,你可以选择说明如何处理合并。通常的策略是递归。这意味着 Git 将遍历每个目录(树)并找出哪些文件与基本修订版本相比有差异,然后使用有变化的文件。(如果两者都有变化,新文件的内容将被在文本上合并,如果这有问题,就会发生冲突。)这就是为什么在执行操作后会看到消息 “Merge made by recursive” 的原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(master) $ git checkout Alex
Switched to branch 'Alex'
(Alex) $ git merge Bob
Merge made by recursive.
BobsFile | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 BobsFile
(Alex) $ git log --oneline --graph
* d612aab Merge branch 'Bob' into Alex
|\
| * 8afb2d3 Bob's File
* | 4abf59e Alex's File
|/
* 5cba624 Hello
(Alex) $ git show d612aa
commit d612aab9858289ed027230d3b9a7b2a7a5e75945
Merge: 4abf59e 8afb2d3

Merge branch 'Bob' into Alex

提交 d612aa… 是一个合并节点,因为它将两个不同的分支合并在一起。由于两者都不是对方的祖先,它们无法进行快进合并,因此创建了合并节点。我们可以使用HEAD^1HEAD^2 来确定提交的父级:

1
2
3
4
(Alex) $ git rev-parse HEAD^1
4abf59ef73c186e93db25e8b7bc4423fbd11bbd0
(Alex) $ git rev-parse HEAD^2
8afb2d368ce26ca71cec539c31400c7001a18efc

当没有需要合并的更改时,这甚至更容易:

1
2
3
4
5
6
7
8
(Alex) $ git checkout master
Switched to branch 'master'
(master) $ git merge Bob
Updating 5cba624..8afb2d3
Fast-forward
BobsFile | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 BobsFile

这里的fast-forward merges 表示master落后于 Bob,因此我们可以简单地将指针向前移动 - 结果就是,我们不需要创建一个合并节点。

所以,Git 使用的两种策略是fast-forward merges 或递归合并。这涵盖了你需要执行的 99% 的合并操作;但值得注意的是,Git 还有一些在特定情况下可以提供帮助的技巧。

Octopus merge

上面的示例都只有一个或两个父节点。然而,Git 合并节点能够表示多于两个父节点,并且它使用一种称为Octopus merge的策略。当你合并超过两个分支时,默认会选择这种策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
(master) $ git reset --hard 5cba624b94a7a622183c960697867c8bba73aa91
HEAD is now at 5cba624 Hello
(master) $ date > NewFile
(master) $ git add NewFile
(master) $ git commit -m "New File"
[master 598ad85] New File
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 NewFile
(master) $ git merge Alex Bob
Trying simple merge with Alex
Trying simple merge with Bob
Merge made by octopus.
AlexsFile | 1 +
BobsFile | 1 +
2 files changed, 2 insertions(+), 0 deletions(-)
create mode 100644 AlexsFile
create mode 100644 BobsFile
(master) $ git log --oneline --graph
*-. 5a2aa0d Merge branches 'Alex' and 'Bob'
|\ \
| | * 8afb2d3 Bob's File
| * | 4abf59e Alex's File
| |/
* | 598ad85 New File
|/
* 5cba624 Hello
(master) $ git show
commit 5a2aa0da3d3b3365703d710dad8aeebc0770b8ef
Merge: 598ad85 4abf59e 8afb2d3

Merge branches 'Alex' and 'Bob'
(master) $ git rev-parse HEAD^1 HEAD^2 HEAD^3
598ad850114e1f7445ee8b02e93ee23060439560
4abf59ef73c186e93db25e8b7bc4423fbd11bbd0
8afb2d368ce26ca71cec539c31400c7001a18efc

在这种情况下,我们有三个提交合并到一个合并节点中;master(已分叉的分支)、之前的 Alex 和 Bob 分支。由于我们的合并节点现在有三个父节点,我们可以使用 git rev-parseHEAD^1/2/3 转换为第一个、第二个和第三个父节点。

如果文件存在冲突怎么办呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(master) $ git checkout Alex
Switched to branch 'Alex'
(Alex) $ echo Hello, my name is Alex > Hello
(Alex) $ git commit -a -m "My name is Alex"
[Alex c2cb955] My name is Alex
1 files changed, 1 insertions(+), 1 deletions(-)
(Alex) $ git checkout Bob
Switched to branch 'Bob'
(Bob) $ echo Hello, my name is Bob > Hello
(Bob) $ git commit -a -m "My name is Bob"
[Bob 7cb6225] My name is Bob
1 files changed, 1 insertions(+), 1 deletions(-)
(Bob) $ git checkout master
(master) $ git merge Alex Bob
Trying simple merge with Alex
Trying simple merge with Bob
Simple merge did not work, trying automatic merge.
Auto-merging Hello
ERROR: content conflict in Hello
fatal: merge program failed
Automatic merge failed; fix conflicts and then commit the result.
(master|MERGING) $ git ls-files --stage
100644 ada4c4c4f33cd190fe40769d5ca9826adb9fb7ce 0 AlexsFile
100644 eea826732acee08a8cf83445e3b98cf58f11ce5c 0 BobsFile
100644 ca4eef2f4e3f1fe92028176cb547b590a08c2259 1 Hello
100644 a5a820416bae2c7b77340e5b2120aab9595d2bfc 2 Hello
100644 98b16693fe64acb9d002af1fe5f5162d58bd40b4 3 Hello
100644 09d774502f97ba9a46f25f8f11601b653c376828 0 NewFile
(master|MERGING) $

我们可以使用 git mergetool 启动一个three-way diff

1
2
3
4
5
6
7
8
9
10
11
12
13
(master|MERGING) $ git mergetool
(master|MERGING) $ git mergetool
merge tool candidates: opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse ecmerge p4merge araxis bc3 emerge vimdiff
Merging:
Hello

Normal merge conflict for 'Hello':
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (opendiff):

(master|MERGING) $ git commit -a -m "Merged"
[master 999a938] Merged

请注意,Octopus merge不能处理超过两个文件的冲突。如果有超过两个文件的冲突,你会得到一个不同的错误消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
(master) $ echo Hello World > Hello
(master) $ git commit -a -m "Hello World"
[master fe79e59] Hello World
1 files changed, 1 insertions(+), 1 deletions(-)
(master) $ git merge Alex Bob
Trying simple merge with Alex
Simple merge did not work, trying automatic merge.
Auto-merging Hello
ERROR: content conflict in Hello
fatal: merge program failed
Automated merge did not work.
Should not be doing an Octopus.
Merge with strategy octopus failed.

ours

ours strategy可以验证它们是相同的,因为由 HEAD 指向的树与由 HEAD^1(即父节点)指向的树相同。后缀^{tree}用于显示与提交相关联的树。

--decorate 参数添加了分支名称到 git log 的输出中,这对于显示合并来自哪里是有用的。--graph 参数在大多数情况下与 --oneline 参数一起使用;虽然你可以运行 git --graph,但完整的提交消息往往会隐藏图的结构。

ours 策略只在你想要编码一组先前的提交但不希望它们影响当前 master 分支时才真正有用(例如,因为你已经挑选了一些内容,不想采纳其他部分,但又想以某种方式保留它们的历史记录)。

Merge Message and Fast Forwards

Fast Forwards将根据你正在合并的分支的名称自动生成。然而,可以通过传递 -m 选项(与 git commit 一样)来提供额外的消息。如果合并消息需要编码附加信息(比如修复了哪个或哪些 bug),这可能会很有用。

还可以强制执行合并,即使没有必要进行合并。如果你有基于主题的分支,将工作在一个单独的分支上进行再合并回 master 分支时可能会很有用。运行 git merge --no-ff 将创建一个合并节点,无论是否可以快进合并。由于合并提交中包含了你正在合并的分支名称作为提交的一部分,因此你最终可以得到描述性的名称来显示已完成的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(master) $ git checkout -b "bug12345"
Switched to a new branch 'bug12345'
(bug12345) $ echo BugFix >> Hello
(bug12345) $ git commit -a -m "Fixing bug 12345"
[bug12345 e2bd64e] Fixing bug 12345
1 files changed, 2 insertions(+), 0 deletions(-)
(bug12345) $ git checkout master
Switched to branch 'master'
(master) $ git merge bug12345 # without --no-ff
Updating 999a938..e2bd64e
Fast-forward
Hello | 2 ++
1 files changed, 2 insertions(+), 0 deletions(-)
(master) $ git reset --hard HEAD^
HEAD is now at 999a938 Merged
(master) $ git merge --no-ff bug12345 # with --no-ff
Merge made by recursive.
Hello | 2 ++
1 files changed, 2 insertions(+), 0 deletions(-)
(master) $ git log --oneline --graph --decorate
* 4a905c8 (HEAD, master) Merge branch 'bug12345'
|\
| * e2bd64e (bug12345) Fixing bug 12345
|/
*-. 999a938 Merged

在使用某些提交工作流时,能够使用 --no-ff 进行合并可能是有用的,其中使用许多分支来开发单个功能,然后随后合并到主分支。还值得注意的是 --merges 参数允许你仅在仓库中过滤合并提交:

1
2
3
(master) apple[merge] $ git log --oneline --merges --decorate
4a905c8 (HEAD, master) Merge branch 'bug12345'
999a938 Merged

后面我们将探讨使用 merge --no-ff进行不同 Git 工作流的各种操作。