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-pick
,revert
和 blame
时,也会更加简单。版本控制
基于 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 有两个重要的属性:对象
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 有两个优势:
git rebase
同样,可以保证历史是单线条的。感兴趣的读者能够看看 pijul 的代码,深究其内部实现。
参考资料: