Git 进阶 - 衍合 rebase

将一个分支合并到另一个分支有两种,一种是大多都很熟悉的 merge(合并),另一种就是本篇要介绍的 rebase(衍合)。

看下本文纲要

已经有 merge 了,为什么需要 rebase ?我们先跟着官方文档学习下 rebase 的基本概念

rebase 是做什么的?

(如果你大概知道 rebase 是做什么的,可以直接跳到第二趴,实战 rebase)

在了解 rebase 之前,先温习下 merge 的过程

假设现在基于 分支,检出一个 分支

然后在这个分支 做一些修改,生成两个提交(C3 和 C4),同时也有其他人在 分支做了提交(C5 和 C6 ),那么在同一位置(C2) 和 hotfix 两个分支分别前进了

使用 merge 合并分支

此时拉取 分支内容并合并到 分支中,Git 会把两个分支最新的快照(C3~C6)以及他们共同的祖先(C2)进行合并,然后就形成了一次新的合并提交(C7)

: git merge master

如果想让 分支看起来没有经过任何合并一样,就可以使用 git rebase

使用 rebase 合并

rebase 有的翻译成衍合,有的直接翻译成变基,变基就很好理解了,就是重新设定基底。

$ git checkout hotfix
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: ***

git rebase 这个命令会将 分支里的提交(C3、C4)取消,保存成临时文件,然后把 更新到最新的 分支,最后把保存的这些内容应用到 分支上,这个过程就是衍合。

rebase 原理 衍合前回到两个分支(所在的分支和想要衍合的分支)的共同祖先,提取你所在分支每次提交时产生的差异(diff),把这些差异分别保存到临时文件里,然后从当前分支转换到你需要衍合的分支,依照顺序使用每一个差异补丁文件。

更新后,它会指向新创建的提交,而老的提交会被丢弃

使用 git rebase 之后产生的历史会是如下

: git rebase master

还是有点乱吗?再通俗点呢,原来的「基点」是 C2,现在把「基点」改变到要衍合的分支处,就是把「基点」搞到 C6 哪儿去,然后把改变的内容,应用上去。然后分支历史就被改写啦。

实战体验 rebase

概念和效果清楚了,实战一下,推荐一个学习 Git 实践的网站,这个工具非常有意思,可以用来学习模拟基本的 Git 操作,戳这里学习

模拟以下操作过程

  1. :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' 是两个不同的提交(会产生不同的历史,但是内容是一样的)。

  1. 再做一次衍合,将 b2 也合并过来
    : git rebase b2
$ git rebase b2
First, rewinding head to replay your work on top of it...
Applying: C2
Applying: C4

还是会提取 分支 和 C1 的 差异,产生了 C2' 和 C4'',转到 分支再把 diff 应用进去。


  1. 此时如果 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 选项

你创建了一个特性分支 来给服务器端代码添加一些功能,然后提交 C3 和 C4。然后从 C3 的地方再增加一个 分支来对客户端代码进行一些修改,提交 C8 和 C9。最后,又回到 分支提交了 C10。

假设在接下来的一次软件发布中,你决定把客户端的修改先合并到主线中,而暂缓并入服务端软件的修改(因为还需要进一步测试)。你可以仅提取对客户端的改变(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

你可能感兴趣的:(Git 进阶 - 衍合 rebase)