Subversion 和 GIT 開發者演進
前言
在開發軟件的過程中,往往是需要多個人參與,版本控制系統的協同工作的重要性不言而喻,除此之外, 版本控制軟件對整個開發流程的記錄對于缺陷追蹤也是非常重要的。版本控制系統也是軟件開發的基礎設施。
筆者開始接觸版本控制系統是大學的時候,最開始安裝了 TortoiseSVN ,然而 TortoiseSVN 僅僅是占據了硬盤空間而沒有發揮作用,很多開發者在接觸新事物的時候,并不一定會有極大的熱情去了解, 有的走了很多彎路后返回到了原地,有的深入以后,覺得其中異常的精彩。當我在 Windows 下編譯 LLVM 的時候, Subversion 開始發揮作用,彼時,幾乎所有開源的大型軟件都是使用 Subversion 進行托管,當然還有部分 CVS。 GIT 遠遠沒有目前流行。后來參加工作后,就是代碼托管的工作,對 Subversion 和 Git 有了一定程度的了解, 逐漸有了自己的思考。
大多數人對版本控制系統的解讀都是站在使用者的角度,而本文是基于一個代碼托管的開發者立場。
版本控制系統見聞
版本控制系統的歷史可以追溯到20世紀70年代,這是一個軍方開發的 CCC (變更和配置控制)系統,名字叫做 CA Software Change Manager 隨后,版本控制系統開始發展起來。
CVS 一度曾經是開源軟件的第一選擇,比如 GNOME、KDE、THE GIMP 和 Wine, 都曾使用過 CVS 來管理。這是一個集中式的版本控制系統,同樣是集中式的還有 Subversion, Visual SouceSafe Perforce,Team Foundation Server。
由于難以忍受 CVS,CollabNet 的開發者開發了著名的 Subversion(SVN) 來取代 CVS, Subversion 誕生于 2000 年, 時至今日,SVN 依然是最流行的集中式版本控制系統,GCC ,LLVM 等開源軟件都使用 SVN 管理,代碼托管網站方面, SourceForge 提供 SVN 的代碼托管。
Visual SouceSafe(VSS)是微軟開發的版本控制系統,到了 2008年,被 Team Foundation Server(TFS) 取代, TFS 并不是傳統意義的版本控制系統,而是云開發協作平臺,支持 Team Foundation Version Control 和 Git, 像微軟這樣的企業,無論是 Windows 還是 Office 還是 其他軟件,代碼量都非常巨大,只有像 TFS 這樣量身定做的系統才合適。
Perforce 是一個商業的版本控制系統,在其官網 www.perfoce.com 介紹, 有著超過10000個用戶使用他們的服務,有 NVIDIA ,Sumsuing,vmware,adidas 等著名企業,而我對他的印象在是 OpenWATCOM C/C++ 編譯器以及 p4merge 工具。p4merge 是 Perforce 提供的一個基于 Qt 開發的跨平臺比較工具。
與集中式版本控制系統對應的是分布式版本控制系統 (Distribution Version Control System) 比較流行的有 git 和 Mercurial, 二者均誕生于 2005 年。
Git 由 Linux 之父, Linus Torvalds 為了替代 BitKeeper 而開發的,關于 Git 的誕生,可以看對 Linus 本人的采訪: 10 Years of Git: An Interview with Git Creator Linus Torvalds Git 非常流行, Linux, FreeBSD, .NET Core CLR, .NET Core Fx, Minix, Android 等項目都使用 Git 來管理, Git 的社區非常成熟,有很多代碼托管網站提供托管服務,如 Github, Bitbucket, 國內有 OSC@GIT,coding,gitcafe, CSDN code, jd code 等等。
技術上同樣優秀的版本控制系統 Mercurial 的使用者少很多,也有著名的瀏覽器 Mozilla Firefox,服務器 Nginx,以及編程語言 Python。 Mercurial 使用 Python 實現,或許這一點也限制了 Mercurial 的發展。
在維基百科中有一個 VCS 列表: Template:Version control software 記錄了多種版本控制系統,誕生時間,分類。
大多數時候,開發者需要學習的版本控制系統為 Subversion 或者是 GIT。這二者已然是兩個版本控制流派的代表。
Git 技術內幕
本節主要介紹 Git 的存儲和傳輸
Git 存儲
git 倉庫在磁盤上可以表現為兩種形式,帶有工作目錄的普通倉庫和不帶工作目錄的裸倉庫。
我們可以創建一個標準倉庫:
mkdir gitrepo &&cd gitrepo &&git --init &&tree -a
目錄結構如下```sh . ├── .git │ ├── branches │ ├── COMMIT_EDITMSG │ ├── config │ ├── description │ ├── HEAD │ ├── hooks │ │ ├── applypatch-msg.sample │ │ ├── commit-msg.sample │ │ ├── post-update.sample │ │ ├── pre-applypatch.sample │ │ ├── pre-commit.sample │ │ ├── prepare-commit-msg.sample │ │ ├── pre-push.sample │ │ ├── pre-rebase.sample │ │ └── update.sample │ ├── index │ ├── info │ │ └── exclude │ ├── logs │ │ ├── HEAD │ │ └── refs │ │ └── heads │ │ └── master │ ├── objects │ │ ├── 89 │ │ │ └── 50b8b1af3c4cc712edb5a995c83a53eb03e6be │ │ ├── d0 │ │ │ └── 2d9281b58703d020c3afe3e2ace204d6d462ae │ │ ├── e6 │ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │ │ ├── info │ │ └── pack │ └── refs │ ├── heads │ │ └── master │ └── tags └── helloworld
```
實際上我們創建一個裸倉庫會發現和普通倉庫的 .git 目錄結構是一致的。
mkdir gitbare.git &&cd gitbare.git &&tree -a
目錄結構:```sh . ├── branches ├── config ├── description ├── HEAD ├── hooks │ ├── applypatch-msg.sample │ ├── commit-msg.sample │ ├── post-update.sample │ ├── pre-applypatch.sample │ ├── pre-commit.sample │ ├── prepare-commit-msg.sample │ ├── pre-push.sample │ ├── pre-rebase.sample │ └── update.sample ├── info │ └── exclude ├── objects │ ├── info │ └── pack └── refs ├── heads └── tags
9 directories, 13 files
```
當我們創建一個倉庫時,默認情況下會創建工作目錄,在工作目錄下有個 .git 的子目錄,這才是存儲庫的目錄。 而我們通常修改代碼的目錄稱之為工作目錄。
總所周知,git 是分布式版本控制系統,這就意味著,只要獲得了 .git 目錄的完整數據,就可以在任意位置恢復成一個帶有工作目錄的倉庫。 對于 Subversion 一樣的集中式版本控制系統,就相當于 .git 目錄被托管在中央服務器上,而本地的 .svn 只是工作目錄的元數據。 二者不同的機制帶來的直接差別就是一旦中央服務器宕機,git 可以迅速的遷移到其他服務器,并且數據的丟失的可能性很小, 而 Subversion 服務器就沒有這么好的運氣了。
每一次提交,git 都會把修改的文件快照,還有更新的目錄結構,以及提交信息,打包成一個個 object,這些 object 被loose object, 所以 git 的 object 可能是 blob tree commit 等。打包的過程會使用 zip 壓縮,這種被廣泛運用的壓縮格式實上壓縮率較低,壓縮速度也慢,但好處有廣泛的支持,專利上比較友好。
如果調用 git gc 命令后,git-gc 會將這些 object 打包成 pack 文件,這些內容在 proGit 都有詳細說明。
Git 傳輸協議
Git 支持多種協議 http, git , ssh, file ,以內部機制區分為啞協議和智能協議,啞協議非常簡單,簡單的說, 客戶端通過 URL 直接拿取服務端的文件。Git 智能協議實現了兩類 RPC 調用,一個是 fetch-pack<->upload-pack, 另一個是 send-pack<->receive-pack。
任何 Git 遠程操作都需要獲得遠程倉庫的引用列表,與自身的引用列表進行比對
這里以 HTTP 為例
1 Fetch-Upload
Step 1:
Request
`
Response
`
Step 2:
Request
`
Response
`
2 Send-Receive 實際上 push 的過程也是 GET 和 POST, 只不過,git-upload-pack 要變成 git-receive-pack ,POST 時,后者請求體中包含有 差異 package。
對于 git HTTP 來說,權限驗證通常是 HTTP 的一套,也就是 WWW-Authenticate, 絕大多數的 HTTP 服務器也就支持 Basic。
即:
user:password ->Base64 encode -->dXNlcjpwYXNzd29yZA==
所以從安全上來說,如果使用 HTTP 而不是 HTTPS , 對 GIT 遠程倉庫進行寫操作簡直就是在裸奔。
git HTTP 支持的 HTTP 返回碼并不多,這些是返回碼是支持的: 200 30x 304 403 404 410
關于 HTTP 的更多文檔細節可以去這個地址查看: HTTP Protocol
基于 HTTP 的智能協議和基于 SSH,Git 協議本質上并無太大的不同,都是通過這兩類 RPC 調用,實現本地倉庫和遠程倉庫的數據交換。
HTTP 協議是通過 http smart server 運行 git-xxx-pack,對其輸入數據,然后讀取 git-xxx-pack 輸出。 SSH 則是通過 ssh 服務器在遠程機器上運行 git-xxx-pack ,數據傳輸的過程使用 SSH 加密。 而 GIT 協議 (git://) 協議則是 通過遠程服務器 git-daemon 運行 git-xxx-pack 實現數據的交互。通常來說 git:// 無法實現差異化的權限管理, 也就是要么全部只讀,全部可寫。
git help daemon
一些更多的技術內幕可以參考 社區大作 《Pro Git》
Git 代碼托管平臺的開發演進
雖然 GIT 是分布式版本控制,但是對于代碼托管平臺來說又是一回事了。對于 HTTP 協議來說,像 NGINX 一樣的服務器只需要實現動態 IP, 然后通過 proxy 或者是 upstream 的方式實現 GIT 代碼托管平臺的 分布式就可以了。但是對于 SSH 來說比較麻煩。
基于 RPC 的 GIT 分布式設計
客戶端訪問倉庫時,路由智能到達 DNS 所記錄的機器或者是無差別代理的機器(前端機器),往往不能到達特定的存儲機器, 開發者使用分布式文件系統或者 分布式 RPC 或者代理等多種方案實現 前端到存儲的關鍵一步。這里主要說分布式 RPC 與 GIT smart 的應用。
分布式 RPC 框架很多,其中著名的有 Apache Thrift ,此項目是 非死book 開源并貢獻給 Apache 基金會的,支持多種語言。
對于 GIT 操作,只需要實現 4個函數。一下是 Thrift 接口文件的一部分:
`
然后存儲服務器通過 pipe 讀取存儲機器上的 git-upload-pack /git-receive-pack 的輸入輸出。 在 Linux 上通過管道讀取 git upload-pack 的輸出:
`
前端服務器上,編寫 模擬 git-upload-pack 或者是 git-receive-pack 的程序。用戶通過 ssh 訪問遠程倉庫時執行的 git 工具變成了模擬后的 git-upload-pack /git-receive-pack, 當使用 HTTP 訪問時,可以整合成 RPC 客戶端整合直接整合進 HTTP 服務器,比如 NGINX 模塊, 或者也可 使用 傳統的 Git Smart HTTP 庫的方式,總的來說 Thrift 有多種語言支持,Git Smart HTTP 整合 Thrift RPC 并不成問題。這個唯一的問題是實現異步比較麻煩,兩者都需要實現異步模式,git 倉庫可能非常大,一次性克隆傳輸數據幾百 MB 或者上 GB, 這個時候 4nK 發送非常必要。
基于 libgit2 的 smart 協議實現
GIT 除了 Linus 本人實現,kernel.org 托管的官方版本外,還有 jgit,libgit2 等,git 是一系列命令組成,幾乎沒有剝離出共享庫的能力, 這樣的后果導致其他語言使用 git 時,不得不使用管道等進程間通訊的模式與 git 工具交互。而 jgit 使用 Java 實現,基本上沒有其他流行語言的綁定能力。libgit2 是一個 GIT 的兼容實現,基于 C89 開發,支持絕大多數 git 特性。開發非常活躍,有多種語言綁定,如 C# Ruby 等, 其中 C# 綁定 Libgit2Sharp 被 VisualStudio, Github for Windows 等使用,而 Ruby 綁定 Rugged ,被 Github, GIT@OSC 等代碼托管平臺使用。
libgit2 并沒有合適的 GIT smart 服務器后端實現,多數情況下,libgit2 主要面向的是客戶端,由于 git 是分布式的,對于倉庫的讀寫也就客戶端 和服務器的行為也是類似的。
Subversion 內幕
此部分中 SVN 協議 指 Apache Subversion 程序 svn(以及兼容的客戶端) 與遠程服務器上的 Apache Subversion svnserve (以及兼容的服務器) 進程通訊的協議, 即 Subversion protocol,協議默認端口是 3690,基于 TCP, 傳輸數據使用 ABNF 范式。
在這里支出,與 Git 完全不同的是,svn 的倉庫存儲在遠程中央服務器上,開發者檢出的代碼只是特定版本,特定目錄的代碼,本地為工作拷貝。
Subversion HTTP 協議實現
Subversion HTTP 協議是一種 基于 WebDAV/DeltaV 的協議,WebDAV 在 HTTP 1.1 的基礎上擴展了多個 Method, 絕大多數的服務器并不支持 WebDAV, 這樣的后果就是,除了 Apache 可以使用 mod dav svn 插件,基本上再也沒有其他的服務器能快速的支持 Subversion 的 HTTP 協議了。代理還是可以的。
WebDAV 協議在 HTTP 1.1 的基礎上 使用 XML 的方式呈現數據,對于 Subversion 這種集中式版本控制系統來說,絕大多數操作都是在線的, WebDAV 包裹這些操作就變得很繁瑣。
比如一個 update-report 請求:
`
然后服務器返回:
`
不同的請求,xml 的內容也完全不同,Subversion HTTP 協議的復雜也讓很多開發者望而卻步。
在 Subversion 的路線圖中,基于 WebDAV/DeltaV 的 HTTP 接入將被 基于 HTTP v2 的實現取代。
A Streamlined HTTP Protocol for SubversionSubversion SVN 協議實現
與 HTTP 不同的是,一個完整的基于 SVN 協議的連接中,倉庫的操作是上下文相關的。當客戶端的連接過來時,服務器,通常說的 svnservice 將發送一段信息給客戶端,告知服務器的能力。
`
Example:
`
這個時候客戶端獲知了這些數據,如果無法兼容,服務器,那么將斷開與服務器的連接,否則,將發送請求數據給服務器,格式如下:
`
Example:
`
與 GIT 數據包類似的地方有一點,git 每一行數據前 4 個16進制字符代表本行的長度,而 這里的 10 進制字符代表 字符的長度,比如 URL 長度36,UA 53。
服務器此時的行為就得通過解析 URL 獲得中央倉庫的位置,判斷協議是否兼容,而 UA 有可能為空,格式并不是非常標準,所以這是值得注意的地方。
服務器將決定使用那種授權方式,MD5 一般是 Subversion 客戶端默認的,無法第三方庫支持,而 PLAIN 和 ANONYMOUS 需要 SASL 模塊的支持, 在 Ubuntu 上編譯 svn,先安裝 libsasl2-dev。
`
客戶端不支持此授權方式時,會輸出錯誤信息,“無法協商驗證方式”
這里的 Realm 是 subversion 客戶端存儲用戶賬戶用戶名和密碼信息的一個 key,只要 realm 一致,就會取相同的 用戶名和密碼。 realm RFC2617
Example:
`
如果是 MD5 ,驗證協商如下:
` 這個 Token 是隨機生成的 UUID, C++ 可以使用 boost 生成,也可以使用平臺的 API 生成。
如果是 PLAIN 授權機制,這里就是用戶名和密碼經 Base64 編碼了, 用 NUL(0) 分隔
usernameNULpassword --> Base64 Encoded
Example:
`
對于純 svn 協議來說,使用 PLAIN 并不安全,且當 Subversion 只作為 GIT 代碼托管平臺的一個服務來說, 使用 CRAM-MD5 并不利于服務整合,這也是一個缺陷了。
這是服務器的下一步驟: `
Incorrect credentials:
`
Success
`
隨后服務器再發送存儲庫 UUID, capabilities 給客戶端 `
Example:
`
如果是 svn up/commit 或者其他的操作,這個時候會檢查 uuid 是否匹配,當然也會檢查 URL 是否匹配。
如果客戶端覺得一切都 OK 啦,那么就會開始下一階段的操作,command 模式,這些規則可以從 Subversion 官方存儲庫查看 Subversion Protocol
與 GIT 或者 SVN HTTP 不同的是,一個完整的 基于 svn 協議的 SVN 操作,只需要建立一次 socket,Subversion 客戶端此時是阻塞的,并且屏蔽了 Ctrl+C 等 信號, 倉庫體積巨大時,這種對連接資源的占用非常突出,因為有數據讀取, socket 并不會超時。這樣的機制使得 svn 服務器的并發受到了限制。
Subversion 兼容實現
Github 基于 HTTP 協議的方式實現了對 Subversion 的兼容,而 GIT@OSC 基于 svn 協議方式實現了對 Subversion 的不完全兼容。
Subversion 協議代理服務器的實現
前面并不完全的分析了 SVN 協議,但是那些協議內容足夠實現一個 SVN 協議動態代理服務器了。
在客戶端 C 和代理服務器 S 建立連接后, S 向 C 發送一個數據包:
- 服務器頭 ```sh
S to C
( success ( 2 2 ( ) ( edit-pipeline svndiff1 absent-entries depth inherited-props log-revprops ) ) ) ```
C 接收到 S 的數據后,必須做出選擇,并發送第一個請求給 S。```sh
C to S
( 2 ( edit-pipeline svndiff1 absent-entries depth mergeinfo log-revprops ) 43:svn://subversion.io/apache/subversion/trunk 53:SVN/1.8.13-SlikSvn-1.8.13-X64 (x64-microsoft-windows) ( ) ) ```
S 接收到 C 的請求后,解析 數據包,提取到 URL 為 svn://subversion.io/apache/subversion/trunk , 而 Gitlab 的規則是 host/user/repo, 如果不同用戶的存儲庫放在不同機器上,這個時候提取到用戶為 apache, 交由路由選擇模塊去處理得到后端的地址,也就是真實 svnserve 的 IP 和端口。
建立與后端服務器 B 的連接。這個時候 S 讀取 B 的數據包,也就是前面的服務器頭,接收完畢直接丟棄即可,然后將客戶端 C 的頭請求轉發給后端服務器。```sh
S to B
( 2 ( edit-pipeline svndiff1 absent-entries depth mergeinfo log-revprops ) 43:svn://subversion.io/apache/subversion/trunk 53:SVN/1.8.13-SlikSvn-1.8.13-X64 (x64-microsoft-windows) ( ) ) ```
至此,代理服務器的后面就不必關系細節了,GIT@OSC 使用 Boost.ASIO 異步框架,
`
一個基本的 SVN 協議動態代理服務器就實現了。
結尾
如果你不是專業的 Git 或者 Subversion 開發者,你可能會覺得上面的內容沒什么用處,實際上也沒什么技術難度。
來自: http://my.oschina.net/GIIoOS/blog/597134?fromerr=nwQr03GF