Git --local-branching-on-the-cheap

9.6 Git 內部原理 - 傳輸協議

傳輸協議

Git 可以用兩種主要的方式跨越兩個倉庫傳輸資料:基於 HTTP 協定之上,和 file://, ssh://, 和 git:// 等智慧傳輸協議。這一節帶你快速流覽這兩種主要的協議操作過程。

啞協議

Git 基於 HTTP 之上傳輸通常被稱為啞協議,這是因為它在服務端不需要有針對 Git 特有的代碼。這個獲取過程僅僅是一系列 GET 請求,用戶端可以假定服務端的 Git 倉庫中的佈局。讓我們以 simplegit 倉庫為例來看看 http-fetch 的過程:

$ git clone http://github.com/schacon/simplegit-progit.git

它做的第一件事情就是獲取 info/refs 檔。這個檔是在服務端運行了 update-server-info 所產生的,這也解釋了為什麼在服務端要想使用 HTTP 傳輸,必須要開啟 post-receive 鉤子(hook):

=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949     refs/heads/master

現在你有一個遠端引用和 SHA 值的列表。下一步是尋找 HEAD 引用,這樣你就知道了在完成後,什麼應該被檢出到工作目錄:

=> GET HEAD
ref: refs/heads/master

這說明在完成獲取後,需要檢出(check out) master 分支。 這時,已經可以開始漫遊操作(walking process)了。因為你的起點是在 info/refs 檔中所提到的 ca82a6 commit 物件,你的開始操作就是獲取它:

=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)

然後你取回了這個物件 - 這在服務端是一個鬆散格式的物件,你使用的是靜態的 HTTP GET 請求獲取的。可以使用 zlib 解壓縮它,去除檔頭,查看它的 commmit 內容:

$ 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

changed the version number

這樣,就得到了兩個需要進一步獲取的物件 - cfda3b 是這個 commit 物件所對應的 tree 物件,和 085bb3 是它的父物件:

=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)

這樣就取得了這它的下一步 commit 物件,再抓取 tree 物件:

=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)

哎呀! - 看起來這個 tree 物件在服務端並不以鬆散格式對象存在,所以得到了404回應,代表在 HTTP 服務端沒有找到該物件。這有好幾個原因 - 這個物件可能在替代倉庫裡面,或者在打包檔裡面,Git 會首先檢查任何列出的替代倉庫:

=> GET objects/info/http-alternates
(empty file)

如果這回傳了幾個替代倉庫列表,那麼它會去那些地方檢查鬆散格式物件和檔案 - 這是一種在軟體分叉(forks)之間共用物件以節省磁碟的好方法。然而,在這個例子中,沒有替代倉庫。所以你所需要的物件肯定在某個打包檔中。要檢查服務端有哪些打包格式檔,你需要獲取 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值,和該物件存在於打包檔中的偏移量,所以你只需要簡單地獲取整個打包檔:

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

現在你也有了這個 tree 物件,你可以繼續在 commit 物件上漫遊。它們全部都在這個你已經下載到的打包檔裡面,所以你不用繼續向服務端請求更多下載了。在這完成之後,由於下載開始時已探明 HEAD 引用是指向 master 分支,Git 會將它檢出到工作目錄。

整個過程看起來就像這樣:

$ git clone http://github.com/schacon/simplegit-progit.git
Initialized empty Git repository in /private/tmp/simplegit-progit/.git/
got ca82a6dff817ec66f44342007202690a93763949
walk ca82a6dff817ec66f44342007202690a93763949
got 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Getting alternates list for http://github.com/schacon/simplegit-progit.git
Getting pack list for http://github.com/schacon/simplegit-progit.git
Getting index for pack 816a9b2334da9953e530f27bcac22082a9f5b835
Getting pack 816a9b2334da9953e530f27bcac22082a9f5b835
 which contains cfda3bf379e4f8dba8717dee55aab78aef7f4daf
walk 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
walk a11bef06a3f659402fe7563abf99ad00de2209e6

智慧協議

HTTP 方法是很簡單但效率不是很高。使用智慧協定是傳送資料的更常用的方法。這些協定在遠端都有 Git 智慧型程序(process)在服務 - 它可以讀出本地資料並計算出用戶端所需要的,並產生合適的資料給它,這有兩類傳輸資料的程序:一對用於上傳資料和一對用於下載。

上傳資料

為了上傳資料至遠端,Git 使用 send-packreceive-pack 程序。這個 send-pack 程序運行在用戶端上,它連接至遠端運行的 receive-pack 程序。

