推送的传输协议流程类似,但服务变为 git-receive-pack,相关的流程如下:
在推送时,Git 协议本身的权限验证机制极其有限,一些分支权限控制等安全功能基本上只能通过钩子实现,而钩子的标准错误实际上也会被 Git 命令行捕获作为响应返回给客户端,如果客户端的 Git 恰好运行在 Windows Terminal、Mintty、iTerm 等等终端中,那么我们就可以将一些信息以彩色的形式输出给用户,这些信息使用 ANSI 转义的。
ssh:// 协议和 git:// 协议同样支持智能传输协议,实现起来只需要把为客户端连接和 git-upload-pack/git-receive-pack 的标准输入和输出建立数据交换的通道即可。在实施 Git Over SSH SSH 服务器时,像 GitLab 会直接使用 OpenSSH,但 OpenSSH 可定制性有限,在分布式 Git 平台上需要实现模拟的 git-upload-pack/git-receive-pack 这样的命令,效率较低。像 GitHub 早期使用了 libssh 实现了 Git Over SSH 服务,BitBucket 使用了 Apache Mina SSHD,还有一些平台使用了 Golang crypto/ssh,无论采用什么样的技术,都应该经过慎重考虑,是否契合平台的架构,维护成本是否合适等等。在实施 Git Over TCP (git://) 服务器时,只需要解析第一个 pktline 数据包即可,git:// 协议简单,表达能力有限,没有足够的权限验证,公有云除了 GitHub 其他平台使用的较少,但我在设计读写分离和高可用时,会优先考虑使用 git:// 协议作为内部传输协议以降低内部负载。
ssh:// 协议和 git:// 协议可以支持数据的多次往返,而 http(s):// 协议只能是 Request-->Response 这样的一个来回,不同的来回实际上状态已经丢失,所以需要指定为 State Less 也就是无状态。
智能协议虽然非常简单,但我们在 Git Over HTTP 上支持 shallow clone 时却不得不注意一些细节,在协商 commit deepin 时,客户端和服务端都在等待对方的响应,这时我们只能通过提前关闭服务端的标准输入中断一方的等待,这就是智能传输协议的大问题,HTTP 传输实现复杂,不支持扩展。另外随着 VFS for Git 这样技术的诞生,使得一个问题浮现在公众面前:“巨型存储库如何优化克隆”。VFS for Git 重新设计了传输协议更显得智能传输协议在这上面尤为不足。
有线传输协议Google 开发者的思路是,通过一个特殊的环境变量开关控制协议的切换。从外表看,传输协议仍然是几组命令的输入输出交换,但从内在看,新的传输协议更像是利用低级别的命令实现功能的扩展。我们依然可以使用上面的调试方法分析 Git 有线协议的传输流程,在新的协议中,服务端先返回了版本信息,支持的命令,过滤器,对象格式等等,客户端再次发送请求需要使用 ls-refs 发现引用,然后是 fetch 命令(以下截图中没有这一操作)获得数据。
实施 Git 有线传输协议非常简单,只需要升级 Git 命令,检测客户端请求是否为 GIT_PROTOCOL=2,然后以环境变量 GIT_PROTOCOL=2 启动上述命令即可,在我们的博客《Git Wire 协议杂谈》 中也有介绍。
Git Wire 协议是 Git 的一次大的改变,在协议中添加了命令、filter 等机制,有效解决了传输协议中最低效的部分,增强了可扩展性,比如我们使用部分克隆时,需要添加 blob filter,即我不需要我就可以不下载文件;支持 SHA256 时,告诉服务端,我需要 object-format=sha256,这为 Git 增加了无限可能。目前 Git 的部分克隆,SHA256 存储库都依赖有线传输协议。
实际上集中式版本控制系统 SVN 早就利用子命令扩展了协议能力,SVN 协议使用 ABNF 描述协议,要比 Git 的有线协议解析起来复杂一些。
Git 数据的交换了解了 Git 的存储结构和传输协议后,再建立宏观上的 Git 数据交换映像就容易得多,对 Git 的操作实际上是发生在三个区域,工作区是我们实质上修改,添加,删除文件的地方,通过 git add/commit/checkout 等命令,我们就将工作区的文件纳入版本管理了,通过 git push/fetch 等命令,就将本地存储库和远程建立了关联。这里需要注意,git pull 实际 上是 git fetch+ git checkout(没有 merge 的情况下),大致如下图: