将一个分支合并到另一个分支有两种,一种是大多都很熟悉的 merge(合并),另一种就是本篇要介绍的 rebase(衍合)。
看下本文纲要
已经有 merge 了,为什么需要 rebase ?我们先跟着官方文档学习下 rebase 的基本概念
rebase 是做什么的?
(如果你大概知道 rebase 是做什么的,可以直接跳到第二趴,实战 rebase)
在了解 rebase 之前,先温习下 merge 的过程
假设现在基于
然后在这个分支
使用 merge 合并分支
此时拉取
如果想让 git rebase
使用 rebase 合并
rebase 有的翻译成衍合,有的直接翻译成变基,变基就很好理解了,就是重新设定基底。
$ git checkout hotfix
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: ***
git rebase
这个命令会将
rebase 原理 衍合前回到两个分支(所在的分支和想要衍合的分支)的共同祖先,提取你所在分支每次提交时产生的差异(diff),把这些差异分别保存到临时文件里,然后从当前分支转换到你需要衍合的分支,依照顺序使用每一个差异补丁文件。
当
使用 git rebase
之后产生的历史会是如下
还是有点乱吗?再通俗点呢,原来的「基点」是 C2,现在把「基点」改变到要衍合的分支处,就是把「基点」搞到 C6 哪儿去,然后把改变的内容,应用上去。然后分支历史就被改写啦。
实战体验 rebase
概念和效果清楚了,实战一下,推荐一个学习 Git 实践的网站,这个工具非常有意思,可以用来学习模拟基本的 Git 操作,戳这里学习
模拟以下操作过程
- 在
:C1 处创建 和 分支,在三个分支分别产生提交 C2~C4
:C2
:C3
:C4
那么当前的历史节点如下图:
2:衍合 b1 分支到 master 分支
git rebase b1
衍合过程分析:先回到 C1,提取当前分支
和 C1 的差异,形成 C4' (C4 和 C1 的差异) 保存起来,当前分支 转换到要衍合的分支 (C2处),再把 C4' 应用进去。
这里要注意一下:在命令里面可以看到 Git 提示衍合过程
$ git rebase b1
First, rewinding head to replay your work on top of it...
Applying: C4
这里其实应用的并不是 C4 那次 commit,而是 C4 和 C1 比较后的 diff,C4'。所以 C4 和 C4' 是两个不同的提交(会产生不同的历史,但是内容是一样的)。
- 再做一次衍合,将 b2 也合并过来
: git rebase b2
$ git rebase b2
First, rewinding head to replay your work on top of it...
Applying: C2
Applying: C4
还是会提取
- 此时如果 b2 merge master 的话 实际就是快速跟进了。
: git merge master
同样的过程,把上面的 rebase
都换成 merge
小结
可以看到不论用 rebase 还是 merge 得到的结果是没有区别的,但是衍合能产生一个更为整洁的提交历史。如果视察一个衍合过的分支的历史记录,看起来更清楚:仿佛所有修改都是先后进行的,尽管实际上它们原来是同时发生的。
你可以经常使用衍合,确保在远程分支里的提交历史更清晰。比方说,某些项目自己不是维护者,但想帮点忙,就应该尽可能使用衍合:先在一个分支里进行开发,当准备向主项目提交补丁的时候,再把它衍合到 origin/master 里面。这样,维护者就不需要做任何整合工作,只需根据你提供的仓库地址作一次快进,或者采纳你提交的补丁。
请注意,合并结果中最后一次提交所指向的快照,无论是通过一次衍合还是一次三方合并,都是同样的快照内容,只是提交的历史不同罢了。衍合按照每行改变发生的次序重演发生的改变,而合并是把最终结果合在一起。
rebase 的其他操作
前面用了大篇幅来说明 rebase 的概念及实践,看下 rebase 的其他操作
onto 选项
--onto
剪切指定范围内提交节点,并在指向的分支上对这些节点执行变基操作
git rebase --onto base from to
将 (from,to] 范围内所有提交的节点在 base 指向的节点之后重建
看官方的例子了解下这个 onto 选项
你创建了一个特性分支
假设在接下来的一次软件发布中,你决定把客户端的修改先合并到主线中,而暂缓并入服务端软件的修改(因为还需要进一步测试)。你可以仅提取对客户端的改变(C8 和C9),然后通过使用 git rebase
的 --onto
选项来把它们在
$ git rebase --onto master server client
这基本上等于在说“检出
现在你决定把 git rebase [主分支] [特性分支]
命令会先检出特性分支
$ git rebase master server
rebase 冲突处理
在 rebase 的过程中,也许也会出现冲突,这时候 Git 会停止 rebase 让你解决冲突(这个过程和 merge 是一样的)
手动处理冲突之后,通过 add
命令暂存冲突文件
可以使用 --continue
选项,继续本次操作
git rebase --continue
或者使用 --abort
选项 放弃本次衍合操作
git rebase --abort
在进行衍合或合并操作时,Git 类似新建了一个匿名分支,当使用 --abort
选项时, Git 会切回原分支,丢弃匿名分支,放弃本次操作。
如果使用 Git 管理工具,当 merge 或者 rebase 操作有冲突需要处理时,都会有相关提示,比如有哪些文件有冲突,也有 continue 和 abort 操作供选择。
rebase 还有其他的一些选项,比如 -i
,后面学习重写历史的时候再做补充
rebase 的风险和使用场景
永远不要衍合哪些已经推送到公共仓库的更新
衍合的时候,实际上抛弃了一些已经存在的 commit 而创建了一些类似的但是不同的新 commit。如果把这个 commit(假设是 C6)推送到远程端,其他人在其基础上工作,然后你使用 git rebase
重写了C6 推送了 C6',那么别人不得不重新合并,而这次合并的内容和之前已经获取到的 C6 是一样,而再获取的时候就可能是 C6-C7-C6'-C8 ( C6 和 C6' 有着相同的内容(包括作者、提交说明等),C7 是其他人的提交,C8是其他人合并 C6' 产生的提交),这个历史记录会变的非常令人费解。
如果把衍合当成一种在推送之前清理提交历史的手段,而且仅仅衍合那些永远不会公开的 commit,那就不会有任何问题。如果衍合那些已经公开的commit,而与此同时其他人已经用这些 commit 进行了后续的开发工作,那就很麻烦了。
参考:Git 分支 - 变基;rebase