Git Rebase

Git是目前最好的版本管理工具,初入职场狠狠学习了一番,做了不少笔记,其中关于git rebase的笔记在公司内部被点赞最多,于是搬到博客。

Rebase,中文世界里常被称为「变基」,即改变基础

改变基础 – git rebase

什么叫基础呢?除了第一条默认分支master以外,其他分支都是在别的分支的某个commit之上发展而来的,如下图:

rebase-1@2x.png

图中,分支experiment是在master分支的commit C2的基础上发展而来的,故而认为C2的experiment分支的base。

通过git rebase命令可以改变分支的base,动手体验一下。
首先,按照上图创建了git工程,commit和branch信息如下(通过SourceTree查看,使用git log --graph --decorate也可以看到类似效果):

rebase-2@2x.png

现在执行rebase操作:

git checkout experiment
git rebase master

新的commit和branch信息如下:

rebase-3@2x.png

显然,分支experiment的base变成了C3:

rebase-4@2x.png

可以看到:分支experiment中的commits都是新的,原来的commits都没了。

rebase和merge操作非常类似,Rebasing里有详细说明。

git rebase的基本使用姿势有两种git rebase <branch>git rebase <SHA-1>。分支名<branch>说到底是指向某个commit的指针,git rebase <branch>将当前分支的base重设为<branch>所指向的commit;git rebase <SHA-1>更直接一些,<SHA-1>指的是目标commit的id。

演示一下git rebase <SHA-1>的使用示例:

rebase-5@2x.png

在上图的基础上执行git rebase 32aafcb(32aafcb是C3的SHA-1标识符),得到的结果如下:

rebase-6@2x.png

可以看到,分支experiment的base由C2变成了C3。

延伸问题:什么情景下,使用rebase而不是merge?

重写历史 – git rebase -i

加上-i参数可以在rebase过程中对某些commit进行编辑,这里的参数-i表示的是interactive,换句话说,这是个交互式的命令。

常会使用到git rebase -i <SHA-1>进行commit操作,包括删除commit、合并commit。

删除commit

有时候,我们想删掉某个commit。

rebase-7@2x.png

假如想删掉experiment最新的commit E3,执行git rebase -i 4063139,之后会进入可交互的vim编辑器,如下:

rebase-8@2x.png

可以对分支experiment上的每个commit进行精细控制:

  • pick是默认操作,啥都不干;
  • squash,删除分支,把更新内容合并到前一个分支,在下一步中会让重新编辑previous commit的log message
  • fixup,和squash差不多,只是不会让重新编辑previous commit的log message
  • drop,删除commit

这里需要删除E3,因此修改第三行的pick为drop,得到的结果如下:

rebase-9@2x.png

可以看到,分支experiment的最后一个commit E3不见了。需要说明的是,由于没有修改分支experiment的base,所以experiment上的其他commit并未发生变化(相应的SHA-1不变)。

合并commit

现在,我想在上面的基础上合并experiment的commit E1和commit E2,仍然执行git rebase -i 4063139,进入vim编辑器:

rebase-10@2x.png

修改第二行的pick为squash,保存后,Git会让重新编辑E1的log message信息:

rebase-11@2x.png

这里直接把原来E2的log message给注释起来,只保留E1的log message,保存,得到的结果如下:

rebase-12@2x.png

原来E1内容发生了变化,因此产生被产生的新commit被替换,SHA-1由682f3dd变为了56f1332。
P.S: 如果当前只有master分支呢?该如何删除commit和合并commit呢?一回事儿…

当Rebase遇到冲突

上文示例不存在冲突问题,相对比较简单,但实际情况没有这么完美,往往有一些冲突需要解决。

在实际开发中,我经常会面对这么一种情景:基于dev分支创建一个需求分支,搞定后,提交pr,同事review pr时会陆陆续续指出一些问题,针对同事的问题后续又在这个分支上提交若干个pr,终于被认可;是时候合并到dev分支了,但是在此过程中,dev分支发生了一些改变(新增了若干个commit),导致merge冲突,有些冲突无法手动merge(譬如Xcode的工程文件.pbxconfig,该文件记录了大量信息,根本无法手动merged)。

针对这种问题,比较常见的做法是,更新本地dev分支,然后将需求分支rebase到最新的dev分支上,此处的rebase过程往往会需要处理一些冲突。

举个稍微复杂点的例子模拟rebase过程中的冲突问题:

