Git 可以以兩種主要的方式跨越兩個倉庫傳輸數據:基于HTTP協議之上,和 `file://, ssh://, 和 git://` 等智能傳輸協議。這一節帶你快速瀏覽這兩種主要的協議操作過程。
## 啞協議
Git 基于HTTP之上傳輸通常被稱為啞協議,這是因為它在服務端不需要有針對 Git 特有的代碼。這個獲取過程僅僅是一系列GET請求,客戶端可以假定服務端的Git倉庫中的布局。讓我們以 simplegit 庫來看看 http-fetch 的過程:
`$ git clone http://github.com/schacon/simplegit-progit.git`
它做的第1件事情就是獲取 info/refs 文件。這個文件是在服務端運行了 `update-server-info` 所生成的,這也解釋了為什么在服務端要想使用HTTP傳輸,必須要開啟 `post-receive` 鉤子:
~~~
=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949 refs/heads/master
~~~
現在你有一個遠端引用和SHA值的列表。下一步是尋找HEAD引用,這樣你就知道了在完成后,什么應該被檢出到工作目錄:
~~~
=> GET HEAD
ref: refs/heads/master
~~~
這說明在完成獲取后,需要檢出 master 分支。 這時,已經可以開始漫游操作了。因為你的起點是在 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)
~~~
Oops - 看起來這個 tree 對象在服務端并不以松散格式對象存在,所以得到了404響應,代表在HTTP服務端沒有找到該對象。這有好幾個原因 - 這個對象可能在替代倉庫里面,或者在打包文件里面, Git 會首先檢查任何列出的替代倉庫:
~~~
=> GET objects/info/http-alternates
(empty file)
~~~
如果這返回了幾個替代倉庫列表,那么它會去那些地方檢查松散格式對象和文件 - 這是一種在軟件分叉之間共享對象以節省磁盤的好方法。然而,在這個例子中,沒有替代倉庫。所以你所需要的對象肯定在某個打包文件中。要檢查服務端有哪些打包格式文件,你需要獲取 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智能型進程在服務 - 它可以讀出本地數據并計算出客戶端所需要的,并生成合適的數據給它,這有兩類傳輸數據的進程:一對用于上傳數據和一對用于下載。
## 上傳數據
為了上傳數據至遠端, Git 使用 send-pack 和 receive-pack 進程。這個 send-pack 進程運行在客戶端上,它連接至遠端運行的 receive-pack 進程。
舉例來說,你在你的項目上運行了 git push origin master, 并且 origin 被定義為一個使用SSH協議的URL。 Git 會使用 send-pack 進程,它會啟動一個基于SSH的連接到服務器。它嘗試像這樣透過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-status 和 delete-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-pack 和 upload-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-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引用,讓客戶端可以檢查是否是一份克隆。
在這里, `fetch-pack` 進程檢查它自己所擁有的對象和所有它需要的對象,通過發送 "want" 和所需對象的SHA值,發送 "have" 和所有它已擁有的對象的SHA值。在列表完成時,再發送 "done" 通知 upload-pack 進程開始發送所需對象的打包文件。這個過程看起來像這樣:
~~~
0054want ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0000
0009done
~~~
這是傳輸協議的一個很基礎的例子,在更復雜的例子中,客戶端可能會支持 `multi_ack` 或者 `side-band` 能力;但是這個例子中展示了智能協議的基本交互過程。
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 安裝 Git
- 1.5 初次運行 Git 前的配置
- 1.6 獲取幫助
- 1.7 小結
- 2. Git基礎
- 2.1 取得項目的 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 技巧和竅門
- 2.8 小結
- 3. Git分支
- 3.1 何謂分支
- 3.2 分支的新建與合并
- 3.3 分支的管理
- 3.4 利用分支進行開發的工作流程
- 3.5 遠程分支
- 3.6 分支的衍合
- 3.7 小結
- 4. 服務器上的Git
- 4.1 協議
- 4.2 在服務器上部署 Git
- 4.3 生成 SSH 公鑰
- 4.4 架設服務器
- 4.5 公共訪問
- 4.6 GitWeb
- 4.7 Gitosis
- 4.8 Gitolite
- 4.9 Git 守護進程
- 4.10 Git 托管服務
- 4.11 小結
- 5. 分布式Git
- 5.1 分布式工作流程
- 5.2 為項目作貢獻
- 5.3 項目的管理
- 5.4 小結
- 6. Git工具
- 6.1 修訂版本(Revision)選擇
- 6.2 交互式暫存
- 6.3 儲藏(Stashing)
- 6.4 重寫歷史
- 6.5 使用 Git 調試
- 6.6 子模塊
- 6.7 子樹合并
- 6.8 總結
- 7. 自定義Git
- 7.1 配置 Git
- 7.2 Git屬性
- 7.3 Git掛鉤
- 7.4 Git 強制策略實例
- 7.5 總結
- 8. Git與其他系統
- 8.1 Git 與 Subversion
- 8.2 遷移到 Git
- 8.3 總結
- 9. Git 內部原理
- 9.2 Git 對象
- 9.3 Git References
- 9.4 Packfiles
- 9.5 The Refspec
- 9.6 傳輸協議
- 9.7 維護及數據恢復
- 9.8 總結
- 9.1 底層命令 (Plumbing) 和高層命令 (Porcelain)