[译] 为什么你应该停止使用 Git rebase 命令

5,341 阅读5分钟
导语:作者阐释了 git rebase 命令的原理和缺陷,rebase 会导致线性的历史没有分支。此外如果 rebase 过程中发生了冲突,还可能会引入更多的问题。作者推荐使用 git merge。

用了几年的 Git 后 ,我发现使用越来越多的先进的 Git 命令已经逐渐成为我日常工作流的一部分了。在我发现 Git rebase 后,我马上就把它收录到我日常工作流中。熟悉 rebase 的人都知道它的强大之处,并且它总是在诱惑别人使用它。然而,我很快就发现了在开始使用 rebase 后带来的一些不是很明显的挑战。在阐述挑战之前,我将快速地讲述一下 merge 和 rebase 之间的差别。

我们首先来思考一下将 feature 分支合并到 master 分支的基础案例。通过 merge 的话,我们创建了一个新的提交g表示两个分支的合并。提交图清晰的展现了发生了什么,我们可以从更大的 Git-repos 中看到熟悉的“火车轨道”轮廓。

Example of merging

同样地,我们可以在 merge 之前选择 rebase。提交会被移除,并且 feature 分支被重置到 master 分支,feature 分支上的提交被重新应用到 master。差别在于这些重新应用的提交通常是原始的副本,它们的 SHA-1 密钥和原来的提交不一样。

Example of rebasing

我们现在将 feature 的基础提交从b变为了c,这就是 rebase 的意思。将 feature 合并到 master 是一个快进合并,因为在 feature 上的所有提交都是 master 的直接子代。

Example of fast-forward merging

和 merge 的方法比较起来,rebase 导致分支的历史都是线性的。我以前更喜欢在合并之前 rebase 分支的原因在于提高了可读性,我认为其他的开发者应该也是这种情况。

但是,这种方式带来了一些不是很明显的挑战。

考虑一下这种情况,有一个依赖在 master 上被移除了,但在 feature 上还在使用。当 feature 分支 rebase 到 master 上时,第一个重新应用的提交会打破你的构建,但只要没有合并冲突,rebase 就不会被中断。从第一个提交出现的错误会保留在随后的所有提交中,这导致了一个链式的错误提交。

这个错误只会在 rebase 完成后才会被发现,并且通常会在顶部增加一个修复 bug 的提交g

Example of failed rebasing

如果你在 rebase 过程中出现了冲突,Git 将会暂停在冲突的提交上,允许你在开始之前解决冲突。在一系列的提交中间解决冲突通常会让人困惑,难以改正,并且可能会导致额外的错误。

引入错误是在 rebase 过程中发生的。这样,当你重写历史的时候,新的错误就会被引入,它们可能会掩盖第一次写入历史时造成的真正的错误。尤其是,当我们使用 Git bisect 时会变得更加困难,Git bisect 可以说是 Git 工具箱中最强大的调试工具了。例如,思考一下下面的 feature 分支,我们假设在分支的末端引入了一个错误。

A branch with bugs introduced towards the end

为了找到引入错误的提交,你可能会搜索几十个甚至上百个提交。这个过程可以通过编写测试错误存在的脚本来自动执行,并通过 Git bisect 使用命令git bisect run <yourtest.sh>来运行。

Bisect 会通过二分查找整个历史,识别出引入 bug 的提交。在上面展示的案例中,它成功地找到第一个错误的提交,因为所有的有问题的提交包含着我们正在寻找的真正的错误。

Example of successfull Git bisect

另一方面,如果我们在 rebase 过程中引入了额外的错误提交(下图的de),bisect 将会遇到麻烦。这个例子中,我们希望 Git 识别出f提交,但它会错误地识别出d,因为它包含了其他的错误打破了测试。

Example of a failed Git bisect

这个问题比看起来更大。

我们为什么要使用 Git?

因为它是我们追踪我们代码中错误来源最重要的工具。Git 是我们的安全网。通过 rebase 虽然能够达成线型历史,但我们会给予较少的优先权。

回顾一下,我必须使用 bisect 追踪系统中上百个提交。这个错误的提交在一条未编译的提交链中间,因为一个错误的 rebase。这个不必要的并且今天可以避免的错误导致我花了一天的时间来追踪提交。

所以,我们怎样才能避免在 rebase 过程中出现错误的提交链呢?一个方法是在 rebase 结束后,测试代码来发现错误,然后回到我们引进错误的地方修复它。另一个方法是,在 rebase 过程中暂停每一个步骤,在继续处理之前测试并修复它。

这是一个笨重的,并且容易犯错的过程。这么做的唯一结果是获得一个线型的历史。还有更简单,更好的方式吗?

答案是:Git merge。它是一个简单的,一步到位的过程,所有的冲突都可以在一个单一的提交中解决。合并的提交清晰地显示了我们的分支之间的交互点,并且我们的历史叙述了实际上发生的和什么时候发生的。

综上所述,推荐使用 merge 而不是 rebase。保持我们历史的真实性是不可低估的。至于人们为什么要使用 rebase,可能是出于虚荣心,科科。