之前提到了分支,既然有分,就一定有合。
在Git中,当一个分支中的修改与另一个分支中的修改不发生冲突的时候,Git会计算合并结果,并创建一个新提交来代表新的统一状态。但是当分支冲突时,Git并不解决冲突,而将该文件在索引中标记为未合并的(unmerged),交给用户处理,用户处理完冲突后,才能够进行最终提交。
首先执行下列代码初始化版本库:
git init
echo abc > file1
git add file1
git commit -m "commit file1"
echo abcd > file2
git add file2
git commit -m "commit file2"
git branch other HEAD^
git checkout other
git branch
echo abcde > file3
git add file3
git commit -m "commit file3"
echo abcdef > file4
git add file4
git commit -m "commit file4"
git checkout master
git branch
在上面的代码中,在master分支外创建了other分支,并进行了两次提交,然后如果想要将other分支合并到master分支上的话,就需要使用到git merge命令。
git merge操作区分上下文,当前分支始终是目标分支,其它一个或多个分支始终合并到当前分支,因此此时需要检出mster分支,然后执行:
$ git merge other
Merge made by the 'recursive' strategy.
file3 | 1 +
file4 | 1 +
2 files changed, 2 insertions(+)
create mode 100644 file3
create mode 100644 file4
此时可以看下发生了什么:
$ git log --graph --pretty=oneline --abbrev-commit
* c434f5c (HEAD -> master) Merge branch 'other'
|\
| * 9a52c7a (other) commit file4
| * ed1d141 commit file3
* | a2c5b9d commit file2
|/
* e3dea4c commit file1
从上面可以看出,git merge会产生一个合并后的提交并添加到当前分支中,而另一个分支不受影响。
而如果在分支other上新建file2,那么此时的合并操作为:
$ git checkout other
git add file2
git commit -m "commit file2 again"
git checkout master
Switched to branch 'other'
$ echo 1111 > file2
$ git add file2
warning: LF will be replaced by CRLF in file2.
The file will have its original line endings in your working directory
$ git commit -m "commit file2 again"
[other 7105a3a] commit file2 again
1 file changed, 1 insertion(+)
create mode 100644 file2
$ git checkout master
Switched to branch 'master'
$ git merge other
CONFLICT (add/add): Merge conflict in file2
Auto-merging file2
Automatic merge failed; fix conflicts and then commit the result.
此时提示冲突,而file2文件的内容就改变为:
$ cat file2
<<<<<<< HEAD
abcd
=======
1111
>>>>>>> other
=======之前的部分表示活动分支该文件的内容,之后的部分表示other分支该文件的内容。用户可以自己抉择如何修改。
若选择留下other分支的内容,就可以删除掉其它内容:
$ cat file2
1111
此时可以使用git add将该文件暂存,然后使用git commit来提交合并。
$ git add file2
$ git commit -m "merge other to master"
[master 741c781] merge other to master
$ git log --graph --pretty=oneline --abbrev-commit
* 741c781 (HEAD -> master) merge other to master
|\
| * 7105a3a (other) commit file2 again
| * 854da83 commit file4
| * 54bd89c commit file3
* | 4e6d232 commit file2
|/
* 32ae382 commit file1
这样也就解决了冲突,并完成了合并。
上面的例子中提供了冲突的例子,但实际开发中可能有多个冲突文件,冲突的部分可能也有很多,此时就需要进行其它处理。
Git可以对有问题的文件进行跟踪,并在索引中将之标记为冲突的(conflicted)或者未合并的(unmerged),因此可以使用git status来显示:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Changes to be committed:
new file: file3
new file: file4
Unmerged paths:
(use "git add ..." to mark resolution)
both added: file2
这里便将file2标记为unmerged,说明该文件是存在冲突的。
也可以使用git ls-files -u命令来显示工作目录中未合并的一组文件:
$ git ls-files -u
100644 acbe86c7c89586e0912a0a851bacf309c595c308 2 file2
100644 5f2f16bfff90e6620509c0cf442e7a3586dad8fb 3 file2
当冲突出现时,通过三方比较或合并标记可以对冲突进行检查:
$ cat file2
<<<<<<< HEAD
abcd
=======
1111
>>>>>>> other
也可以使用git dif命令:
$ git diff
diff --cc file2
index acbe86c,5f2f16b..0000000
--- a/file2
+++ b/file2
@@@ -1,1 -1,1 +1,5 @@@
++<<<<<<< HEAD
+abcd
++=======
+ 1111
++>>>>>>> other
git diff是比较工作目录与索引的差异,但这里的比较好像并不是如此,因为其打印结果跟之前似乎有所不同。
这里的git diff命令是特殊的,特定于合并的变体,以同时显示针对两个父版本做的修改。上半部分对应HEAD的父版本,后半部分对应other分支的父版本。Git也给第二个父版本命名为MERGE_HEAD。
可以将工作目录和HEAD及MERGE_HEAD版本库进行比较:
$ git diff HEAD file2
diff --git a/file2 b/file2
index acbe86c..0f4a467 100644
--- a/file2
+++ b/file2
@@ -1 +1,5 @@
+<<<<<<< HEAD
abcd
+=======
+1111
+>>>>>>> other
$ git diff MERGE_HEAD file2
diff --git a/file2 b/file2
index 5f2f16b..0f4a467 100644
--- a/file2
+++ b/file2
@@ -1 +1,5 @@
+<<<<<<< HEAD
+abcd
+=======
1111
+>>>>>>> other
这样就清楚多了,用户只需要对上述的file2内容做修改,然后暂存,提交即可。
而git diff HEAD还有一种写法:
$ git diff --ours
* Unmerged path file2
diff --git a/file2 b/file2
index acbe86c..0f4a467 100644
--- a/file2
+++ b/file2
@@ -1 +1,5 @@
+<<<<<<< HEAD
abcd
+=======
+1111
+>>>>>>> other
同时git diff MERGE_HEAD也还有一种写法:
$ git diff --theirs
* Unmerged path file2
diff --git a/file2 b/file2
index 5f2f16b..0f4a467 100644
--- a/file2
+++ b/file2
@@ -1 +1,5 @@
+<<<<<<< HEAD
+abcd
+=======
1111
+>>>>>>> other
git diff命令的打印结果中,标记部分有两列,第一列表示活动分支,第二列表示other分支,++表示两个分支中都存在,+表示只有一个分支中存在。也就是说,git diff命令只显示仍然有冲突的部分。
而在解决冲突的过程中,可以使用一些特殊的git log选项来找出变更的确切来源和原因:
$ git log --merge --left-right -p
commit > cb56d13a485bf70d1104749d1261c70c7c4282e0 (other)
Author: wood_glb
Date: Sun Jun 26 19:49:36 2022 +0800
commit file2 again
diff --git a/file2 b/file2
new file mode 100644
index 0000000..5f2f16b
--- /dev/null
+++ b/file2
@@ -0,0 +1 @@
+1111
commit < 097862d886a284294ffcd6bccf25d293f6dbf618 (HEAD -> master)
Author: wood_glb
Date: Sun Jun 26 19:49:28 2022 +0800
commit file2
diff --git a/file2 b/file2
new file mode 100644
index 0000000..acbe86c
--- /dev/null
+++ b/file2
@@ -0,0 +1 @@
+abcd
上面的打印结果说明了冲突的来源,分别是other和master分支的某一次提交。在合并中两个分支都影响冲突的文件,上面的命令将会显示这两部分历史中的所有提交,并显示每次提交引入的实际变更。
上面命令的各个选项含义为:
git对冲突的追踪主要分为以下几个部分:
之前提到可以使用git ls-files来定位冲突的文件:
$ git ls-files -s
100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0 file1
100644 acbe86c7c89586e0912a0a851bacf309c595c308 2 file2
100644 5f2f16bfff90e6620509c0cf442e7a3586dad8fb 3 file2
100644 00dedf6bd5f3e493ce8b03c889912f47b01297d4 0 file3
100644 0373d9336f8c8ee90faff225de842888e884a48b 0 file4
$ git ls-files -u
100644 acbe86c7c89586e0912a0a851bacf309c595c308 2 file2
100644 5f2f16bfff90e6620509c0cf442e7a3586dad8fb 3 file2
$ git cat-file -p acbe86c
abcd
$ git cat-file -p 5f2f16bf
1111
git ls_files的-s参数显示所有文件的各个阶段,-u参数则只关注冲突文件。而使用git cat-file命令则可以查看变体文件的内容。
如果冲突已经完全解决,并使用git add暂存,git commit提交后,该合并就结束了。
此时再使用git ls-files来查看文件是如何存储的:
$ git ls-files -s
100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0 file1
100644 d1d06ad36a5841e8c06a90bd29707290d979cc2b 0 file2
100644 00dedf6bd5f3e493ce8b03c889912f47b01297d4 0 file3
100644 0373d9336f8c8ee90faff225de842888e884a48b 0 file4
上面的打印中file2的暂存编号也为0,表示无冲突。
同时在git commit后,使用git show来查看这次合并提交:
$ git show
commit 9fb3688d8542f3b386e4352b1077993f523a953e (HEAD -> master)
Merge: 097862d cb56d13
Author: wood_glb
Date: Sun Jun 26 20:42:30 2022 +0800
merge other to master
diff --cc file2
index acbe86c,5f2f16b..d1d06ad
--- a/file2
+++ b/file2
@@@ -1,1 -1,1 +1,1 @@@
- abcd
-1111
++1111
当查看该合并提交时,应注意到:
如果开始合并操作后,而由于某些原因需要中止操作,在合并提交执行最后的git commit命令前,使用如下命令:
$ git reset --hard HEAD
HEAD is now at a521623 commit file2
该命令可以将工作目录和索引都还原到git merge命令之前。
如果要中止或在其已经结束后放弃,使用以下命令:
$ git reset --hard ORIG_HEAD
HEAD is now at a521623 commit file2
在开始合并操作前,Git把活动分支的HEAD保存在ORIG_HEAD,这样上述命令就是合理的。
这之后的内容对于实际开发可能用的不多,只是了解即可。
在复杂的项目开发过程中,如果合并的操作不合理,比如同一修改在不同分支之间来回合并,就会出现交叉合并的情况。因此为了应对某些复杂的情况,开发人员将问题普遍化,参数化并提出了可替代,可配置的合并策略来处理不同的情况。
有两种导致合并的常见退化情况:
这两种情况下执行git merge都不会引入合并提交,以免出现新的问题。
常规合并中的合并策略会产生一个最终提交,并添加到当前分支,表示合并的组合状态:
比如之前的合并操作,就会显示合并策略。
$ git merge -m "merge other to master" master other
Merge made by the 'recursive' strategy.
file3 | 1 +
file4 | 1 +
2 files changed, 2 insertions(+)
create mode 100644 file3
create mode 100644 file4
有两个特殊的策略:
这两种策略都会产生一个最终提交,添加到当前分支中,代表合并的组合状态。
用户如果想要明确指定使用的合并策略,可以使用git merge的-s选项,不过一般情况下,如果对Git没有深刻的理解的话,不建议如此操作。
这里提到的每个合并策略都使用相关的合并驱动程序来解决和合并每个单独文件。一个合并驱动程序接受三个临时文件来表示共同祖先,目标分支版本和其它分支版本。而驱动程序通过修改目标分支得到合并结果。
存在几种内置的合并驱动程序:
大多数文本文件被text驱动程序处理,大多数二进制文件被binary驱动程序处理,特殊需求则可以指定自定义合并驱动程序。
如果分支other包含了很多提交,在某些系统中,将other合并到master分支会产生一个差异,并将其当作一个单独的patch打到master,然后在历史中创建一个新元素,这就是所谓的压制提交。
压制提交会将所有的单独提交压制为一个大修改,然后应用到目标分支上,这样就只需关心目标分支的历史记录,而other分支的历史记录将会丢失。
在Git中,可以根据需要进行压制提交,这需要借助git merge的--squash选项。但是使用该选项要考虑到可能出现的后果,将所有提交进行压制意味着以后如果需要分离之前的提交就会变得非常麻烦,因为提交历史记录消失了。