对于引用而言,通常存储在 refs 目录下,和松散对象一样,这种机制可能存在性能问题,因此,在运行 git gc 后,引用会被打包到 packed-refs 文件中集中管理,为了加快引用的查询,引用名会使用字典排序,Git 同样会使用二分法查找在 packed-refs 中查找引用。尽管查找引用的速度非常快,但面对 Android 这样引用数量巨大的项目,Git 依然会显得心有余而力不足,这就需要设计一个好的方案解决其性能问题。
Git 存储原理的运用了解到 Git 的存储原理后,我们可以基于其原理做一些有趣的事情,比如要快速找到存储库中存在哪些大文件,我们可以通过分析 Pack Index,将文件的偏移按照递减的顺序排列,依次相减就可以知道某一对象在 Pack 中占据的大致大小,这样就可以实现大文件的检测。这种机制要比从 Pack 中依次读取文件大小高效的多,同时对于平台而言,尽管存在一些误差,但这种方案却是十分经济有效的。
另外,在实现代码托管平台存储库快照的功能时,可以通过研究存储库引用的存储机制,利用引用名称空间实现存储库的快照,相对于直接克隆快照的方案,该方案节省了非常大的存储空间。
Git 的传输协议对于现代版本控制系统而言,传输协议与代码托管平台的关系更为密切,只要支持了该版本控制系统的传输协议才意味着平台支持这个版本控制系统,要支持 Git,代码托管平台也就需要了解 Git 的传输协议。
传输协议的发展和版本控制系统的不断发展类似,Git 的传输协议也是在不断发展以适应新的情况。谈到 Git 传输协议,我们最常用的是智能协议,除了智能协议,Git 还有本地协议,哑协议(Dump Protocol),以及有线协议(Wire Protocol/v2 Protocol)。本地协议通常指通过文件系统路径或者 file:// 协议路径访问本机上的存储库的协议,该协议本质上是通过命令调用将其他目录的存储库拷贝到指定目录,这类协议的用处较少,其中有一个细节需要讲清楚,基于文件系统路径的克隆,也就是非 file:// 协议克隆,会将源存储库的对象,这里通常是 .pack 文件通过硬链接的方式共享,这实际上是利用了 Git 对象的只读特性,也就是只能删除和新增而不能修改,另外,两个目录并不在同一个分区则不支持硬链接,也就不能使用硬链接共享对象。
哑协议旨在为服务端没有 Git 服务时提供只读的 Git Over HTTP 访问支持,正因为不支持写操作,目前几乎所有的公共代码托管平台均已经不在支持哑协议了。
既然哑协议不堪重任,那么也只能另起炉灶设计一个好的协议了,这就有了智能协议,但随着 Git 被广泛使用,智能协议也有一些先天性缺陷,于是就产生了有线传输协议。
智能传输协议Git 目前主要支持的网络协议有三种,分别是 http(s)://,ssh://,git:// 无论哪种协议,拉取实质上都是 git-fetch-pack/git-upload-pack 的数据交换,推送都是 git-send-pack/git-receive-pack 的数据交换,在 2018 年以前,均是采用智能传输协议,我们可以使用 Wireshark 这样的工具抓包分析其传输流程,也可以使用 GIT_CURL_VERBOSE=2 GIT_TRACE_PACKET=2 这样设置环境变量后运行相关命令调试 Git,在 Windows 中可以使用我编写的包管理器 baulk 中的命令运行器 baulk-exec 运行相关命令,如:
baulk-exec GIT_CURL_VERBOSE=1 GIT_TRACE_PACKET=2 git ls-remote https://github.com/baulk/baulk.git分析协议的方法已经有了,我们就可以轻易的知道智能协议的流程,以 http(s):// 为例,我们把传输的第一个步骤叫做引用发现,客户端根据存储库的 URL 使用 GET 请求到 /repo.git/info/refs?service=git-upload-pack 这样的地址,服务端则以 --advertise-refs --stateless-rpc 这样的参数启动 git-upload-pack,该命令启动后将存储库目前的 HEADcommitID,存储库支持的 capabilities,以及 HEAD 对应的 symref 以及所有的引用名及其 commitID 返回给客户端,客户端根据这些信息,以及本地的存储库已经存在的对象清点出需要的 want 和存在的 have commitID,然后通过 POST /repo,git/git-upload-pack 发送给服务端,服务端通过执行 git-upload-pack --stateless-rpc /path/to/repo.git 将打包好的对象返回给客户端,待客户端清点好对象,传输就结束了,对于 git pull 请求还需要将更新的文件检出到工作目录。
这里需要注意,实施 Git Over HTTP 服务器时,Git 客户端需要在 POST 请求响应最开始添加 001e# service=git-upload-pack\n0000,另外我们还需要正确的设置 Content-Type,服务端处理POST 请求时,请求体可能使用 gzip 编码,需要解压缩处理。