不要再用Git rebase来实现线性提交历史了
Git为非线性历史而生, 并鼓励非线性历史. 如果这让你不喜欢, 那你最好使用只支持线性历史记录的更简单的VCS
使用Git工作这么多年后, 我在日常工作流程中逐渐使用了越来越多的高级 Git 命令. 转而 Git rebase 后不久, 我很快就把它纳入了日常工作流程. 熟悉rebase的人都知道它是多么强大的工具, 也知道一直使用它是多么诱人. 不过, 我很快就发现, rebase带来了一些挑战, 这些挑战在刚开始使用时并不明显. 在介绍这些挑战之前, 我先简要回顾一下merge和rebase之间的区别.
首先, 让我们考虑一个基本的例子: 你想将一个特性分支与主分支整合. 通过merge, 我们会创建一个新的提交g
, 表示两个分支的merge. 提交图清楚地显示了所发生的一切, 我们可以看到在大型 Git 仓库中熟悉的"火车轨道"图的轮廓.
merge示例
另外, 我们也可以在merge之前rebase提交. 提交被移除后, feature
分支会被重置为master
分支, 然后这些提交会被重新应用到feature
分支之上. 这些重新应用的提交的差异通常与原始提交完全相同, 但它们的父提交不同, 因此 SHA-1 密钥也不同.
rebase示例
现在, 我们将feature
的基本提交从b
改为了c
, 这就是字面上的rebase. 将feature
合并到master
现在是快进merge, 因为feature
上的所有提交都是master
的直接后代.
快进merge示例
与merge方法相比, merge后的历史是线性的, 没有发散分支. 可读性的提高是我喜欢在合并前去rebase分支的原因, 我希望其他开发者也能如此.
不过, 这种方法也存在一些不明显的挑战.
考虑这样一种情况: feature
上仍在使用的依赖关系在master
上已被移除. 当feature
被rebase到master
上时, 第一次重新应用的提交会破坏你的构建, 但只要没有合并冲突, rebase过程就会不间断地进行. 第一次提交产生的错误将在后续所有的提交中继续存在, 从而导致一连串的错误提交.
这种错误只有在rebase过程结束后才会被发现, 通常会通过在上面应用新的错误修复提交g
来修复.
rebase失败示例
如果在rebase过程中发生冲突, Git 会在冲突提交处暂停, 让你修复冲突后再继续. 在对一长串提交进行rebase的过程中解决冲突通常会令人困惑, 也很难做到正确无误, 同时还是潜在错误的另一个来源.
如果在rebase过程中引入错误, 问题会更大. 这样, 在重写历史记录时就会引入新的错误, 而这些错误可能会掩盖历史记录首次编写时引入的真正错误. 尤其是, 这将增加使用 Git bisect 的难度, 而 Git bisect 可以说是 Git 工具箱中最强大的调试工具. 以下面的特性分支为例. 假设我们在分支末尾引入了一个 Bug:
分支末尾引入了一个 bug
你可能会在该分支合并到master
数周后才发现这个错误. 要找到引入该 bug 的提交, 你可能需要搜索数十或数百个提交. 可以编写一个脚本来测试 bug 是否存在, 然后通过 Git bisect 自动运行该脚本, 使用命令git bisect run <yourtest.sh>
.
Bisect 会在历史记录中执行折半搜索, 找出引入 bug 的提交. 在下面的示例中, 它成功找到了第一个有问题的提交, 因为所有中断的提交都包含了我们要找的 bug.
成功的Git bisect示例
另一方面, 如果我们在rebase过程中引入了额外的错误提交(这里是d
和e
), bisect 就会遇到麻烦. 在这种情况下, 我们希望 Git 识别出提交f
是错误的, 但它却错误地识别出了提交d
, 因为它包含了其他一些破坏测试的错误.
Git Git bisect失败示例
这个问题比一开始看起来要严重得多.
我们为什么要使用 Git? 因为它是我们追踪代码错误源头的最重要工具. Git 是我们的安全网. 通过rebase, 我们降低了它的优先级, 转而追求线性历史.
前不久, 我不得不对几百个提交进行bisect, 以追踪系统中的一个错误. 有问题的提交位于一长串无法编译的提交中间, 原因是一位同事执行了错误的rebase. 这个完全可以避免的不必要错误导致我多花费了将近一天的时间来追踪这个提交.
那么, 我们该如何避免rebase过程中的提交链断裂呢? 一种方法是让rebase过程结束, 测试代码以发现任何错误, 然后回溯历史以修复引入的错误. 为此, 我们可以使用交互式rebase.
另一种方法是让 Git 在rebase过程的每一步都暂停, 测试是否有错误, 并在继续之前立即修复.
这样做既麻烦又容易出错, 而且这样做的唯一目的是为了实现线性历史. 有没有更简单、更好的方法呢?
有, Git merge. 这是一个简单, 一步到位的过程, 所有冲突都在一次提交中解决. 由此产生的merge提交会清楚地标示出分支间的整合点, 而我们的历史则会描述实际发生了什么, 以及何时发生的.
保持历史真实, 其重要性不容低估. 但是通过rebase来做, 你是在欺骗自己, 也是在欺骗团队. 你假装提交是今天写的, 而实际上它们是昨天根据另一个提交写的. 你将提交从其原始上下文中剥离出来, 掩盖了实际发生的情况. 你能确定代码能够构建吗? 你能确定提交信息仍然有意义吗?你可能认为自己在清理和澄清历史, 但结果很可能恰恰相反.
我们无法预知未来会给代码库带来哪些错误和挑战. 但可以肯定的是, 真实的历史会比重写(或伪造)的历史更有用.
但到底是什么在驱动人们rebase分支呢?
我得出的结论是虚荣心作祟. rebase分支纯粹是一种审美操作. 作为开发者, 我们会被表面上干净的历史所吸引, 但从技术和功能角度来看, 这都是不合理的.
非线性历史记录.
非线性历史图表, 即"火车轨道", 可能会让人望而生畏. 我一开始也是这么觉得的, 但没必要害怕它们. 有很多强大的工具可以分析复杂的 Git 历史并将其可视化, 有基于 GUI 的, 也有基于 CLI 的. 这些图表包含了关于发生了什么以及何时发生的宝贵信息, 而我们将其线性化并不会带来任何好处.
Git为非线性历史而生, 并鼓励非线性历史. 如果这让你不喜欢, 那你最好使用只支持线性历史记录的更简单的VCS.
我认为你应该保持历史的真实性. 熟悉分析历史的工具, 不要被改写历史的诱惑所迷惑. 重写的回报微乎其微, 但风险却很大. 下一次, 当你在历史记录中通过折半搜索追踪一个鬼鬼祟祟的错误时, 你就会感谢我了.
转载自:https://juejin.cn/post/7270345042580750392