上一篇介绍了Git相关的内容,这一篇介绍Mercurial(又称hg)。

同样作为DVCS,Mercurial的核心概念和Git是一致的,都是基于改动集的,所有的改动,作为一个集合来提交,要么全部提交成功,要么全部提交失败。本文要介绍Mercurial是如何形成改动集的,改动集所基于的文件集和存储方式。

Mercurial的存储方式

Mercurial采用一种叫做Revlog的格式来存储文件。为了查看Revlog格式,我们用一个实际的Mercurial仓库做例子。

# 在一个空的目录下执行下面操作
$ hg init
$ echo a1 > a.txt
$ hg add
adding a.txt
$ hg ci -m "add a.txt"

上面的命令行操作初始化了Mercurial仓库,里面提交了一个文件a.txt。Mercurial的仓库目录名字是.hg,内容如下:

$ tree -A .hg
.hg
├── 00changelog.i
├── cache
│   ├── branch2-served
│   ├── rbc-names-v1
│   └── rbc-revs-v1
├── dirstate
├── last-message.txt
├── requires
├── store
│   ├── 00changelog.i
│   ├── 00manifest.i
│   ├── data
│   │   └── a.txt.i
│   ├── fncache
│   ├── phaseroots
│   ├── undo
│   ├── undo.backupfiles
│   └── undo.phaseroots
├── undo.backup.dirstate
├── undo.bookmarks
├── undo.branch
├── undo.desc
└── undo.dirstate

a.txt这个文件在仓库中对应的是.hg/store/data/a.txt.i,而这个.i结尾的文件就是以Revlog方式存储的。

可以用hg debugindex命令查看Revlog格式:

$  hg debugindex .hg/store/data/a.txt.i
   rev    offset  length  delta linkrev nodeid       p1           p2
     0         0       6     -1       0 47680620abf0 000000000000 000000000000

