Skip to content

darcs vs git

问题

CITA-Cloud设计的一个目标,或者说是我个人的一个执念,就是希望能够将区块链拆分成多个现有软件的组合。这样就能复用已有的软件,提高开发速度,提升区块链的产品质量。

上一篇文章提到分叉树和交易池跟 git 的一些操作很像,因此花了一些时间调研版本控制软件,看是否能直接复用。

版本控制软件

版本控制软件发展历史很悠久,相关的软件也非常多。按照发展的顺序可以分为:

  1. 中心化的版本控制软件,比如 ClearCaseSVN 。它们的特点是有个中心化的服务器,一些比较大的企业还专门配有管理人员。好处是可以非常方便的设置权限,可以细化到目录,如果你没有仓库下某个目录的权限,那么你同步代码之后,本地相应的目录是空的。它的协作方式是加锁,修改代码之前要先checkout相关的文件,提交修改,最后要有个checkin操作,在这期间别人是无法修改同样的文件的。
  2. 分布式的版本控制软件,比如 gitdarcs 等等。随着开源软件的发展,软件开发中的大规模协作逐渐成为主流,版本控制软件也随之进化为分布式的。它们的特点是没有中心化的服务器,每个人 clone 了之后,本地都是一个完整的仓库,特别符合上一篇文章提到的 Local First 原则。

在分布式的版本控制软件领域里,相关软件其实很多,但是现在基本上已经形成git一家独大的情形。作为对比的另外一方,darcs其实也挺落魄的,它是用 Haskell 开发的,因此在 Haskell 圈子里面用的挺多的,GHC原来也是用darcs的,但是后来改用 git 了。但是 darcs 凭借着优雅的理论,还是有一些忠诚的拥趸。

区别

darcsgit 最大的区别是, git 是基于 snapshot 的,而 darcs 是基于 patch 的。

用户修改一个文件之后, git 会保存修改前和修改后的两份文件,而 darcs 则保存的仅仅是通过 diff 算法计算出的 patch

git的做法比较简单粗暴,但是性能比较好,是典型的工程师做法。而 darcs 则比较有学术气息,背后有一套完善的Patch Theory

使用层面,两者还是比较类似的,很多子命令都是对应的。 darcs 的使用可以参考教程

比较突出的区别是 darcs 没有分支的概念,一个仓库就是一系列的 patch 。但是可以通过多个仓库来模拟多个分支,本地的多个仓库之间可以相互 pull ,而且是直接用文件系统路径,也挺方便的。

刚才提到 darcs 的一个仓库就是一系列的 patch ,但是这些 patch 之间不是我们直觉认为的按照提交顺序排成一条线,而是按照相互间的依赖顺序形成一个 DAG

我们来看一个具体的例子:

$ darcs show dependencies            
digraph {
   graph [rankdir=LR];
   node [imagescale=true];
   "18522b92" [label="add A"]
   "75dc6b70" [label="add B"]
   "c16edac9" [label="modify A"]
   "a1365f85" [label="modify B"]
   "d2a4c3ff" [label="modify both"]
   "847091fa" [label="modify A again"]

   "c16edac9" -> {"18522b92"}
   "a1365f85" -> {"75dc6b70"}
   "d2a4c3ff" -> {"18522b92" "75dc6b70" "c16edac9" "a1365f85"}
   "847091fa" -> {"18522b92" "75dc6b70" "c16edac9" "a1365f85" "d2a4c3ff"}
}

我们先增加 AB 两个文件,然后分别修改两个文件的内容,注意这些操作是分成四次提交的。我们从依赖信息上看到, modify A 依赖 add Amodify B 依赖 add B ,但是这两组修改之间是没有依赖关系的。

两个仓库合并的时候,其实就是把两边的补丁放到一起,重新梳理一下依赖关系,形成一个新的 DAG 。没有依赖关系的 patch 之间的先后顺序是可以随意调整的。

