Git为非线性前史而生, 并鼓舞非线性前史. 假如这让你不喜欢, 那你最好运用只支持线性前史记录的更简略的VCS

运用Git作业这么多年后, 我在日常作业流程中逐渐运用了越来越多的高级 Git 指令. 转而 Git rebase 后不久, 我很快就把它纳入了日常作业流程. 了解rebase的人都知道它是多么强壮的东西, 也知道一向运用它是多么诱人. 不过, 我很快就发现, rebase带来了一些应战, 这些应战在刚开始运用时并不显着. 在介绍这些应战之前, 我先简要回忆一下merge和rebase之间的差异.

首先, 让咱们考虑一个根本的比如: 你想将一个特性分支与主分支整合. 经过merge, 咱们会创立一个新的提交g, 表示两个分支的merge. 提交图清楚地显示了所产生的一切, 咱们可以看到在大型 Git 库房中了解的”火车轨迹”图的概括.


merge示例

另外, 咱们也可以在merge之前rebase提交. 提交被移除后, feature分支会被重置为master分支, 然后这些提交会被从头运用到feature分支之上. 这些从头运用的提交的差异一般与原始提交完全相同, 但它们的父提交不同, 因此 SHA-1 密钥也不同.

0_HAd2rzmvSR90Gv3H.gif
rebase示例

现在, 咱们将feature的根本提交从b改为了c, 这就是字面上的rebase. 将feature兼并到master现在是快进merge, 由于feature上的一切提交都是master的直接后代.

0_tkfAj-Ik4LYezpEu.gif
快进merge示例

与merge办法比较, merge后的前史是线性的, 没有发散分支. 可读性的提高是我喜欢在兼并前去rebase分支的原因, 我希望其他开发者也能如此.

不过, 这种办法也存在一些不显着的应战.

考虑这样一种状况: feature上仍在运用的依靠关系在master上已被移除. 当feature被rebase到master上时, 第一次从头运用的提交会损坏你的构建, 但只要没有兼并抵触, rebase进程就会不间断地进行. 第一次提交产生的过错将在后续一切的提交中持续存在, 从而导致一连串的过错提交.

这种过错只有在rebase进程完毕后才会被发现, 一般会经过在上面运用新的过错修正提交g来修正.

0_JLxRFD9dbxrvaOgI.gif
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进程中引进了额定的过错提交(这里是de), 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.

我认为你应该坚持前史的实在性. 了解分析前史的东西, 不要被改写前史的诱惑所利诱. 重写的报答微乎其微, 但危险却很大. 下一次, 当你在前史记录中经过减半搜索追寻一个鬼头鬼脑的过错时, 你就会感谢我了.