Revlog格式中的每一行包括以下信息:

  • rev,每个revlog文件自带的版本号,从0开始。因为这个字段存在,Mercurial每个单独的文件可以有自己的版本号
  • offset,当前版本在Revlog中内容存储的开始位置
  • length,当前版本在Revlog中内容存储的长度
  • delta,如果当前版本是增量存储的话,delta指向基线版本号
  • linkrev,指向改动集,后续会介绍
  • nodeid,对当前版本做SHA哈希得出来的id(和Git一样,Mercurial也是采用哈希的方式来索引文件的。
  • p1,父版本的nodeid,除了0版本外,所有的版本都有一个父版本
  • p2,母版本的nodeid,如果一个版本是合并产生的,那么这个版本同时有父版本和母版本

现在让我们来更新一下a.txt,看看其Revlog的变化:

$ echo a2 >> a.txt
$ hg add
$ hg ci -m "update a.txt"
$ hg debugindex .hg/store/data/a.txt.i
   rev    offset  length  delta linkrev nodeid       p1           p2
     0         0       6     -1       0 47680620abf0 000000000000 000000000000
     1         6       9     -1       1 648e4d356cb9 47680620abf0 000000000000

可以看到各字段发生了相应的变化。

Mercurial的文件集和改动集

Mercurial管文件集叫做manifest,管改动集叫做changeset。大家应该可以猜到,manifest和changeset也是以Revlog的格式存储的。

可以用hg debugindex --manifest方式查看manifest:

$  hg debugindex --manifest
   rev    offset  length  delta linkrev nodeid       p1           p2
     0         0      48     -1       0 104b18002222 000000000000 000000000000
     1        48      48     -1       1 49954031d18a 104b18002222 000000000000

同样,可以用hg debugindex --changelog方式查看changlog:

$  hg debugindex --changelog
   rev    offset  length   base linkrev nodeid       p1           p2
     0         0      82      0       0 c6619554cf9e 000000000000 000000000000
     1        82      83      1       1 5ee6eca6aba4 c6619554cf9e 000000000000

我们对a.txt进行了两次提交,所以产生了两条manifest记录和两条changelog记录。

manifest的内容

可以使用hg --debug manifest或者hg debugdata --manifest rev来查看manifest的内容:

$  hg --debug manifest
648e4d356cb9b5b2dc40f24027ab66f4666e9a46 644   a.txt

可以看到当前manifest只包含了一个文件a.txt。我们尝试添加一个新的文件b.txt,再看看manifest的变化:

$  echo b1 > b.txt
$  hg add
adding b.txt
$  hg ci -m "add b.txt"
$  hg --debug manifest
648e4d356cb9b5b2dc40f24027ab66f4666e9a46 644   a.txt
9187ebce6938d80b6fbfbb49ee3bedf16830c687 644   b.txt

可以看到最新的manifest里面增加了b.txt相关的内容。

总的来说manifest实现了之前描述过的DVCS的文件集,用于表示当前改动集中所有文件的集合。

changeset的内容

前面提到,changeset的内容可以通过hg debugindex --changelog查看:

$  hg debugindex --changelog
   rev    offset  length   base linkrev nodeid       p1           p2
     0         0      82      0       0 c6619554cf9e 000000000000 000000000000
     1        82      83      1       1 5ee6eca6aba4 c6619554cf9e 000000000000
     2       165      80      2       2 06e6ce98c711 5ee6eca6aba4 000000000000

因为我们添加了b.txt所以changelog升了一个版本,最高版本从1变成2了。可以通过hg debugdata --changelog rev查看changelog的内容:

# 查看最新的版本2
$ hg debugdata --changelog 2
868c703c8333bb1fe14da15392ed4cf93f48e0b2
username
1531880540 -3600
b.txt

add b.txt%

changelog里面包含了几个信息:

  • 868c703c...,对应的manifest的nodeid
  • username,用户名
  • 1531880540 -3600,这是时间信息
  • 注释内容

所以,基本上Mercurial的changelog可以对应Git的commit。内容大致一致。

到changeset的反向链接

最后,如何从某个文件的版本,或者某个manifest找到对应的changeset呢,请看(https://www.mercurial-scm.org/wiki/Design)页面的这张图:

   .--------linkrev-------------.
   v                            |
.---------.    .--------.    .--------.
|changeset| .->|manifest| .->|file    |---.
|index    | |  |index   | |  |index   |   |--.
`---------' |  `--------' |  `--------'   |  |
    |       |      |      |     | `-------'  |
    V       |      V      |     V    `-------'
.---------. |  .--------. |  .---------.
|changeset|-'  |manifest|-'  |file     |
|data     |    |data    |    |revision |
`---------'    `--------'    `---------'

还记得Revlog有一个字段叫做linkrev吗!文件或者manifest的每个版本的linkrev都指向与之关联的changelog的版本,籍此实现对应关系,使得可以从文件或者manifest反向查找到changelog。

小结

关于Mercurial的这种基于Revlog的设计的优缺点的一些评述:

  • 每个文件可以有自己的版本好,和传统的CVS/SVN有相似之处。
  • Revlog可以以增量的方式存储文件的某个版本,和Git比,存储比较高效。Git对于文件的不同版本存储的是完整的文件内容(除非对仓库进行Pack操作)
  • 目前,每个文件在Mercurial仓库中是分散存储的。例如,a.txt在仓库中的存储位置是.hg/store/data/a.txt.i。这有几个问题,a.txt.i的命名依赖于a.txt,不像Git那样文件的存储和文件名是分开的。另外一个问题是在Mercurial仓库中尚不支持Pack操作,当仓库中小文件非常多的时候,分散存储会导致空间浪费。因为文件系统一般分配空间是按4K/8K/16K这种大小分配的,文件再小,至少也占4K。
  • changelog也是以Revlog方式存储,而Revlog不支持将多个版本合并。所以Mercurial的changelog不能像Git那样,可以把多个commit压缩成一个。
  • 以Revlog方式存储的changelog,子版本与父母版本之间是通过Revlog字段p1p2进行链接的,有被篡改的风险。而在Git中,每个commit的内容里面都包含前一个commit的id,commit和commit之间是影链接关系。
  • 就合并操作而言,Mercurial只能进行两路合并,因为Revlog里面只有p1p2两个父母字段。
  • Mercurial一开始的设计是不支持分支操作的,如果需要分支,就需要重新克隆Mercurial仓库。

综上而言,虽然Mercurial出现的比Git早,而且设计精巧也很实用,但是不如Git流行,也是有道理的。

Mercurial官方文档链接

其他参考链接

(本篇完)