# 创建仓库
git init
# 创建名为foo的文件,注入一行内容「created by master」
cat << EOF > foo
created by master
EOF
# master分支的第一个commit - C0
git add foo; git commit -m "C0"
# 创建一个名为experiment的分支
git checkout -b experiment
# 修改foo,重新注入内容「modified by experiment, 1st」
cat << EOF > foo
modified by experiment, 1st
EOF
# experiment分支的第一个commit - E1
git add foo; git commit -m "E1"
# 修改foo,重新注入内容「modified by experiment, 2st」
cat << EOF > foo
modified by experiment, 2st
EOF
# experiment分支的第一个commit - E2
git add foo; git commit -m "E2"
# 修改foo,重新注入内容「modified by experiment, 3s」,并添加一行「appended by experiment」
cat << EOF > foo
modified by experiment, 3st
appended by experiment
EOF
# experiment分支的第一个commit - E3
git add foo; git commit -m "E3"
# 切回master分支
git checkout master
# 修改foo,重新注入内容「modified by experiment, 3s」,并添加一行「appended by experiment」
cat << EOF > foo
final! modified by master
EOF
# master分支的第一个commit - C1
git add foo; git commit -m "C1"

创建两个分支:master和experiment。

master分支的第一个commit C0创建一个文本文件foo,并注入一行文本「created by master」;第二个commit C1修改了第一行的文本。
experiment分支在master的C0的基础上创建,之后提交了3次commit,第一次和第二次均只修改了第一行文本,第三个commit也修改了第一行文本,并且新增了第二行文本「appended by experiment」。

下图是更形象的描述:

rebase-13@2x.png

现在的目标是将experiment分支rebase到master的最新commit C1上,且experiment分支最终commit的foo内容为:

final! modified by master
appended by experiment

执行命令:

git checkout experiment
git rebase master

有如下警告:

First, rewinding head to replay your work on top of it...
Applying: E1
Using index info to reconstruct a base tree...
M foo
Falling back to patching base and 3-way merge...
Auto-merging foo
CONFLICT (content): Merge conflict in foo
error: Failed to merge in the changes.
Patch failed at 0001 E1
The copy of the patch that failed is found in: .git/rebase-apply/patch
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

错误信息说明:合并E1和C1时产生冲突,冲突点在文件foo上。

此时有3个选择:abort,continue、skip。

  • abort,顾名思义,放弃该rebase
  • continue,在此之前,用户需要手动解决E1和C1的冲突,然后git rebase --continue通过
  • skip,略过E1,这意味着用户愿意放弃「E1与C1冲突的patch」

我认为可以这么理解rebase的过程:若想将experiment分支rebase到master分支上,先在master的基础上创建一个临时分支(类似于变量,记为experiment),然后将experiment分支上的commit对应的patch逐个添加到到experiment的index上,如果完全没问题,就创建一个commit,并移动experiment到该commit上…如果有冲突,则终止该过程,给出如上的警告信息,用户可以选择abort;也可以选择skip,如果是skip,则意味着忽略掉冲突的内容,没有冲突的内容仍然保留在暂存区,但不会创建commit,接着处理下一个commit,直到结束;如果用户在过程中选择continue,则意味着用户不愿放弃冲突的commit,要想游戏继续,用户得自己手动将conflict给干掉。

如下是我根据自己对rebase解决冲突问题的理解所写的伪代码:

var _experiment_ = master.copy() // 创建一个临时分支
for (var commit in experiment.commits) {
var noConflict = YES
var conflictFiles = []
for (file in commit) {
if (file.isOK) {
// 将无冲突文件添加到暂存区
_experiment_.index.add(file)
} else {
// 标记冲突
noConflict = NO
// 缓存冲突文件
conflictFiles.add(file)
}
}
if (noConflict) {
// 无冲突,创建新分支(根据commit的元信息,及暂存区的内容)
var newCommit = createNewCommit(commit, _experiment_.index)
// 添加新分支
_experiment_.next = newCommit
// 移动_experiment_
_experiment_ = newCommit
// 清空index
_experiment_.index.clear()
} else {
// ... 将冲突报告给用户,等待处理
if (skip) { // 放弃current commit中的冲突文件,处理下一个commit
conflictFiles.clear()
continue;
} else if (continue) { // 用户选择continue,并且已经解决掉了conflictFiles中的文件冲突问题
// 将无冲突文件添加到暂存区
_experiment_.index.add(conflictFiles)
// 创建新分支(根据commit的元信息,及暂存区的内容)
var newCommit = createNewCommit(commit, _experiment_.index)
// 添加新分支
_experiment_.next = newCommit
// 移动_experiment_
_experiment_ = newCommit
// 清空index
_experiment_.index.clear()
}
}
}

总之,要完成上述示例所要达到的目标,可以这样:

git rebase master
git rebase --skip # 忽略E1中的冲突文件
git rebase --skip # 忽略E2中的冲突文件

到最后一个commit了,不能再skip了,否则最终得到的experiment完全和master一致。现在开始手动解决foo冲突:

<<<<<<< 305b5e89276ff01a66760ea9c7e8d65950154b13
final! modified by master
=======
modified by experiment, 3st
appended by experiment
>>>>>>> E3

解决结果:

final! modified by master
appended by experiment

然后:

git add * # 不要忘了
git rebase --continue

Game over!

rebase-14@2x.png

本文参考