【翻译】 为什么你应该停止使用 Git rebase
Fredrik V.Morken
在使用Git工具好几年后, 我发现在自己的日常工作流里开始使用越来越多的Git 高级命令。在我发现Git rebase 命令后, 我很快地把它应用到哦的日常工作流里。对那些熟悉rebase工具强大之处的人来说,不断地使用rebase是多么的诱人啊!然而, 我很快意识到,使用rebase所带来的真正挑战在你最开始使用的时候并不是那么容易被察觉到。再开始之前,我先快速地概括一下git merge 和 git rebase的区别。
让我们先看一个基础的例子, 你想要把你开发的一个功能分支(feature)整合到主(master)分支上面。使用merge, 我们将创建一个新的commit g, g代表着将两个分支merge的事件。commit历史图表也清除的表明了在这个节点发生了什么,我们可以看到提交历史(commit history)图的“火车轨道”轮廓和很多大型git仓库的提交历史图很像。
我们还可以选择在merge前先rebase一下。这样的话, 功能分支上的commits会先被去掉, 接下来, 功能分支被重置到和master分支一致,然后, 之前被去掉的commits会被重新添加到功能分支的顶部。重新添加的commits一般和原来被去掉的commits内容一致, 但因为它们的父节点发生了变化, 所以它们有着不同的 SHA-1 keys。
我们现在把功能分支和master分支的共同父节点(基础节点)从b变成了c,也就是rebase了功能分支。现在合并功能分支和主分支是一个快速的合并了(不会创建merge节点, 提交历史也不会分叉), 因为功能分支上的所有提交都直接继承自主分支。
和直接merge方法相比,提交历史是线性的, 没有分叉。可读性的提升是我过去喜欢在merge前rebase分支的主要原因, 而且我也希望其他开发者也这样做。
然而, 这样做带来的挑战可能并不能被明显的观察到。
考虑一下这种情况, 一个依赖(比如一个函数、一个第三方库)在功能分支还在被使用,而在master分支上的最新提交上已经别移除。当功能分支被rebase到master上时,第一个重新应用的提交会破会你的build(即项目不能成功启动), 但,只要没有合并冲突, rebase过程会持续下去不被打断。这样第一个提交就有的错误会一直保留到接下来的提交上, 导致一系列的有bug的提交节点。
这个错误一般只会在rebase过程结束后才能被发现,通常被一个新的bugfix 提交g来修复。
如果你在rebase过程中遇到了冲突,Git会暂停在这个冲突的提交,在继续之前让你解决冲突。在rebase一长串提交的过程中来解决冲突通常是很困惑的,也很容易引入其它的错误。
在rebase过程中引入错误是另一个令人头疼的问题。这种情况下, 新的错误在你重写提交历史的时候被引入,它们会伪装成第一个提交所引入的bug(实际上不是)。特别的, 这将另用git bisect调试bug更加困难。作为一个例子,考虑如下功能分支,让我们假设在f节点的提交引入了一个bug。
你可能直到几周后该功能分支被合并到master分支后才能发现这个bug。 为了找到引入这个bug的提交,你可能要浏览上百个提交。这个过程可以用测试脚本来自动化定位这个bug,通过git bisect来自动运行, 命令是 git bisect run <yourtest.sh>
。
bisect 会执行一个二分搜索来浏览提交历史来定位引入的bug。在下面这个例子里,它成功的找到了引入这个bug的第一个错误提交,因为所有的坏提交(使项目不能正常运行)都包含我们在找的这个bug。
然而, 如果我们在rebase的过程中引入了新的坏提交(比如d和e), 那么,bisect 会遇到麻烦。在这种情况下,我们希望git定位到f才是引入bug的坏提交,但它却错误的定位到了d, 因为d包含了一些其它的错误使得测试脚本通不过。
这个问题比它最开始看起来的要大得多。
我们究竟为什么要使用git呢?因为它使我们追踪代码中bug的最重要的工具。git就是我们安全网。而使用rebase,我们给了git更少的优先级以为了得到一个线性的提交历史。
不久前, 我不得不bisect几百个提价来追踪一个系统中的bug。这个坏提交最终被定为在一长串提交的中间,而这个坏提交是由于一个同事在错误的执行rebase引入的。这个完全没必要而且可避免的错误最终导致我花了将近一天的时间来追踪这个提交。
所以, 我们怎么才能在rebase的过程中避免引入坏提交呢? 一个方法是先让rebase进行完,然后全方位的测试代码来及时发现和定位bug, 然后回到提交历史去修改掉。另一种方法是,在rebase的每一步都先暂停,测试bug,及时修复,然后再继续。
这真是一个笨重而又容易出错的过程,而这么做的唯一原因是我们想让提交历史变成线性的。还有一个更简单更好的方法吗?
有! Git merge啊。它很简单、而且是一步操作,所有的冲突都在一个提交里解决。新的merge提交清楚的标记了我们分支在这里整合,而且我们的提交历史也被完整的保留,和他们真实发生的一样。
保持提交历史的真实的重要性不应该被低估。通过rebase, 你是在对自己和团队撒谎。你假装这些提交是今天写的,而实际上它们是昨天写的(译者:实际上提交历史中的时间点显示的还是rebase前的时间点),而且基于另一个不同的提交。你把原始提交中的内容拿了出来,导致分辨不出真实发生了什么。你能保证这些代码能编译通过吗?你能保证这些提交信息说得通吗?你也许觉得你在整洁你的提交历史, 但结果却恰恰相反。
很难说清楚你的功能分支会给你的代码仓库带来什么样的错误和挑战,但可以确定的是,一个真实的提交历史会比重写的(假的)历史要好。
那么, 人们rebase分支的动机是什么呢?
我觉得结论是:虚荣。rebase纯粹是一个审美操作,它呈现出一个清洁的提交历史给开发者们。但是,这在技术上和功能上都站不住脚。
非线性的提交历史, “火车轨道”, 可能看起来有点吓人。最开始我也这么觉得,但实际上真没必要害怕它们。有很多华丽的工具可以分析和可视化git的提交历史,既有图形化界面的也有命令行的。这个图表包含着丰富的提交信息(什么时候提交的, 提交了什么),如果我们把它们可视话了,我们就得不到了。
git被发明也鼓励非线性提交历史,如果不考虑这点,你也许最好用更简单一点的VCS工具,比如那些只支持线性提交历史的。
我觉得你应该保证你的提交历史的真实性。习惯于用工具分析它,而不是受到诱惑而重写它。重写获得的好处是很少的,但风险却很大。下次你用bisect来追踪一个难搞定的bug时会来感谢我的。
这篇文章是基于我在2016年挪威的JavaZone的演讲,视频地址是:https://vimeo.com/182068915
译者: 文章原文地址:点我