Fork me on GitHub

Git rebase 变基

合并和变基

Git中整合来自不同分支的修改主要有两种方法:merge以及rebase

当两个不同分支,各自提交了更新,就会形成分叉:

rebase

整合分支最容易的方法是merge命令:把两个分支的最新快照C3C4,以及二者最近的共同祖先C2进行三方合并,合并的结果是生成一个新的快照。

rebase

还有一种方法叫做变基rebase:你可以提取在C4中引入的补丁和修改,然后在C3的基础上应用一次,将修改都移至C3上。

我们先切换到experiment分支,再变基:

1
2
$ git checkout experiment
$ git rebase master

变基原理是,首先找到这两个分支的最近共同祖先C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底C3, 最后将存为临时文件的修改依序应用。
即将C4变基到C3,以C3为基础重新生成C4(注:此C4experiment上的C4不一样,commit id不同)。

rebase

之后切回master进行合并。

rebase

这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。下面具体举例说明。

模拟日常开发 - 合并

两个分支masterdev分别有了提交,现在要将dev合并到master上。

rebase

直接 git merge:

1
2
$ git checkout master
$ git merge dev

那么git会这么做:
1、找出master分支和dev分支的最近共同祖先commit(357cb79)
2、将master最新一次commit(e88ccad)dev最新一次commit(05352ed)合并后生成一个新的commit(33d1a54),有冲突的话需要解决冲突。
3、将masterdev上,自357cb79之后的所有提交按照提交时间的先后顺序进行依次应用到master分支上。

rebase

git rebase 后再 git merge:

1
2
3
4
5
$ git checkout dev
$ git rebase master

$ git checkout master
$ git merge dev

1、rebase之前需要经master分支拉到最新。
2、切换分支到需要rebase的分支,这里是dev分支。
3、执行git rebase master,有冲突就解决冲突,解决后直接git add .git rebase --continue即可。可以发现并没有多出一次commit,且master上新增提交的commit id值已经变了。

rebase

4、切换到master分支,执行git merge dev,可以看到HEAD被置为a73089d

rebase

rebase

采用rebase的方式进行分支合并,master并没有多出一个新的commitdev分支上的commitrebase之后其hash值发生了变化,不再是dev分支上提交的时候的hash值了,但是提交的内容被全部复制保留了,并且整个master分支的commit记录呈线性记录。

总结:
git merge操作合并分支,会让两个分支的每次提交都按照提交时间(并不是push时间)排序,并且经过对比,会将两个分支的新增的commit合并成一个新的commit,最终的master分支树会分叉。

git rebase操作实际上是,将dev分支基于master之后的所有的commit打散成一个一个的patch,并重新生成新的commit id,再次基于master最新的commit上进行提交,并不依据两个分支上实际的每次提交的时间点排序,rebase完成后,切到master进行合并dev也不会生成一个新的commit,可以保持整个分支树的完美线性。

无论是通过变基,还是通过合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。

模拟日常开发 - 代码提交

平时提交代码,commit前没有pull其他人的提交,相当于远程仓库和本地的分支分叉了,因为pull相当于fetch + merge,所以此时拉取后会形成分支,此时对刚pull下来的分叉的commit进行rebase,会基于此commit,将之后的两个分叉的所有提交,重新生成一条新的提交线。

rebase

基于他人的提交变基,会将两边的新提交重新生成,生成一条直线。

rebase

rebase

模拟日常开发 - 合并多个 commit 为一个完整 commit

当我们在本地仓库中提交了多次,在我们把本地提交push到公共仓库中前,为了让提交记录更简洁明了,我们希望把如下分支B、C、D三个提交记录合并为一个完整的提交,然后再push到公共仓库。

rebase

现在我们在测试分支上添加了四次提交,我们的目标是把最后三个提交合并为一个提交:

rebase

这里我们使用命令:

1
git rebase -i [startpoint] [endpoint]

其中-i的意思是--interactive,即弹出交互式的界面让用户编辑完成合并操作,[startpoint] [endpoint]则指定了一个编辑区间,如果不指定 [endpoint],则该区间的终点默认是当前分支HEAD所指向的commit(注:该区间指定的是一个前开后闭的区间)。

在查看到了log日志后,我们运行以下命令:

1
2
3
git rebase -i 36224db
或:
git rebase -i HEAD~3

然后我们会看到如下界面:

rebase

上面注释的部分列出的是我们本次rebase操作包含的所有提交,下面注释部分是git为我们提供的命令说明。每一个commit id前面的pick表示指令类型,git为我们提供了以下几个命令:

  • pick:保留该 commit
  • reword:保留该 commit,但我需要修改该 commit 的注释
  • edit:保留该 commit, 但我要停下来修改该提交(不仅仅修改注释)
  • squash:将该 commit 和前一个 commit 合并
  • fixup:将该 commit 和前一个 commit 合并,但我不要保留该提交的注释信息
  • exec:执行 shell 命令
  • drop:我要丢弃该 commit

我们将commit内容编辑如下:

rebase

然后是注释修改界面:

rebase

编辑完保存即可完成commit的合并了。

模拟日常开发 - 将某一段 commit 粘贴到另一个分支上

当我们项目中存在多个分支,有时候我们需要将某一个分支中的一段提交同时应用到其他分支中,就像下图:

rebase

我们希望将develop分支中的C~E部分复制到master分支中,这时我们就可以通过rebase命令来实现。
在实际模拟中,我们创建了masterdevelop两个分支。

master分支:

rebase

develop分支:

rebase

输入命令:

1
git rebase [startpoint] [endpoint] --onto [branchName]

其中,[startpoint] [endpoint]仍然指定了一个前开后闭的编辑区间,--onto的意思是要将该指定的提交复制到哪个分支上。
所以,在找到C(90bc0045b)E(5de0da9f2)commit id后,我们输入以下命令:

1
git rebase 90bc0045b^ 5de0da9f2 --onto master

注:因为[startpoint] [endpoint]指定的是一个前开后闭的区间,为了让这个区间包含C,我们将区间起始点向后退了一步。

运行完成后查看当前分支的日志:

rebase

可以看到,C~E部分的提交内容已经复制到了G的后面了。
我们看一下当前分支的状态:

rebase

当前HEAD处于游离状态,实际上,此时所有分支的状态应该是这样:

rebase

所以,git只是将C~E部分的提交内容复制一份粘贴到了master所指向的提交后面,我们需要做的就是将master所指向的commit设置为当前HEAD所指向的commit就可以了,即:

1
2
git checkout master
git reset --hard 0c72e64

rebase

注意

最后需要注意的是,不要通过rebase对任何已经提交到公共仓库中的commit进行变基修改,否则可能会丢弃了一些别人的所基于的提交。