最近工作上需要用到git cherry-pick来生成一个特殊的软件版本,具体要求如下
commit和branch如下图:
G <-- master
|
F2
|
E
| F2 <-- my-goal
F1 /
| F1
D /
| C <-- v3.0.1
|/
B
|
A
具体的做法是:
git checkout -b my-goal v3.0.1
git cherry-pick F1
git cherry-pick F2
其中遇到很多问题,例如:
cherry-pick F1
后无法编译,因为F1
依赖D
中的一些变更cherry-pick F2
出现大量冲突,后来通过人工肉眼进行对比修改,可以成功cherry-pick
对于第1个问题:要么就把D
也cherry-pick过来,要么手动把D
的部分必要修改(F1依赖的部分)也加过来。
对于第2个问题:既然人可以成功解决冲突,为啥git不能自动帮我解决呢?这就涉及到git的merge算法。
git merge文件是以行为单位进行一行一行进行合并的,但是有些时候并不是两行内容不一样git就会报冲突,因为git会帮我们自动进行取舍,分析出哪个结果才是我们所期望的,如果git都无法进行取舍的时候才会报冲突,这个时候才需要我们进行人工干预。那git是如何帮我们进行merge操作的呢?
在介绍git merge算法前,先来看一个比较简单的算法:Two-way merge
。
Two-way merge
解决的问题是:如何把两个文件进行合并。
举个例子,假设你和另外一个人同时修改了一个文件,这时merging算法
看到了这两个文件,如下图:
merging算法
发现两个文件大部分都一样,只有30行不一样,
Yours
的版本里内容是:Print("hello")
Mine
的版本里内容是:Print("bye")
但是merging算法
怎么知道是你修改了30行还是另外一个人修改了?可能会有以下几种情况:
Mine
版本没有修改,Yours
版本修改了内容(从Print("bye")
修改 Print("hello")
)Yours
版本没有修改,Mine
版本修改了内容(从Print("hello")
修改 Print("bye")
)Yours
和Mine
都修改了内容,(Yours
从???
修改成Print("hello")
;Mine
从???
修改成Print("bye")
Yours
和Mine
都增加了一行对于一个merge算法来说,该怎么处理上述4中情况呢?
Mine
版本没有修改,Yours
版本修改了内容 => 应该选Yours
版本Yours
版本没有修改,Mine
版本修改了内容 => 应该选Mine
版本Yours
和Mine
都修改了内容 => 需要手动解决冲突Yours
和Mine
都增加了一行 => 需要手动解决冲突由于缺乏必要的信息,Two-way merge
根本无法帮助我们解决冲突,TA只能帮助我们发现冲突,需要手动解决冲突。
如果让merging算法
知道更多一些信息,merging算法
是否可以帮助我们自动解决一些简单的冲突呢?下面来看一下Three-way merge
算法。
Three-way merge
是在Two-way merge
的基础上又增加了一个信息,即两个需要合并的文件修改前的版本。如下图所示,merge算法现在知道三个信息:
Mine
:需要合并的一个文件Yours
:另一个需要合并的文件Base
:两个文件修改前的版本这时merging算法
发现:
Base
版本里的内容是:Print("bye")
Yours
的版本里内容是:Print("hello")
Mine
的版本里内容是:Print("bye")
说明Yours
对这一行做了修改,而Mine
对这行没有做修改,因此对Yours
和Mine
进行merge后的结果应该采用Yours
的修改,于是就变成Print("hello")
。
这就是Three-way merge
的大致原理。
我们来看一个更加复杂的案例,如下图:
按行对比两个文件后,merging算法
发现有3个地方不一样,分别是:
Mine
的版本里面新增了一行我们来看一下这三种冲突改怎么解决:
Yours
修改了,因此使用Yours
的版本Yours
和Mine
都修改了,需要手工解决冲突Mine
新增了一行,因此使用Mine
的版本我们来看下git是如何使用Three-way merge
来进行git merge
操作的。
先来看下git merge
在官网的定义:
git-merge - Join two or more development histories together
即把两个或两个以上的开发历史进行合并。
这样讲比较抽象,来看一个简单的例子,假设我们有2个branch:
我们在task001
上开发了一段时间,需要把main
上的修改合并到task001
,这时可以运行
$ git checkout task001
$ git merge main
merge后结果如下
merge的过程其实就是使用Three-way merge
,其中
Base
= commit 1
Mine
= commit 4
Yours
= commit 3
merge后会生成一个新的merge节点commit 5
,并且commit 5
会同时依赖commit 3
和commit 4
。
我们继续在task001
上开发了几个commit后,终于完成了任务,需要把task001
合并会main
,这时可以运行
$ git checkout main
$ git merge task001
这次merge的过程也是一次Three-way merge
,其中:
Base
= commit 3
Mine
= commit 7
Yours
= commit 6
一般情况下Base
会选择Yours
和Mine
节点的最近的公共祖先
。
但是有的时候最近的公共祖先
不是唯一的,例如出现如下图所示的情况:
merge X'' Y'
和X' Y''
的时候发现有两个节点都符合最近的公共祖先
,即:
X' Y
X Y'
我们称这种情况为:Criss-cross-merge
,这时就需要用到Recursive three-way merge
算法,具体步骤如下:
最近的公共祖先
递归调用merge,生成成一个虚拟的节点Base
git软件
中使用的就是Recursive three-way merge
算法。
cherry-pick在官网的定义如下:
git-cherry-pick - Apply the changes introduced by some existing commits
即把已经有的commit apply到其他分支,git cherry-pick
其实也是使用Three-way merge
,其中:
Mine
= 执行cherry-pick
时所在的branch的HEADYours
= 被cherry-pick
的那个commitBase
= 被cherry-pick
的那个commit的前一个commit这样讲比较抽象,举个例子:
E <-- master
|
D
| C <-- foo_feature(*)
|/
B
|
A
假设我们目前在foo_feature
分支,运行git cherry-pick D
,这时Three-way merge
的参数:
Mine
= C
Yours
= D
Base
= B
假设我们目前在foo_feature
分支,运行git cherry-pick E
,这时Three-way merge
的参数:
Mine
= C
Yours
= E
Base
= D
rebase官方定义如下:
git-rebase - Reapply commits on top of another base tip
即使用其他分支作为基础,重新apply当前分支所有的commit,git rebase
的过程可以看做是不断的做git cherry-pick
,举个例子:
E <-- master
|
| F <-- foo_feature(*)
D /
| C
|/
B
|
A
在foo_feature branch
运行下面运行git rebase master
命令,就会变成下面的样子:
E <-- master
|
| F <-- foo_feature(*)
| /
| C
D /
| E
| /
| D
|/
B
|
A
相当于运行了下面几个命令:
git checkout master
git checkout -b foo_feature_rebased
git cherry-pick C
git cherry-pick F