责任编辑译者:lzaneli,百度 TEG 前端合作开发技师
“分拆前文档还在的,分拆后就不见踪影了”、“我碰到 Git 分拆的 bug 了” 是这段话时常听见不然,但吗是 Git 的 bug 么?也许而已你的预期不对。责任编辑透过传授sert分拆和 Git 的分拆思路,step by step 如是说 Git 是怎么做两个分拆的,让我们对 Git 的分拆结论有两个精确的市场预期,因此防止出现分拆交通事故。
本系列产品透过两篇该文来详尽如是说译者的 Git采用心得体会。
首篇该文见:
故事情节天数
在已经开始节录以后,嘿嘿听呵呵那个故事情节。
如下表所示图,小华从结点 A 拉了两条 dev 组成部分出,在结点 B 中追加了两个文档 http.js,因此分拆到 master 组成部分,分拆结点为 E。那个这时候辨认出会引发圣戈当斯区 bug,急忙退回那个分拆,追加两个 revert 结点 E。过了两天小华竭尽全力在 dev 组成部分下面合作开发追加了两个文档 main.js,并在那个文档中 import 了 http.js 里头的方法论,在 dev 组成部分下面所有人运转恒定。Saverdun他将这时的 dev 组成部分分拆到 master 这时候却辨认出,http.js 文档不见踪影了,引致 main.js 里头的方法论运转收起了。但此次分拆并没任何人武装冲突。他又得再次做了呵呵 revert,因此迷惘的揣测是 Git 的 bug。
这段话时常听见不然:
—— ”分拆前文档还在的,分拆后就不见踪影了“
—— ”我碰到 Git 的 bug 了“
坚信许多老师或多或少在不熟悉 Git 分拆思路的这时候都会出现过类似下面的事情,明明在分拆前文档还在的,为什么分拆后文档就不在了么?一度还揣测是 Git 的 bug。这篇该文的目的就是想跟我们讲清楚 Git 是怎么去分拆组成部分的,以及一些底层的基础概念,从而防止出现如故事情节中的问题,并对 Git 的分拆结论有两个精确的市场预期。
如何分拆两个文档
在看怎么分拆两个组成部分以后,我们嘿嘿看呵呵怎么分拆两个文档,因为两个文档的分拆是两个组成部分分拆的基础。
我们应该都听说过“sert分拆”那个词,不知道我们有没思考过为什么两个文档的分拆需要sert分拆,只有二向是否可以自动完成分拆。如下表所示图
很明显答案是不能,如上图的例子,Git 没法确定这一行代码是我修改的,还是对方修改的,或者以后就没这行代码,是我们俩同时追加的。这时 Git 没办法帮我们做自动分拆。
所以我们需要sert分拆,所谓sert分拆,就是找到两个文档的两个分拆 base,如下表所示图,这样子 Git 就可以很清楚的知道说,对方修改了这一行代码,而我们没修改,自动帮我们分拆这两个文档为 Print(“hello”)。
接下来我们了解呵呵什么是武装冲突?武装冲突简单的来说就是sert分拆中的三方都互不相同,即参考分拆 base,我们的组成部分和别人的组成部分都对同个地方做了修改。
Git 的分拆思路
了解完怎么分拆两个文档之后,我们来看两个采用 git merge 来做组成部分分拆。如上图,将 master 组成部分分拆到 feature 组成部分上,会追加两个 commit 结点来记录此次合并。
Git 会有许多分拆思路,其中常见的是 Fast-forward、Recursive 、Ours、Theirs、Octopus。下面分别如是说不同分拆思路的基本原理以及应用场景。默认 Git 会帮你自动挑选合适的分拆思路,如果你需要强制指定,采用git merge -s <思路名字>
了解 Git 分拆思路的基本原理可以让你对 Git 的分拆结论有两个精确的市场预期。
Fast-forward
Fast-forward 是最简单的一种分拆思路,如上图中将 some feature 组成部分分拆进 master 组成部分,Git 只需要将 master 组成部分的指向移动到最后两个 commit 结点上。
Fast-forward 是 Git 在分拆两个没分叉的组成部分时的默认行为,如果不想要这种表现,想明确记录下每次的分拆,可以采用git merge –no-ff。
Recursive
Recursive 是 Git 组成部分分拆思路中最重要也是最常用的思路,是 Git 在分拆两个有分叉的组成部分时的默认行为。其算法可以简单描述为:递归寻找路径最短的唯一共同祖先结点,然后以其为 base 结点进行递归sert分拆。说起来有点绕,下面透过例子来解释。
如下表所示图这种简单的情况,圆圈里头的英文字母为当前 commit 的文档内容,当我们要分拆中间两个结点的这时候,找到他们的共同祖先结点(左边第两个),接着进行sert分拆得到结论为 B。(因为分拆的 base 是“A”,下图靠下的组成部分没修改内容仍为“A”,下图靠上的组成部分修改成了“B”,所以分拆结论为“B”)。
但现实情况总是复杂得多,会出现历史记录链互相交叉等情况,如下表所示图:
当 Git 在寻找路径最短的共同祖先结点的这时候,可以找到两个结点的,如果 Git 选用下图这两个结点,那么 Git 将无法自动的分拆。因为根据sert分拆,这里是是有武装冲突的,需要手动解决。(base 为“A“,分拆的两个组成部分内容为”C“和”B“)
而如果 Git 选用的是下图那个结点作为分拆的 base 时,根据sert分拆,Git 就可以直接自动分拆得出结论“C”。(base 为“B“,分拆的两个组成部分内容为”C“和”B“)
作为人类,在那个例子里头我们很自然的就可以看出分拆的结论应该是“C”(如下表所示图,结点 4、5 都已经是“B”了,结点 6 修改成“C”,所以分拆的市场预期为“C”)
那怎么保证 Git 能够找到正确的分拆 base 结点,尽可能的减少武装冲突呢?答案就是,Git 在寻找路径最短的共同祖先结点时,如果满足条件的祖先结点不唯一,那么 Git 会竭尽全力递归往下寻找直至唯一。还是以刚刚那个例子图解。
如下表所示图所示,我们想要分拆结点 5 和结点 6,Git 找到路径最短的祖先结点 2 和 3。
因为共同祖先结点不唯一,所以 Git 递归以结点 2 和结点 3 为我们要分拆的结点,寻找他们的路径最短的共同祖先,找到唯一的结点 1。
接着 Git 以结点 1 为 base,对结点 2 和结点 3 做sert分拆,得到两个临时结点,根据sert分拆的结论,那个结点的内容为“B”。
再以那个临时结点为 base,对结点 5 和结点 6 做sert分拆,得到分拆结点 7,根据sert分拆的结论,结点 7 的内容为“C”
至此 Git 完成递归分拆,自动分拆结点 5 和结点 6,结论为“C”,没武装冲突。
Recursive 思路已经被大量的场景证明它是两个尽量减少武装冲突的分拆思路,我们可以看到有趣的一点是,对于两个分拆组成部分的中间结点(如上图结点 4,5),只参与了 base 的计算,而最终或者说被sert分拆拿来做分拆的结点,只包括末端以及 base 结点。
需要注意 Git 而已采用这些思路尽量的去帮你减少武装冲突,如果武装冲突不可防止,那 Git 就会提示武装冲突,需要手工解决。(也就是或者说意义上的武装冲突)。
Ours & Theirs
Ours 和 Theirs 这两种分拆思路也是比较简单的,简单来说就是保留双方的历史记录,但完全忽略掉这一方的文档变更。如下表所示图在 master 组成部分里头执行git merge -s ours dev,会产生蓝色的这两个分拆节点,其内容跟其上两个结点(master 组成部分方向上的)完全一样,即 master 组成部分分拆前后项目文档没任何人变动。
而如果采用 theirs 则完全相反,完全抛弃掉当前组成部分的文档内容,直接采用对方组成部分的文档内容。
这两种思路的两个采用场景是比如现在要实现同一功能,你同时尝试了两个方案,分别在组成部分是 dev1 和 dev2 上,最后经过测试你选用了 dev2 那个方案。但你不想丢弃 dev1 的这样一个尝试,希望把它合入主干方便后期查看,那个这时候你就可以在 dev2 组成部分中执行git merge -s ours dev1。
Octopus
这种分拆思路比较神奇,一般来说我们的分拆结点都只有两个 parent(即分拆两条组成部分),而这种分拆思路可以做两个以上组成部分的分拆,这也是 git merge 两个以上组成部分时的默认行为。比如在 dev1 组成部分上执行git merge dev2 dev3。
他的两个采用场景是在测试环境或预发布环境,你需要将多个合作开发组成部分修改的内容分拆在一起,如果不用那个思路,你每次只能分拆两个组成部分,这样就会引致大量的分拆结点产生。而采用 Octopus 这种分拆思路就可以用两个分拆节点将他们全部分拆进来。
Git rebase
git rebase 也是一种时常被用来做分拆的方法,其与 git merge 的最大区别是,他会更改变更历史对应的 commit 结点。
如下表所示图,当在 feature 组成部分中执行 rebase master 时,Git 会以 master 组成部分对应的 commit 结点为起点,追加两个全新的 commit 代替 feature 组成部分中的 commit 结点。其原因是新的 commit 指向的 parent 变了,所以对应的 SHA1 值也会改变,所以没办法复用原 feature 组成部分中的 commit。(这句话的理解需要这篇该文的基础知识)
对于分拆这时候要采用 git merge 还是 git rebase 的争论,我个人的看法是没银弹,根据团队和项目习惯选择就可以。git rebase 可以给我们带来清晰的历史记录,git merge 可以保留真实的提交天数等信息,因此不容易出问题,处理武装冲突也比较方便。唯一有一点需要注意的是,不要对已经处于远端的多人共用组成部分做 rebase 操作。
我个人的两个习惯是:对于本地的组成部分或者确定只有两个人采用的远端组成部分用 rebase,其余情况用 merge。
rebase 还有两个非常好用的东西叫 interactive 模式,采用方法是git rebase -i。可以实现压缩几个 commit,修改 commit 信息,抛弃某个 commit 等功能。比如说我要压缩下图 260a12a5、956e1d18,将他们与 9dae0027 分拆为两个 commit,我只需将 260a12a5、956e1d18 前面的 pick 改成“s”,然后保存就可以了。
限于篇幅,git rebase -i 还有许多实用的功能暂不展开,感兴趣的老师可以自己研究呵呵。
总结
现在我们再来看呵呵该文开头的例子,我们就可以理解为什么最后一次 merge 会引致 http.js 文档不见踪影了。根据 Git 的分拆思路,在分拆两个有分叉的组成部分(上图中的 D、E‘)时,Git 默认会选择 Recursive 思路。找到 D 和 E’的最短路径共同祖先结点 B,以 B 为 base,对 D,E‘做sert分拆。B 中有 http.js,D 中有 http.js 和 main.js,E’中什么都没。根据sert分拆,B、D 中都有 http.js 且没变更,E‘删除了 http.js,所以分拆结论就是没 http.js,没武装冲突,所以 http.js 文档不见踪影了。
那个例子理解基本原理之后解决方法有许多,这里简单带过两个方法:1. revert 结点 E之后,这时的 dev 组成部分要抛弃删除掉,再次从 E结点拉出组成部分竭尽全力工作,而不是在原 dev 组成部分上竭尽全力合作开发结点 D;2. 在结点 D 分拆回 E’结点时,先 revert 呵呵 E‘节点生成 E’‘(即 revert 的 revert),再将结点 D 分拆进来。
Git 有许多种组成部分分拆思路,责任编辑如是说了 Fast-forward、Recursive、Ours/Theirs、Octopus 分拆思路以及sert分拆。掌握这些分拆思路以及他们的采用场景可以让你防止出现一些分拆问题,并对分拆结论有两个精确的市场预期。
希望这篇该文对我们有用,感兴趣的老师可以逛一逛我的博客 http://www.lzane.com 或看看我的其他该文。
参考
sert分拆 http://blog.plasticscm.com/2016/02/three-way-merging-look-under-hood.htmlRecursive 分拆【视频】https://www.youtube.com/watch?v=Lg1igaCtAck书籍 Scott Chacon, Ben Straub – Pro Git-Apress (2014)书籍 Jon Loeliger, Matthew McCullough – Version Control with Git, 2nd Edition – O’Reilly Media (2012)更多干货尽在百度控制技术,官方微信交流群已建立,交流讨论可加:Journeylife1900(备注百度控制技术) 。