章節 ▾ 第二版

10.6 Git 內部機制 - 傳輸協議

傳輸協議

Git 可以在兩個倉庫之間透過兩種主要方式傳輸資料:“啞”協議和“智慧”協議。本節將快速介紹這兩種主要協議的運作方式。

啞協議

如果您正在設定一個僅可透過 HTTP 讀取的倉庫,很可能會使用啞協議。此協議之所以稱為“啞”,是因為在傳輸過程中伺服器端不需要任何 Git 特定的程式碼;獲取過程是一系列 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 會在這些備用倉庫中查詢鬆散檔案和 packfile——這是一個很好的機制,可以使彼此分叉的專案在磁碟上共享物件。但是,由於此案例中沒有列出備用倉庫,您的物件一定在 packfile 中。要檢視此伺服器上可用的 packfile,您需要獲取 objects/info/packs 檔案,該檔案包含它們的列表(也由 update-server-info 生成)

=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack

伺服器上只有一個 packfile,所以您的物件顯然在裡面,但您會檢查索引檔案以確保。這對於伺服器上有多個 packfile 的情況也很有用,這樣您就可以看到哪個 packfile 包含您需要的物件

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)

現在您有了 packfile 索引,您可以檢視您的物件是否在其中——因為索引列出了 packfile 中包含的物件及其偏移量。您的物件在那裡,所以繼續獲取整個 packfile

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)

您獲得了樹物件,所以您繼續遍歷您的提交。它們都包含在您剛剛下載的 packfile 中,因此您不必再向伺服器發出任何請求。Git 會檢出您在開始時下載的 HEAD 引用指向的 master 分支的工作副本。

智慧協議

啞協議簡單但效率不高,並且無法處理客戶端到伺服器的資料寫入。智慧協議是更常用的資料傳輸方法,但它需要在遠端端有一個瞭解 Git 的程序——它可以讀取本地資料,弄清楚客戶端有什麼以及需要什麼,併為其生成自定義的 packfile。有兩種資料傳輸流程:一對用於上傳資料,一對用於下載資料。

上傳資料

要將資料上傳到遠端程序,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 以及其他一些功能,包括客戶端識別符號)。

資料以塊的形式傳輸。每個塊都以一個 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”。

接下來,客戶端傳送一個 packfile,包含伺服器尚不擁有的所有物件。最後,伺服器會響應成功(或失敗)指示

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 的輸出和 packfile。然後,伺服器透過其 HTTP 響應指示成功或失敗。

請記住,HTTP 協議可能還會將此資料包裝在分塊傳輸編碼中。

下載資料

下載資料時,會涉及 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 程序,開始傳送所需資料 packfile

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

同樣,這與上面的格式相同。此請求的響應指示成功或失敗,幷包含 packfile。

協議摘要

本節提供了傳輸協議的非常基礎的概述。該協議包含許多其他功能,例如 multi_ackside-band 功能,但涵蓋它們超出了本書的範圍。我們試圖讓您對客戶端和伺服器之間的通用往返通訊有所瞭解;如果您需要比這更多的知識,您可能需要檢視 Git 原始碼。