凌云的博客

行胜于言

跟我一起学Git (七) 合并

分类:Git| 发布时间:2014-03-13 23:29:00


Git是一个分布式的版本管理系统,因此它允许开发者在任何时候合并修改 而不需要一个中心代码库。 在Git中要合并的所有分支都必须在同一个代码库中。

关于合并的例子

当需要合并 other_branch 到 branch 中时,你需要签出目标分支然后合并 other_branch 进来,如下所示:

$ git checkout branch
$ git merge other_branch

准备合并

在你进行合并之前,最好是保持你的工作目录的整洁

合并两个分支

让我们通过一个最简单的例子来演示如何合并两个分支

$ git init
/tmp/conflict/.git/
$ cat > file
Line 1 stuff
Line 2 stuff
Line 3 stuff
^D
$ git add file
$ git commit -m"Initial 3 line file"
[master (root-commit) 4fe8c6c] Initial 3 line file
 1 file changed, 3 insertions(+)
create mode 100644 file

让我们创建另一个提交到主分支

$ cat > other_file
Here is stuff on another file!
$ git add other_file
$ git commit -m"Another file"
[master e86b4f1] Another file
 1 file changed, 1 insertion(+)
create mode 100644 other_file

现在我们有一个两个提交的分支,下一步让我们切换到另一个分支,并且修改第一个文件

$ git checkout -b alternate master^
Switched to a new branch 'alternate'

$ git show-branch
* [alternate] Initial 3 line file
 ! [master] Another file
--
+ [master] Another file
*+ [alternate] Initial 3 line file

$ cat >> file
Line 4 alternate stuff
^D
$ git commit -a -m"Add alternate's line 4"
[alternate 74fff9a] Add alternate's line 4
 1 file changed, 1 insertion(+)

好了,现在我们一共有两个分支,并且每一个分支都有差异, master分支多了一个分支,而alternate分支修改了file, 下一步让我们来合并这两个分支

$ git checkout master
Switched to branch 'master'

$ git status
# On branch master
nothing to commit, working directory clean

$ git merge alternate
Merge made by the 'recursive' strategy.
 file | 1 +
 1 file changed, 1 insertion(+)

现在你可以通过以下命令来查看提交图

$ git log --graph --pretty=oneline --abbrev-commit
*   55d6e40 Merge branch 'alternate'
|\
| * 74fff9a Add alternate's line 4
* | e86b4f1 Another file
|/
* 4fe8c6c Initial 3 line file

有冲突的合并

合并操作难免会有冲突,让我们演示一下发生冲突的情况

$ cat >> file
Line 5 stuff
Line 6 stuff
^D
$ git commit -a -m "Add line 5 and 6"
[master c96a1d6] Add line 5 and 6
 1 file changed, 2 insertions(+)

现在,在另一个分支中修改相同的文件

$ git checkout alternate
Switched to branch 'alternate'

$ git show-branch
* [alternate] Add alternate's line 4
 ! [master] Add line 5 and 6
--
 + [master] Add line 5 and 6
*+ [alternate] Add alternate's line 4

$ cat >> file
Line 5 alternate stuff
Line 6 alterante stuff

$ cat file
Line 1 stuff
Line 2 stuff
Line 3 stuff
Line 4 alternate stuff
Line 5 alternate stuff
Line 6 alterante stuff

$ git diff
diff --git a/file b/file
index f92ee5a..a8694a6 100644
--- a/file
+++ b/file
@@ -2,3 +2,5 @@ Line 1 stuff
Line 2 stuff
Line 3 stuff
Line 4 alternate stuff
+Line 5 alternate stuff^M
+Line 6 alterante stuff^M

$ git commit -a -m"Add alternate line 5 and 6"
[alternate da6dced] Add alternate line 5 and 6
 1 file changed, 2 insertions(+)

让我们来查看现在的情况:

$ git show-branch
* [alternate] Add alternate line 5 and 6
 ! [master] Add line 5 and 6
--
*  [alternate] Add alternate line 5 and 6
 + [master] Add line 5 and 6
*+ [alternate^] Add alternate's line 4

下一步,让我们切换到master分支,然后尝试合并

$ git checkout master
Switched to branch 'master'

$ git merge alternate
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.

当发生合并冲突时,你可以使用git diff来查看发生冲突的范围

$ git diff
diff --cc file
index 6789c80,a8694a6..0000000
--- a/file
+++ b/file
@@@ -2,5 -2,5 +2,10 @@@ Line 1 stuf
Line 2 stuff
Line 3 stuff
Line 4 alternate stuff
++<<<<<<< HEAD
 +Line 5 stuff
 +Line 6 stuff
++=======
+ Line 5 alternate stuff
+ Line 6 alterante stuff
++>>>>>>> alternate

这个git diff明显输出了你的工作目录的文件和index中的文件的差异, 但是和传统的git diff命令输出的不同, 在发生合并冲突时的git diff命令的输出前面有两列, 第一列的+表示工作目录的文件和目标分支文件增加的部分, 而第二列表示工作目录的文件和合并分支文件增加的部分

