章節 ▾ 第二版

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-packreceive-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-statusdelete-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-packupload-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_ackside-band 能力,但詳細介紹它們超出了本書的範圍。我們試圖讓你瞭解客戶端和伺服器之間大致的來回互動;如果你需要比這更多的知識,你可能需要檢視 Git 原始碼。

scroll-to-top