最近微软开源了一个新的项目GVFS——Git Virtual File System,这是一个很棒的项目,也已经有很多相关的报道了。出于兴趣,我也通过阅读文档与源码,理解了GVFS的协议与工作原理。
一、环境要求
GVFS的客户端,必须在Windwos 10下编译。而且还必须是最新的版本。Win+R -> winver.exe查看一下,如果版本号不是1703(或以上),就无法正确的运行。
下载免费的Visual Studio 2017社区版,就能够编译。GVFS的Reame里面“Building GVFS”一节,介绍得还是很清楚的,照做即可。
二、如何测试GVFS
目前服务器端对GVFS的支持,只有微软自家的平台才行,不过Visual Studio Online也是免费的,可以自己注册一个。在创建的项目中,记得添加.gitattributes
文件,还要包含一行* -text
。
编译出来的SetupGVFS.exe,执行一下,就可以在git bash里用了。记住,要以管理员的身份运行git bash!
三、执行过程
在执行完成之后,当前目录下,会出现一个MyFirstProject目录,但是在这个目录下的src文件夹,才是实际的git仓库。
类似于执行以下命令:
# mkdir MyFirstProject
# cd MyFirstProject
# git clone https://zhuangbiaowei.visualstudio.com/_git/MyFirstProject src
四、执行细节
1. GET /gvfs/config
按照文档的说法,还挺像那么回事的,事实上,微软自己的服务器,返回的内容很简单。
{"AllowedGvfsClientVersions":null}
所以,客户端也表示WARNING: Unable to validate your GVFS version
新版本的Visual Studio Online加了一个属性:
{"AllowedGvfsClientVersions":null,"CacheServers":[]}
2. GET /info/refs?service=git-upload-pack
这是标准的Git via HTTP,为了取得这个仓库的相关的refs。返回的内容大概是这样的:
001e# service=git-upload-pack
000000a551292cea1631238803732c7e89b78243f5f8d6c9 HEAD multi_ack thin-pack side-band side-band-64k no-progress multi_ack_detailed no-done shallow allow-tip-sha1-in-want
003f51292cea1631238803732c7e89b78243f5f8d6c9 refs/heads/master
0000
其中51292cea1631238803732c7e89b78243f5f8d6c9,就是master分支最新的一次commit id
3. POST /gvfs/objects
按照上一条命令获取的commit id,客户端向服务器提交了一组对象请求。而服务器返回了一个pack文件。这个pack文件包含的objects的规则如下:
objectIds.each do |object_id|
if object_id.type == "commit"
commit_ids = `git rev-list -${CommitDepth} object_id` #取出所需深度的commit_id
tree_ids = get_all_tree_id(commit_ids) #只要tree对象,不要blob对象
object_ids << commit_ids
object_ids << tree_ids
else
object_ids << object_id
end
end
最后,调用git自身的pack-objects命令,将这些对象打包成一个文件,就可以返回了。
如果我们将请求得到的数据保存为一个文件file-name.pack
,可以用以下两条命令,查看这个pack
# git index-pack file-name.pack
# git verify-pack -v file-name.pack
1a17720ebf1ed29cc384feda3f6b254f71634902 tree 79 86 12
14cc2f21152b7655d869fd8eef97996e54b935f0 commit 232 173 98
......
d2246ca4db84bb71793d1529064f8bd46b283005 tree 63 72 9490
non delta: 86 objects
chain length = 1: 3 objects
objects.pack: ok
4. GET /gvfs/prefetch[?lastPackTimestamp={secondsSinceEpoch}]
在第一次调用时,lastPackTimestamp会等于-1,相当于请求所有历史上曾经在服务器端打包过的文件。之后这个lastPackTimestamp,会变成最近一次的请求时间。
根据我的推断,服务器端应该根据时间戳,将所有这个时间点之后打包的文件,全部计算出来,再打包一次,发送给客户端。
如果将这个获取到的二进制文件打开,我们会看到这样的数据结构:
4750 5245 2001 0100 ddaa 5759 0000 0000
# 'GPRE '开头的5个字母
# 无符号整数代表版本号,目前为1
# 2个字节,代表包数量,目前始终是0100,表示只有一个包
# 8个字节,代表一个时间戳,0x5957aadd转换成十进制数是1498917597,是一个UNIX时间戳
# Time.at(1498917597) = '2017-07-01 21:59:57 +0800'
ffff ffff ffff ff7f ffff ffff ffff ffff
# 根据文档,前8个字节代表包的长度,后8个字节代表包的索引,但是目前看来,始终不变。大概因为只有一个包的缘故
5041 434b 0000 0002 0000 005b a502 789c # 从这里开始,后续都是原始的pack的内容
3334 3030 3331 5108 7275 74f1 75d5 cb4d
6160 f46b 4c6f 0ddf 364d 8c49 2570 c752
998f d521 4adf 00bf a40c 3a93 0b78 9c2b
5. Mounting File Tree
至此,Clone+Fetch commit & tree的操作,全部完成。客户端将会执行mount的操作,利用已经获取到的tree数据,构造出一个FUSE(用户空间文件系统、Filesystem in Userspace,简称FUSE),在过去只有类UNIX操作系统才能支持,现在Windows 10也能够很好的支持了。
有了FUSE,用户看起来就已经获取到了整个git代码仓库,但事实上,客户端只获取到了全部的目录树信息,当用户需要真正需要访问这些文件时,才去服务器端获取。
6. POST /gvfs/sizes
在客户端执行cd src操作之后,用户首次想要查看已经clone下来的git仓库的内容,这时,事实上仓库的内容并不存在。因此,客户端会发起一个sizes请求,在POST的body中提交一堆的对象ID。
[
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
]
而服务器端,则返回每个对象的大小。这种操作,在git操作仓库时可以通过以下命令获取git cat-file -s obj_id
。返回的格式如下:
[
{
"Id" : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"Size" : 123
},
{
"Id" : "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"Size" : 456
}
]
7. GET /gvfs/objects/{objectId}
在客户端,如果需要打开某一个具体的文件时,客户端才需要真正获取这个文件的内容,通过一个GET gvfs/objects/obj_id,就可以获取到,服务端的处理也很简单,唯一需要注意的时:HTTP头的内容包括:Content-Type: application/x-git-loose-object
五、深入参考
想要更加深入的理解GVFS,可以参考我最近放出的一个开源项目,是基于ruby语言的gitlab-grack,添加了几个接口,初步能够在任何一种操作系统(不必是Windows)运行一个支持GVFS的git http server,欢迎大家来围观。Grack-with-GVFS
关键的修改,在lib/grack/server.rb
与lib/grack/gvfs_helper.rb
这两个文件里。