基于 patch 的版本控制系统是如何处理合并的 -- 一种新思路

基于 patch 的版本控制系统……

patch 是版本控制系统中最为渊远流长的概念之一,平常的各类操做中都须要跟它打个照面。好比 git diff 输出格式就是个 patch 文件,git cherry-pick 会把摘取的修改以 patch 的形式应用到目标分支上。此种例子比比皆是。不过须要指出的是,当前普遍使用的版本控制系统,好比 svn/git/hg,都是基于 snapshot 而不是 patch 的。基于 snapshot 的版本控制系统,以 snapshot 的方式存储当前版本。虽然这一类版本控制系统也会用到 patch,不过它们只有在须要时才计算出 patch 文件来。patch 是这一类版本控制系统的产物,而非基石。
(注意:切勿混淆 commit 和 snapshot 的概念,二者并不等价。Git 显然不会在每一个 commit 中存储对整个仓库的 snapshot,这么作太占空间。事实上,Git 的 commit 只包含指向 snapshot tree 的指针,参见:Git-内部原理-Git-对象git

天然存在基于 patch 的版本控制系统,好比 darcs 和 pijul,只是较为默默无闻。它们会是本文的主角。在基于 patch 的版本控制系统当中,当前版本由历史上一系列 patch 决定。须要在开头澄清的是,尽管本文着力于基于 patch 的版本控制系统的优点,但这并不表示我的认为基于 patch 的版本控制系统是更好的选择。基于 snapshot 的版本控制系统之因此流行,天然有它的优点所在。本文的目的是介绍基于 patch 的版本控制系统,尤为聚焦于这一类系统是如何处理合并的,旨在提供一种新思路。当咱们须要借鉴版本控制系统来解决一类问题时,除了参考 git 和 svn,有时候也能够看下 pijul。github

……是如何处理合并的

基于 patch 的版本控制系统,在跟 Git 比较时,一般会拿 git cherry-pick 说事。咱们知道,git cherry-pick 会拿出给定 commit 的修改,应用到当前版本上。初看上去,Git 提取了给定 commit 到当前版本上。然而仔细观察后会发现,cherry-pick 以后新增的 commit,跟给定的 commit,其 ID 并不相同。事实上,git cherry-pick 只是提取了给定 commit 的修改到当前版本上。换句话说,cherry-pick 的是改动的内容,而非 commit 自己。若是过后又合并了当初 git cherry-pick 的 commit,在 Git 的眼里,它认为一样的修改发生了两次。数据结构

假设如下的场景:在开发 feature 分支上,发现 master 分支上有一个 bug,影响到新功能的开发,因此在 feature 分支上修了,而后 cherry-pick 到 master 分支上来。后来因为业务上的变更,master 分支去掉了这个修复。当咱们合并 feature 分支后,这个修复又会从新出如今 master 分支。svn

在基于 patch 的版本控制系统没有这个问题,在它们眼里,不管在哪一个分支上,一样的修改都是同一个 patch。在合并时,它们比较的是 patch 的多寡,而非 snapshot 的异同。一样的道理,基于 patch 的版本控制系统,在处理 cherry-pickrevertblame 时,也会更加简单。版本控制

基于 snapshot 的版本控制系统,在合并时采用三路合并(three-way merge)。好比 Git 中合并就是采用递归三路合并。所谓的三路合并,就是 theirs(A) 和 ours(B) 两个版本先计算出公共祖先 merge_base(C),接着分别作 theirs-merge_base 和 ours-merge_base 的 diff,而后合并这两个 diff。当两个 diff 修改了一样的地方时,就会产生合并冲突。指针

若是是基于 patch 的版本控制系统,会把对方分支上多出来的 patch 添加到当前分支上。效果看上去就像 git rebase 同样。若是添加过程当中发生了冲突怎么办?code

patch 有两个重要的属性:对象

  1. 假设 patch B 依赖于 patch A,patch C 依赖于 patch B,B 能够在 A 和 C 之间自由移动而不改变最终结果。这种移动操做称之为 commute。
  2. 每一个 patch 都有一个对应的 inverse patch,能够把这个 path 引入的修改去掉。

darcs 在处理合并冲突时,会先添加若干个 inverse patch,回退到能够直接添加 patch 的时候。额外添加的 inverse patch 和以前有冲突的 patch 合并在一块儿,成为一个新的 patch。这其中可能还会有 commute 操做来移动 patch 到适当的位置。递归

一般对于差异较大的 Git 分支,不建议用 rebase 操做,由于 rebase 过程当中,可能会发生由于修复冲突带来的日后更多的冲突 - 冲突的滚雪球效应。darcs 的合并,也会有一样的问题,一个合并操做耗费的时间可能会赶上指数爆炸。three

pijul 经过引入名为有向图文件(directed graph file,如下简称为 digle)的数据结构,解决了这个问题。抛开我所不了解的具体细节不谈(对于细节感兴趣的读者,看这篇文章),由 digle 表示的数据结构可以保证不会发生合并冲突。这意味着,咱们能够用 digle 做为 patch 的内部实现,这样两个 patch 的合并就是两个 digle 的合并,而 digle 的合并是不会产生冲突的。这么一来,合并过程当中就不会有滚雪球效应了,咱们能够在最后把 digle 具象成实际的 patch 的时候,才开始解决合并冲突。

pijul 的 merge 有两个优势:

  1. 最终结果跟用了 git rebase 同样,可以保证历史是单线条的。
  2. 因为 merge 过程当中可以参考中间各个 patch 的信息,合并的效果理论上应该比简单粗暴的三路合并要好。

感兴趣的读者能够看看 pijul 的代码,深究其内部实现。

参考资料:

相关文章
相关标签/搜索