因此 darcs 在合并分支的时候,会比 git 要智能很多, git 经常搞出一些莫名其妙的冲突出来。

但是代价是 darcs 合并操作非常慢,最坏情况下是指数级的复杂度。 GHC 之所以放弃 darcs 切换到 git ,就是因为他们在合并一个比较大的分支时,经常跑很久都没反应。

而且 darcs 倾向于将修改拆分成小的 patch ,这更加剧了性能问题。我们看上面例子中的 modify both ,表示同时修改了AB两个文件,但是这次我们是作为一次修改提交的。再看依赖关系,我们就会发现 modify both 依赖了前面的所有的 patch 。因此,如果我们想更好的发挥 darcs 的优势,就不能像 git 那样,一次提交修改很多个文件。但是这个挺反人类的,我们提交一般都是完成一个特性开发提交一次,除非是源码文件组织的特别好,否则很难不涉及到多个文件的修改。

darcs 还可以让用户来指定 patch 之间的依赖。它的教程里有个例子是,如果我们在 A 文件里增加了一行 use B ,表示新增 A 模块对 B 模块的依赖。那么我们应该让这个 patch 依赖 B 文件相关的最近的一个 patch ,否则合并的时候可能会出问题。相当于在版本控制中加入了一些代码语义上的依赖信息。

这么做当然是有好处的,但是感觉太麻烦了。这就像 HaskellPython 的对比一样,大家都知道静态类型语言好,把类型定义清楚好处很多,但是实际开发的时候,还不是 Python 真香?

前面提到了 darcs 仓库里的 patch 是按依赖关系组织成 DAG 的,当然一般就是树状的。因此我们可以把树上不同分支的 patch 看成仓库内的分支,它管这个叫 自发分支 ,名字挺酷的。但是随之而来的困扰就是我根本闹不清楚当前的 workspace 是怎么算出来的,尤其是合并操作之后,经常搞出一些莫名奇妙的变化。

结论

回到最初的问题,我想用版本控制软件来管理分叉树和交易池,不管是 git 还是 darcs ,在这个场景上都不适合。

主要是因为版本控制软件所在的层次太低了。举一个最简单的情况,两个开发者同时往一个文件末尾增加一行相同的内容。在merge的时候,版本管理工具是无法知道,这两个相同的修改是重复的,还是真的要做两遍。

还是需要更多高层的信息,darcs这方面相对要好一些,但是还远远不够。

另外一种方案是,一个交易一个文件,一个块对应一个文件夹,把文件 mv 到对应的文件夹,就表示把交易加入块中。这样就不涉及文件内容,修改只跟文件操作相关。但是这种情况下还是会有问题,大家可以试试如下操作:

  1. 新建一个仓库。
  2. 增加 tx1tx2tx3 三个文件。
  3. 创建两个分支, branch1branch2
  4. branch1 上做两次提交:创建文件夹 block1 ,然后 mv tx1 tx2 ./block1 ; 创建文件夹 block2 ,然后 mv tx3 ./block2
  5. branch2 上也做两次提交:创建文件夹 block1 ,然后 mv tx1 ./block1 ; 创建文件夹 block2 ,然后 mv tx2 tx3 ./block2
  6. 合并两个分支。

在默认设置下, git 最终合并结果是 block1 文件夹中有 tx1 tx2block2 文件夹中有 tx2 tx3 ,而 darcs 的最终合并结果是 block1 文件夹中只有 tx1block2 文件夹中只有 tx3tx2 还在根目录下。这个跟它们选择的合并策略有关系,比如 darcs 就是发现修改有冲突的时候干脆两边都不改,而 git 好像恰恰相反。

这些策略的选择其实没有对错之分,每个软件都是根据自己适用的场景,或者是作者的个人喜好作出了选择。但是这些选择到了另外一个场景可能就不行了,我们可能需要有一个可以灵活配置,或者是方便做二次开发的版本控制方面的库或者软件。