舉例來說,你在你的專案上執行了 git push origin master, 並且 origin 被定義為一個使用 SSH 協議的 URL。Git 會使用 send-pack 程序,它會啟動一個基於 SSH 的連接(connection)到伺服器。它嘗試像這樣透過 SSH 在服務端運行命令:

$ ssh -x git@github.com "git-receive-pack 'schacon/simplegit-progit.git'"
005bca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status delete-refs
003e085bb3bcb608e1e84b2432f8ecbe6306e7e7 refs/heads/topic
0000

這裡的 git-receive-pack 命令會立即對它所擁有的每一個引用回應一行 - 在這個例子中,只有 master 分支和它的SHA值。這裡第1行也包含了服務端的能力清單(這裡是 report-statusdelete-refs)。

每一行以4位元組的十六進位開始,用於指定整行的長度。你看到第1行以005b開始,這在十六進位中表示91,意味著第1行有91位元組長。下一行以003e起始,表示有62位元組長,所以需要讀剩下的62位元組。再下一行是0000開始,表示伺服器已完成了引用列表過程。

現在它知道了服務端的狀態,你的 send-pack 程序會判斷哪些 commit 是它所擁有但服務端沒有的。針對每個引用,這次推送都會告訴對端的 receive-pack 這個資訊。舉例說,如果你在更新 master 分支,並且增加 experiment 分支,這個 send-pack 將會是像這樣:

0085ca82a6dff817ec66f44342007202690a93763949  15027957951b64cf874c3557a0f3547bd83b3ff6 refs/heads/master report-status
00670000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d refs/heads/experiment
0000

這裡全部是’0’的SHA-1值表示之前沒有過這個物件 - 因為你是在添加新的 experiment 引用。如果你在刪除一個引用,你會看到相反的: 就是右邊全部是’0’。

Git 針對每個引用發送這樣一行資訊,就是舊的SHA值,新的SHA值,和將要更新的引用的名稱。第1行還會包含有用戶端的能力。下一步,用戶端會發送一個所有那些服務端所沒有的物件的一個打包檔。最後,服務端以成功(或者失敗)來回應:

000Aunpack ok

下載資料

當你在下載資料時,fetch-packupload-pack 程序就起作用了。用戶端啟動 fetch-pack 程序,連接至遠端的 upload-pack 程序,以協商後續資料傳輸過程。

在遠端倉庫有不同的方式啟動 upload-pack 程序。你可以使用與 receive-pack 相同的透過 SSH 管道的方式,也可以通過 Git 後臺來啟動這個進程,它預設監聽在 9418 號埠上。這裡 fetch-pack 程序在連接後像這樣向後臺發送資料:

003fgit-upload-pack schacon/simplegit-progit.git\0host=myserver.com\0

它也是以4位元組指定後續位元組長度的方式開始,然後是要執行的命令,和一個空位元組,然後是服務端的主機名稱,再跟隨一個最後的空位元組。Git 後臺程序會檢查這個命令是否可以執行,以及那個倉庫是否存在,以及是否具有公開許可權。如果所有檢查都通過了,它會啟動這個 upload-pack 程序並將用戶端的請求移交給它。

如果你透過 SSH 使用獲取(fetch)功能,fetch-pack 會像這樣運行:

$ ssh -x git@github.com "git-upload-pack 'schacon/simplegit-progit.git'"

不管哪種方式,在 fetch-pack 連接之後, upload-pack 都會以這種形式回傳:

0088ca82a6dff817ec66f44342007202690a93763949 HEAD\0multi_ack thin-pack \
  side-band side-band-64k ofs-delta shallow no-progress include-tag
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
003e085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 refs/heads/topic
0000

這與 receive-pack 回應很類似,但是這裡指的能力是不同的。而且它還會指出 HEAD 引用,讓用戶端可以檢查是否是一份 clone。

在這裡,fetch-pack 程序檢查它自己所擁有的物件和所有它需要的物件,通過發送 “want” 和所需物件的 SHA 值,發送 “have” 和所有它已擁有的物件的 SHA 值。在列表完成時,再發送 “done” 通知 upload-pack 程序開始發送所需物件的打包檔。這個過程看起來像這樣:

0054want ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0000
0009done

這是傳輸協議的一個很基礎的例子,在更複雜的例子中,用戶端可能會支援 multi_ack 或者 side-band 能力;但是這個例子中展示了智慧協議的基本交互過程。