最近工作上需要用到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.1git cherry-pick F1git 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 1Mine = commit 4Yours = commit 3merge后会生成一个新的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 3Mine = commit 7Yours = commit 6
一般情况下Base会选择Yours和Mine节点的最近的公共祖先。
但是有的时候最近的公共祖先不是唯一的,例如出现如下图所示的情况:

merge X'' Y'和X' Y''的时候发现有两个节点都符合最近的公共祖先,即:
X' YX Y'我们称这种情况为:Criss-cross-merge,这时就需要用到Recursive three-way merge算法,具体步骤如下:
最近的公共祖先递归调用merge,生成成一个虚拟的节点Basegit软件中使用的就是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 = CYours = DBase = B假设我们目前在foo_feature分支,运行git cherry-pick E,这时Three-way merge的参数:
Mine = CYours = EBase = Drebase官方定义如下:
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