现在,简单地将文件修改为如下内容:

$ cat file
Line 1 stuff
Line 2 stuff
Line 3 stuff
Line 4 alternate stuff
Line 5 stuff
Line 6 alterante stuff

如果你已经解决了冲突问题,可以使用git add命令添加文件

$ git add file

然后提交修改

$ git commit

Merge branch 'alternate'

Conflicts:
    file
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#       .git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#   (use "git commit" to conclude merge)
#
# Changes to be committed:
#
#       modified:   file
#

修改提交信息后提交

$ git show-branch
! [alternate] Add alternate line 5 and 6
 * [master] Merge branch 'alternate'
--
- [master] Merge branch 'alternate'
+* [alternate] Add alternate line 5 and 6

合并冲突详解

在上一个例子中,演示了当发生冲突时不能自动合并的情况。 现在让我们通过另一个例子来看看Git提供了什么工具来帮助我们解决冲突。

$ git init
Initialized empty Git repository in /tmp/conflict/.git/
$ echo hello > hello
$ git add hello
$ git commit -m"Initial hello file"
[master (root-commit) 78a6935] Initial hello file
1 file changed, 1 insertion(+)
 create mode 100644 hello

$ git checkout -b alt
Switched to a new branch 'alt'

$ echo world >> hello

$ echo 'Yay!' >> hello

$ git commit -a -m"one world"
[alt eb916ae] one world
 1 file changed, 2 insertions(+)

$ git checkout master
Switched to branch 'master'

$ echo worlds >> hello

$ echo 'Yay!' >> hello

$ git commit -a -m"All worlds"
[master ddcd6cf] All worlds
 1 file changed, 2 insertions(+)

$ git merge alt
Auto-merging hello
CONFLICT (content): Merge conflict in hello
Automatic merge failed; fix conflicts and then commit the result.

和预料的一样,Git警告你文件 hello 发生冲突了

定位冲突文件

当git merge因为冲突失败后,会在index文件中标记冲突的文件

$ git status
# On branch master
# You have unmerged paths.
#   (fix conflicts and run "git commit")
#
# Unmerged paths:
#   (use "git add <file>..." to mark resolution)
#
#       both modified:      hello
#
no changes added to commit (use "git add" and/or "git commit -a")

$ git ls-files -u
100644 ce013625030ba8dba906f756967f9e9ca394464a 1       hello
100644 e63164d9518b1e6caf28f455ac86c8246f78ab70 2       hello
100644 562080a4c6518e1bf67a9f58a32a67bff72d4f00 3       hello

检查冲突

当发生冲突时,Git会将冲突的部分标记出来

$ cat hello
hello
<<<<<<< HEAD
worlds
=======
world
>>>>>>> alt
Yay!

其中<<<<<<<这一行和=======这一行之间的部分是来自目标分支的, 而=======和>>>>>>>直接的内容是来自于要合并的分支的。

git diff和冲突

当发生合并冲突时git diff会输出其特有的格式

$ git diff
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello
@@@ -1,3 -1,3 +1,7 @@@
hello
++<<<<<<< HEAD
 +worlds
++=======
+ world
++>>>>>>> alt
Yay!

什么意思?它只是简单地输出了两个差异的合成, 第一个对应HEAD,第二个对应要合并的分支MERGE_HEAD

$ git diff HEAD
diff --git a/hello b/hello
index e63164d..1f2f61c 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,7 @@
hello
+<<<<<<< HEAD
worlds
+=======
+world
+>>>>>>> alt
Yay!


$ git diff MERGE_HEAD
diff --git a/hello b/hello
index 562080a..1f2f61c 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,7 @@
hello
+<<<<<<< HEAD
+worlds
+=======
world
+>>>>>>> alt
Yay!

在新版本的Git中,git diff --ours等同于git diff HEAD。 而git diff --theirs等同于git diff MERGE_HEAD。 git diff --base等同于git diff $(git merge-base HEAD MERGE_HEAD)。

如果你将hello文件修改为

$ cat hello
hello
worldly ones
Yay!

那么此时git diff的结果将为:

$ git diff
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello
@@@ -1,3 -1,3 +1,3 @@@
hello
- worlds
-world
++worldly ones
Yay!

另外,如果你将hello选择为其中一个分支的版本比如:

$ cat hello
hello
world
Yay!

那么git diff的输出将为:

$ git diff
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello

当你选择其中一个分支的版本时, git diff会认为你不关心和其他分支的差异, 因而将不会将这个差异输出。

git log和冲突

当你在解决冲突时,你可以使用如下命令查看修改是从哪里来的,以及修改的原因。

$ git log --merge --left-right -p
commit < ddcd6cf8fce9aa33d8c04d2b99694047bc30cbbc
Author: Jianlong Chen <jianlong99@gmail.com>
Date:   Mon Feb 3 22:54:50 2014 +0800

    All worlds

