版本号
Subversion(以下简称svn)是使用版本号来区分不同的文件版本的。对于其来说,每一次commit都会生成一个新的版本号并且是全局的,为了保障这个性质,svn要求每一次数据操作都是原子的。如图:
这些文件者存在$repoBase$/repos/db/revs/ 目前下。
存储结点
Svn使用结点表示每一个文件或目录,其数据存储结构类似于文件系统但有又不一样的地方。
比如我们在svn里存了这么一个目录结构,版本为r1:
/
/project
/project/fileA.java
那么其在svn里的样子是:
fileA.java结点里存储的就是文件最终的值。这些结点满足以下条件:
1、 每个结点(node)只能是Directory或File;
2、 与linux不同,一个文件可以有多个parent,见下图;
3、 对于Directory里面存储的是指向下一级的指针,这些指针都存储在一个指针表(NodeTable)里;
4、 对于File里面是存储的文件数据;
5、 每一个node将会存储一个称为“先前版本序列(AncestorNode)”的这么一个文件
Bubble-Up模型
这里我们以文件更新为例。假设我们有一个工程,其目录结构如下:
/
/project
/project/sourceA.java
/project/sourceB.java
即里面有两个名为sourceA.java与sourceB.java的源文件。那么当我们初次commit时,svn里存储的结构如下:
我们可以看到,一个版本号在svn中对应的是整个目录的状态。现在假设我们对sourceA.java进行一些更改并提交上去,svn将会做如下工作:
而对于没有修改的文件,只需存储一个指上向一个版本的指针即可。那么最后得到的结果就是:
其它增加、删除操作也同理。从这里的结构我们可以看出,这种设计对于想找到某个文件的某个版本非常,只需要定位到对应的全局版本号即可。
Ancestor Node
现在我们着重来说一下“先前版本序列(Ancestor Node)”,以下简称AN。假如说文件经过3次更改提交后到版本4,那个它的AN里存储的数据则应该是:(R1,R2,R3),表明其是经过3个版本演进而来的。现在假设自版本1后,branch了一个分支,并提交了R4,R5,那么当branch合并(merge)后,AN应该是以下样子:
如果在R1同时还开了一个分支2,当分支1没合时,分支2合并时,trunk里AN存储的内容为:
如此一来,我们就可以很容易地知道某个文件的历史了。这样做还有一个好处,就是可以避免重复版本的合并问题。假如分支B1有R2,B2还有R2,那么在B1合并以后B2再合并就没有必须把B2的R2合进去了。svn的全局版本号会保证B1与B2的R2是绝对一样的。
差值存储
这里需要注意的是sourceB.java里并不是存的新版sourceB.java的全文件,而是delta值(就是差值)。Svn针对文件的类型使用了三种计算方法,即:
· Text Delta:用于纯文件,例如源代码,使用的是V.Delta算法。具体算法可以参看:http://pdf.aminer.org/000/578/273/an_empirical_study_of_delta_algorithms.pdf中的3.4节
· Property Delta:用于key-value类型的property文件
· Tree Delta:用于目录结构
这里着重来说一下TreeDelta。注意:svn使用不同的文件系统(FSFS或Berkeley DB)会有不同的结果。自svn1.2后,FSFS就成了默认的存储文件系统了,所以这里我们以FSFS来说明。我们还是用例子来说明,这样更容易理解。假设有下列目录结构:
/
/project/
/project/java
/project/resource
那么当我们将其提交到svn上后,treedelta里存的内容为:
<tree-delta>
<open name='project'>
<directory>
<tree-delta>
<open name='java'>
<directory></directory>
</open>
<openname='resource'>
<directory></directory>
</open>
</tree-delta>
</directory>
</open>
</tree-delta>
其中的意思我想都应该很明白,open标签即是表示新开一个结点的意思。除了open以外,还存在delete、add标签,相应的都比较容易理解。
这里需要对Delta值说明一下,svn里的delta只能用于从源文件A生成目标文件B,不能反过来。如图:
从上图我们可以知道,svn不是支持二进制文件delta值的,这种情况下,存的就是全新的文件了。
来看看svn实际存储的值
上面说了几种delta,需要注意的是,一般对目录的添加、修改,svn并不是存的treedelta,而是存的PLAIN格式,至少svn1.6是这样的。举个例子:
首先我们创建一个空的repo,理所当然的里面只有根目录,即“/”,那么r0存的内容是:
PLAIN
END
ENDREP
id: 0.0.r0/17
type: dir
count: 0
text: 0 0 4 4 2d2977d1c96f487abe4a1e202dd03b4e
cpath: /
17 107
那么这就是svn数据里的一个结点(node),也就是说r0版本只有一个dir,叫“/”,也就是根。现在我们添加
/project/
/project/java
/project/resource
三个目录,则r1里存储的为:
PLAIN
K 14
bugtraq:number
V 4
true
END
ENDREP
id: 0-1.0.r1/46
type: dir
count: 0
props: 1 0 33 0 47292cf701b9dafc4b652bbcff41d55d
cpath: /java
copyroot: 0 /
PLAIN
K 14
bugtraq:number
V 4
true
END
ENDREP
id: 2-1.0.r1/204
type: dir
count: 0
props: 1 158 33 0 47292cf701b9dafc4b652bbcff41d55d
cpath: /resources
copyroot: 0 /
PLAIN
K 4
java
V 15
dir 0-1.0.r1/46
K 9
resources
V 16
dir 2-1.0.r1/204
END
ENDREP
id: 0.0.r1/407
type: dir
pred: 0.0.r0/17
count: 1
text: 1 324 70 70 4f979dc380d411aa56da7a2152951989
cpath: /
copyroot: 0 /
_0.0.t0-0 add-dir false true /java
_2.0.t0-0 add-dir false true /resources
407 532
从上面可以清楚看出,最后一条结点记录了“/”添加了“java”与“resource”两个目录(红色部分), 而前面两个记录则记录了java与resource文件夹的信,第三个结点的pred即AN(AncestorNode)下一次,所以AN在这里是以链表的形式存储的。总的来说这里也非常符合上面的bubble-up模型。