-
A1. 附录 A: Git 在其他环境
- A1.1 图形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小结
-
A2. 附录 B: 在应用程序中嵌入 Git
-
A3. 附录 C: Git 命令
10.6 Git 内部原理 - 传输协议
传输协议
Git 可以在两个仓库之间以两种主要方式传输数据:“哑协议”(dumb protocol)和“智能协议”(smart protocol)。本节将快速介绍这两种主要协议的运作方式。
哑协议
如果你正在设置一个仓库以 HTTP 方式提供只读服务,那么很可能会使用哑协议。这个协议之所以被称为“哑协议”,是因为它在传输过程中不需要服务器端有任何 Git 特定的代码;抓取(fetch)过程是一系列 HTTP GET
请求,客户端可以假定服务器上 Git 仓库的布局。
注意
|
哑协议如今已相当少用。它难以保障安全或实现私有化,因此大多数 Git 主机(无论是云端还是本地部署)都会拒绝使用它。通常建议使用智能协议,我们将在后面更详细地描述它。 |
让我们跟踪 simplegit 库的 http-fetch
过程。
$ git clone http://server/simplegit-progit.git
该命令做的第一件事是下载 info/refs
文件。这个文件是由 update-server-info
命令写入的,这就是为什么你需要将其作为 post-receive
钩子启用,以便 HTTP 传输正常工作。
=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949 refs/heads/master
现在你有了远程引用和 SHA-1 的列表。接下来,你查找 HEAD 引用是什么,这样就知道完成后要检出什么。
=> GET HEAD
ref: refs/heads/master
完成这个过程后,你需要检出 master
分支。此时,你已准备好开始遍历过程。因为你的起点是在 info/refs
文件中看到的 ca82a6
提交对象,所以你首先抓取它。
=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)
你会得到一个对象——这个对象在服务器上是松散格式的,你通过静态 HTTP GET 请求抓取了它。你可以用 zlib 解压它,去除头部,然后查看提交内容。
$ git cat-file -p ca82a6dff817ec66f44342007202690a93763949
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700
Change version number
接下来,还有两个对象需要检索——cfda3b
,它是我们刚刚检索到的提交所指向的内容树;以及 085bb3
,它是父提交。
=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)
这为你提供了下一个提交对象。获取树对象。
=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)
糟糕——看起来那个树对象在服务器上不是松散格式的,所以你收到了一个 404 响应。这有几个原因——该对象可能在备用仓库中,或者它可能在此仓库的打包文件(packfile)中。Git 首先检查任何列出的备用仓库。
=> GET objects/info/http-alternates
(empty file)
如果这返回了一个备用 URL 列表,Git 就会检查那里的松散文件和打包文件——这对于相互分支的项目来说,是一种很好的在磁盘上共享对象的机制。然而,在这种情况下,由于没有列出备用仓库,你的对象肯定在一个打包文件中。要查看此服务器上有哪些打包文件,你需要获取 objects/info/packs
文件,该文件包含了它们的列表(也由 update-server-info
生成)。
=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
服务器上只有一个打包文件,所以你的对象显然在那里,但你会检查索引文件以确保。如果你在服务器上有多个打包文件,这也很实用,这样你就可以看到哪个打包文件包含你需要的对象。
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)
现在你有了打包文件索引,你可以查看你的对象是否在其中——因为索引列出了打包文件中包含的对象的 SHA-1 值以及这些对象的偏移量。你的对象就在那里,所以继续获取整个打包文件吧。
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)
你已经有了你的树对象,所以你可以继续遍历你的提交。它们也都在你刚刚下载的打包文件中,因此你无需再向服务器发送任何请求。Git 会检出 master
分支的一个工作副本,该分支由你一开始下载的 HEAD 引用所指向。
智能协议
哑协议简单但效率较低,并且无法处理从客户端向服务器写入数据。智能协议是一种更常见的数据传输方法,但它需要远程端有一个了解 Git 的智能进程——它能够读取本地数据,判断客户端拥有什么以及需要什么,并为其生成一个自定义的打包文件。数据传输有两组进程:一组用于上传数据,一组用于下载数据。
上传数据
要向远程进程上传数据,Git 使用 send-pack
和 receive-pack
进程。send-pack
进程在客户端运行,并连接到远程端的 receive-pack
进程。
SSH
例如,假设你在项目中运行 git push origin master
,并且 origin
被定义为使用 SSH 协议的 URL。Git 会启动 send-pack
进程,该进程通过 SSH 与你的服务器建立连接。它尝试通过一个类似于这样的 SSH 调用在远程服务器上运行一个命令:
$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"
00a5ca82a6dff817ec66f4437202690a93763949 refs/heads/master□report-status \
delete-refs side-band-64k quiet ofs-delta \
agent=git/2:2.1.1+github-607-gfba4028 delete-refs
0000
git-receive-pack
命令立即为它当前拥有的每个引用响应一行——在本例中,只有 master
分支及其 SHA-1。第一行还包含服务器能力的列表(这里是 report-status
、delete-refs
以及其他一些,包括客户端标识符)。
数据以块(chunks)的形式传输。每个块都以一个 4 字符的十六进制值开头,指定块的长度(包括长度本身占用的 4 字节)。块通常包含一行数据和一个尾随的换行符。你的第一个块以 00a5 开头,这是 165 的十六进制表示,意味着该块长 165 字节。下一个块是 0000,表示服务器已完成其引用列表。
现在,send-pack
进程知道服务器的状态了,它会确定自己有哪些提交是服务器没有的。对于本次推送将要更新的每个引用,send-pack
进程都会将该信息告知 receive-pack
进程。例如,如果你正在更新 master
分支并添加一个 experiment
分支,send-pack
的响应可能看起来像这样:
0076ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 \
refs/heads/master report-status
006c0000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d \
refs/heads/experiment
0000
Git 为你正在更新的每个引用发送一行,其中包含行的长度、旧的 SHA-1、新的 SHA-1 以及正在更新的引用。第一行还包含客户端的能力。全部为 '0’ 的 SHA-1 值表示之前那里什么都没有——因为你正在添加 experiment
引用。如果你正在删除一个引用,你会看到相反的情况:右侧全部为 '0’。
接下来,客户端发送一个包含服务器尚未拥有的所有对象的打包文件。最后,服务器响应一个成功(或失败)的指示。
000eunpack ok
HTTP(S)
这个过程通过 HTTP 也大致相同,尽管握手方式略有不同。连接通过以下请求发起:
=> GET http://server/simplegit-progit.git/info/refs?service=git-receive-pack
001f# service=git-receive-pack
00ab6c5f0e45abd7832bf23074a333f739977c9e8188 refs/heads/master□report-status \
delete-refs side-band-64k quiet ofs-delta \
agent=git/2:2.1.1~vmg-bitmaps-bugaloo-608-g116744e
0000
这是第一次客户端-服务器交换的结束。然后客户端发出另一个请求,这次是 POST
请求,其中包含 send-pack
提供的数据。
=> POST http://server/simplegit-progit.git/git-receive-pack
POST
请求包含 send-pack
的输出和打包文件作为其有效载荷。然后服务器通过其 HTTP 响应指示成功或失败。
请记住,HTTP 协议可能会将这些数据进一步封装在分块传输编码(chunked transfer encoding)中。
下载数据
当你下载数据时,会涉及 fetch-pack
和 upload-pack
进程。客户端启动一个 fetch-pack
进程,该进程连接到远程端的 upload-pack
进程,以协商将要下载的数据。
SSH
如果你通过 SSH 执行抓取,fetch-pack
会像这样运行:
$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"
fetch-pack
连接后,upload-pack
会返回类似这样的内容:
00dfca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
side-band side-band-64k ofs-delta shallow no-progress include-tag \
multi_ack_detailed symref=HEAD:refs/heads/master \
agent=git/2:2.1.1+github-607-gfba4028
003fe2409a098dc3e53539a9028a94b6224db9d6a6b6 refs/heads/master
0000
这与 receive-pack
的响应非常相似,但功能有所不同。此外,它还会返回 HEAD 指向的内容(symref=HEAD:refs/heads/master
),这样如果这是一个克隆操作,客户端就知道要检出什么。
此时,fetch-pack
进程会查看它拥有的对象,并通过发送“want”和紧随其后的所需 SHA-1 来响应它需要的对象。它会发送所有已拥有的对象,格式为“have”和紧随其后的 SHA-1。在这个列表的末尾,它会写入“done”,以启动 upload-pack
进程开始发送所需数据的打包文件。
003cwant ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0009done
0000
HTTP(S)
抓取操作的握手需要两个 HTTP 请求。第一个是向哑协议中使用的相同端点发出的 GET
请求。
=> GET $GIT_URL/info/refs?service=git-upload-pack
001e# service=git-upload-pack
00e7ca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
side-band side-band-64k ofs-delta shallow no-progress include-tag \
multi_ack_detailed no-done symref=HEAD:refs/heads/master \
agent=git/2:2.1.1+github-607-gfba4028
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
0000
这与通过 SSH 连接调用 git-upload-pack
非常相似,但第二次交换是作为单独的请求执行的。
=> POST $GIT_URL/git-upload-pack HTTP/1.0
0032want 0a53e9ddeaddad63ad106860237bbf53411d11a7
0032have 441b40d833fdfa93eb2908e52742248faf0ee993
0000
同样,这与上面的格式相同。对此请求的响应指示成功或失败,并包含打包文件。
协议总结
本节包含了传输协议的非常基本概述。该协议还包括许多其他功能,例如 multi_ack
或 side-band
能力,但详细介绍它们超出了本书的范围。我们试图让你了解客户端和服务器之间大致的来回交互;如果你需要比这更多的知识,你可能需要查看 Git 源代码。