diff --git a/hello b/hello
index ce01362..e63164d 100644
--- a/hello
+++ b/hello
@@ -1 +1,3 @@
hello
+worlds
+Yay!

commit > eb916aed33f6128a216411a0e95e3313f54bfdb9
Author: Jianlong Chen <jianlong99@gmail.com>
Date:   Mon Feb 3 22:54:20 2014 +0800

    one world

diff --git a/hello b/hello
index ce01362..562080a 100644
--- a/hello
+++ b/hello
@@ -1 +1,3 @@
hello
+world
+Yay!

选项说明如下:

  • --merge 表示只显示那些有冲突的文件相关的提交
  • --left-right 表示如果提交是来自我们版本的提交则输出<,如果提交是来自其他版本的提交则输出>
  • -p 表示输出每个提交的提交信息和patch

Git是如何追踪冲突的

  • .git/MERGE_HEAD 包含你要合并的SHA1码
  • .git/MERGE_MSG 当你使用git commit后发生冲突时,MERGE_MSG包含默认的合并信息
  • index文件包含3个版本的冲突文件,merge base版本,"our"的版本以及"theire"的版本。
  • 冲突版本的文件不在index相反它保存在工作目录中

我们先用 git ls-files 来看看index的内容:

$ git ls-files -s
100644 ce013625030ba8dba906f756967f9e9ca394464a 1       hello
100644 e63164d9518b1e6caf28f455ac86c8246f78ab70 2       hello
100644 562080a4c6518e1bf67a9f58a32a67bff72d4f00 3       hello

可以看到确实有3个版本的hello

$ git cat-file -p e63164d951
hello
worlds
Yay!

$ git diff :1:hello :3:hello
diff --git a/:1:hello b/:3:hello
index ce01362..562080a 100644
--- a/:1:hello
+++ b/:3:hello
@@ -1 +1,3 @@
hello
+world
+Yay!

解决冲突

接下来让我们解决冲突

$ cat hello
hello
everyone
Yay!

$ git add hello

$ git ls-files -s
100644 ebc56522386c504db37db907882c9dbd0d05a0f0 0       hello

可以看到index只剩下修改后的hello了。 最后,你可以使用 git commit 来提交最后的结果 并且使用 git show 来查看合并的提交:

$ cat .git/MERGE_MSG
Merge branch 'alt'

Conflicts:
        hello

$ git commit
[master 41ce868] Merge branch 'alt'

$ git show
commit 41ce8680c3ae210ee574b25fe46dc18e1f7a864f
Merge: ddcd6cf eb916ae
Author: Jianlong Chen <jianlong99@gmail.com>
Date:   Tue Feb 4 09:17:54 2014 +0800

Merge branch 'alt'

Conflicts:
    hello

diff --cc hello
index e63164d,562080a..ebc5652
--- a/hello
+++ b/hello
@@@ -1,3 -1,3 +1,3 @@@
hello
- worlds
-world
++everyone
Yay!

取消或者重新合并

如果你要取消合并或者发现合并出错了想要重新合并可以使用以下命令:

$ git reset --hard HEAD

如果你已经提交了合并,想取消合并可以使用以下命令:

$ git reset --hard ORIG_HEAD

需要注意的是,如果你是在不整洁的工作目录下进行合并的, 那么你将丢失所有的未提交修改。

合并策略

退化合并(Degenerate Merges)

以下两种情况属于退化的合并

  • Already up-to-date 当前分支已经包含要合并分支的提交
  • Fast-forward(快进)这种情况相反,要合并的分支包含当前分支的提交

普通合并

有三种合并策略:

  • Resolve 这种策略只能处理两个分支,将要合并分支的提交从 merge-base 开始到这个分支的最新分支合并到当前分支(three-way merge)
  • Recursive 这种策略也同样只能处理两个分支, 这种策略主要用于要合并的两个分支有多个merge-base的情况, 它首先将多个merge-base合并成一个临时的提交,然后再进行三路合并(three-way merge)
  • Octopus 这种策略用于合并两个或多个分支,实现很简单,它只是多次使用 Recursive 策略进行合并

默认的合并策略为 Recursive

专业合并

以下合并策略主要用于特殊情况:

  • Ours 这种策略只是简单地忽略其他分支的修改
  • Subtree 这种策略将其他分支的内容合并到当前分支一个特定的子目录中

可以通过git commit -s strategy 来指定合并合并。
比如:

$ git merge -s resolve Bob

合并驱动器(Merge Drivers)

本文中介绍的所有合并策略都使用 merge driver 来处理要合并的文件

有以下几种合并驱动器:

  • text merge driver 会将有冲突的部分用(<<<<<, ======, >>>>>>)标记起来
  • binary merge driver 只是简单地保留目录分支的版本
  • union merge driver在最终的文件中保留所有版本

默认情况下,Git会用 text merge driver 对所有文本文件进行处理, 而对二进制文件使用binary merge driver。