一、 Git Merge 合并策略
1.1 Fast-Forward Merge(快进式合并)
//在分支1下操作,会将分支1合并到分支2中
git merge <分支2>
最简单的合并算法,它是在一条不分叉的两个分支之间进行合并。快进式合并是默认的合并行为,并没有产生新的commit。如下图所示,虚线部分是合并前,在经过如下命令后:
//当前在Main分支下操作
git merge Dev
Git 将 Main 和 HEAD 指针移动到 Dev 所在的提交对象上,此时合并完成,不涉及到内容的变更和比较,所以这种合并方式效率很高。从上面的图可以发现,这种合并策略要求合并的两个分支上的提交,必须是一条链上的前后关系。
可以通过git merge --no-off
参数来进行关闭快进式合并,关闭后会强制使用Three Way Merge
(三路合并),下面来具体讲讲三路合并
1.2 Three Way Merge 三路合并
当两个分支的提交对象不在一条提交链上时,Git 会默认执行三路合并的方式进行合并。
首先 Git 会通过算法寻找两个分支的最近公共祖先节点,再将找到的公共祖先节点作为base节点,并使用三路合并的策略来进行合并,也就是我们常见的合并方式(三路指的是两个需要合并的分支的提交对象(比如下图中的提交对象 1,2),最近共同祖先(提交对象 0)三个对象):
可以通过传递不同参数来使用不同的合并策略:
git merge [ -s <strategy>] [-X <strategy-option>] <commit>...
- 参数
-s <strategy>
用于设定合并策略 - 参数
-X <strategy-option>
用于为所选的合并策略提供附加的参数
1.3.2 Resolve
策略
Resolve 策略是默认的三路合并策略,既可以使用 git merge <分支>
又可以使用 git merge -s resolve <分支>
来执行合并,该合并策略只能用于合并两个分支,也就是当前分支和另外的一个分支,使用三路合并策略。这种合并策略被认为是最安全、最快的合并分支策略。
比如在 Main 分支下执行以下命令:
//当前在Main分支下操作
git merge Dev
Git 会创建一个新的提交对象(如上图中的提交对象 3),然后该提交对象将合并提交对象 1 和提交对象 2 的内容,形成提交对象 3。但是一旦遇到两个分支对象有多个共同祖先时,此时 Resolve 策略就无法实现合并了,这个时候就需要用 Recursive
策略。
1.3.1 Recursive 策略
如下图所示,在对两个分支上的提交A和B进行合并时,我们发现了它们有两个共同祖先,分别是:ancestor0
和 ancestor1
。这就是 Criss-Cross
现象:
我们在此处复现一下这个现象:
//1.初始化一个recursiveMerge仓库
$ git init recursiveMerge
//2.创建一个master-commit1.txt文件并提交
$ git commit -m "master-commit1"
[master (root-commit) 85cba5f] master-commit1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 master-commit1.txt
//3.新建一个分支dev,并切换到dev。在dev分支创建一个dev-commit1.txt文件并提交
$ git checkout -b dev
Switched to a new branch 'dev'
$ git commit -m "dev-commit1"
[dev 9763dcc] dev-commit1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dev-commit1.txt
//4.切换到master分支,创建一个master-commit2.txt文件并提交
$ git checkout master
Switched to branch 'master'
$ git commit -m "master-commit2"
[master 5c23645] master-commit2
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 master-commit2.txt
//5.新建一个分支feature, 切换到feature,合并dev和feature
$ git merge dev
hint: Waiting for your editor to close the file... unix2dos: converting file D:/recursiveMerge/.git/MERGE_MSG to DOS format...
libpng warning: iCCP: known incorrect sRGB profile
libpng warning: iCCP: known incorrect sRGB profile
dos2unix: converting file D:/recursiveMerge/.git/MERGE_MSG to Unix format...
Merge made by the 'ort' strategy.
dev-commit1.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dev-commit1.txt
//6.切换到master,合并dev和master
$ git merge dev
hint: Waiting for your editor to close the file... unix2dos: converting file D:/recursiveMerge/.git/MERGE_MSG to DOS format...
libpng warning: iCCP: known incorrect sRGB profile
libpng warning: iCCP: known incorrect sRGB profile
dos2unix: converting file D:/recursiveMerge/.git/MERGE_MSG to Unix format...
Merge made by the 'ort' strategy.
dev-commit1.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dev-commit1.txt
我们在 Git GUI 中查看此时的分支图像:
那么Recursive 策略对于这种情况是怎么做的呢?
如果Git在寻找共同祖先时,在参与合并的两个分支上找到了不只一个满足条件的共同祖先,它会先对共同祖先进行合并,建立临时快照。然后,把临时产生的“虚拟祖先”作为合并依据,再对分支进行合并。对于上图而言,会将ancestor 0
和 ancestor 1
先合并成一个虚拟祖先 ancestor 2
,最后再将它与A和B提交一起进行三路合并:
在处理合并时,有可能会出现不同分支在相同文件上的冲突,因此可以使用下列选项来在发生冲突时起作用,常见的有:
Ours
选项
在遇到冲突时,选择当前分支版本,而忽略其他分支版本。如果他人的改动和本地改动不冲突,会将他人的改动合并进来:
# 在冲突合并中,选择当前分支内容,自动丢弃其他分支内容
git merge -s recursive -Xours
Theirs
选项
和 Ours
选项相反,遇到冲突时,选择他人的版本,丢弃当前分支版本
# 选择其他分支内容,自动丢弃当前分支内容
git merge -s recursive -Xtheis
此外还有 subtree[=<path>]
,renormalize
,no-renormalize
等等选项,具体可以看官方文档Git - merge-strategies Documentation
1.3 Octopus Merge
多路合并
git merge -s octopus <分支1> <分支2> ... <分支N>
此合并策略可以合并两个以上的分支,但是拒绝执行需要手动解决的复杂合并,它的主要用途是将多个分支合并到一起,该策略是对三个及三个以上的分支合并时的默认合并策略。
来做一个实验,此时我的 Git 仓库中有三个分支 feature1,feature2,feature3,执行 octopus 合并:
$ git merge -s octopus feature1 feature2 feature3
Trying simple merge with feature1
Trying simple merge with feature2
Trying simple merge with feature3
Merge made by the 'octopus' strategy.
feature1-1.txt | 0
feature3-2.txt | 0
four.txt | 0
second.txt | 0
third-2.txt | 0
third.txt | 0
6 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 feature1-1.txt
create mode 100644 feature3-2.txt
create mode 100644 four.txt
create mode 100644 second.txt
create mode 100644 third-2.txt
create mode 100644 third.txt
1.3.3 subtree
策略
这是一个经过调整的 recursive
策略,当合并树 A 和树 B 时,如果树 B 和树 A 的一个子树相同,B 树首先进行调整以匹配 A 的树结构,以免两棵树在同一级别进行合并。同时也针对两棵树的共同祖先调整。
git merge -s subtree <树A> <树B>
该部分会单独再出一篇,来姐姐子树合并和子模块合并
1.3.4 ours
策略
这种策略可以合并任意数量的分支,但是合并的结果总是使用当前分支的内容,丢弃其他分支的内容。
这种模式和上面的 recursive
策略中的 ours
选项不同,ours
选项是在某个分支下,忽略其他人的版本,ours
策略是忽略其他的分支。
二、 Git Rebase 变基
rebase
是指在另一个基端之上重新应用提交。很多时候我们在两个分支上用rebase
。但是**rebase**
命令的作用对象不仅仅限于分支。任何的提交引用,都可以被视作有效的**rebase**
基底对象。包括一个提交ID、分支名称、标签名称或者**HEAD~1**
这样的相对引用。
2.1 标准模式
git rebase
默认是标准模式,一般用于多个分支之间:在一个分支中集成另外分支的最新修改。
#当前在Feature分支下操作,会得到如下的变基状态
git checkout Feature
git rebase Main
#当前在branch1分支下操作,会得到如下的变基状态
git checkout Branch1
#将Main分支中没有Branch1分支的提交,拷贝到Branch2分支中
git rebase Main --onto Branch2
当执行git rebase
命令后,整个变基过程都会自动进行,如果在该命令后加上-i
参数后,就会进入到交互模式:
2.2 交互模式
git rebase -i
或者git rebase -interactive
开始后执行一个交互式rebase 会话。一般用于当前分支的历史提交进行编辑操作。
# 针对该commit 后的提交进行交互式操作
git rebase -i [commit ID]
运行命令后,会跳出交互界面,比如在git bash中输入如下命令:git rebase -i ecd259f
页面会出现下面的界面:
上图中的两条记录是提交列表,指的是 ID 为ecd259f...
的 commit 之前的提交。我们可以对这两个提交进行相关的操作,在上面的页面中也提供了几个命令:
pick
:保留该commit,和drop相对,也就是什么也不做(在IDEA中不选择,直接rebase,也会得到如下结果)reword
:修改该提交的commit message,如果该commit之后也有提交,那么之后的提交信息也会被改变edit
: 选择一个提交并修改该提交的内容squash
:合并该提交与上一个提交,也可以合并多个,两两合并,间隔合并fixup
:与squash类似,但是会抛弃当前的commit messageexec
: 执行shell命令,运行一条自定义命令
可以在提交列表前面进行参数修改,比如将 pick
改成 squash
就是将两个提交合并到 ID 为 ecd259f...
的提交对象中
squash a5eb754 first commit --init
squash 2df01ad ignoreList
三、总结
Git merge 和 Git rebase 的区别与联系
在日常对冲突的处理中,很明显的区别在于 rebase 的处理方式能让提交链更加清晰,而使用 merge 方式会显得提交链复杂交错。
下面我们具体来看看两者的区别与联系
区别
- 合并方式:
git merge
创建一个新的合并提交,将两个分支的更改合并到一起,而git rebase
将一系列提交从一个分支转移到另一个分支,并重新组织这些提交: - 提交历史:
git merge
保留每个分支的独立提交历史,形成一个合并的提交历史树,而git rebase修改提交历史的结构,使提交历史更线性:
- 冲突处理:在合并过程中,如果存在冲突,
git merge
和git rebase
都需要手动解决冲突。但是,解决冲突的方式略有不同。git merge
在合并提交中保留冲突的解决方案,而git rebase
在每个冲突点停下来,必须在解决冲突后才能继续进行rebase操作。 - 提交ID:由于
git rebase
重新应用提交,所以重新组织后的提交具有新的提交ID,而git merge
创建的合并提交保留了原始提交的ID。比如上图中,git rebase
命令执行后,对应的 master 分支的三个 commit 对象的提交 ID 都要被修改。
联系
- 合并操作:无论是
git merge
还是git rebase
,它们都是用于将一个分支的更改合并到另一个分支上。两者都是在对分支进行合并,是来解决冲突的。 - 版本同步:无论是
git merge
还是git rebase
,它们都可以用于将分支与上游分支同步,以确保分支包含上游的最新更改。 - 解决冲突:无论是
git merge
还是git rebase
,在合并过程中都可能存在冲突,需要手动解决冲突。
参考资料
https://www.geeksforgeeks.org/merge-strategies-in-git/
https://morningspace.github.io/tech/git-merge-stories-2/
https://git-scm.com/docs/merge-strategies/