標籤:

版本控制系統

【遷移】版本控制系統

本系列第二十篇,給 coder 最愛的工具:Version Control System。當今流行的開源版本控制系統並不多: Subversion、 GIT、Mercurial、Bazaar。CVS 就淡忘了吧,雖然依然有很多人用,但實在是不能說流行了,或者更準確點,「時髦」。

個人覺得一個 VCS 應該具備兩點最核心的特性:

  • 提交的原子性:一次提交多個文件的修改,要麼全部提交成功,要麼全部提交失敗;一次提交能作為一個整體容易的查找出來並一個操作就撤銷掉。CVS 和 Visual Source Safe 是這方面的失敗者。
  • 不需要管理員授權就可以隨意的創建私有分支。

其它諸如圖形界面、重命名的支持、高級的合併演算法,都是錦上添花,沒有並不會大幅降低工作效率。上面第二個特性可能很多 Subversion 愛好者不以為然,那是你們被虐習慣了而已。有一個版本控制工具叫 SVK,構建在 Subversion 基礎上,可以在本地創建私有分支,基本上是 DVCS 的路子,但是現在已經處於維護階段,不再繼續開發了。

本來想再羅嗦幾句 DVCS 的含義,寫了好幾段,發現太意識流,刪除了。。。且簡略說一下 GIT 的一些特點,因為我覺得 GIT 是最純粹的 DVCS 工具。

  • GIT 里版本圖是 commit 根據父子關係形成的單向無循環圖,這裡每個節點以 commit SHA1 做標記;
  • git fetch/push 是在不同開發者之間同步這個版本圖的過程,最終大家能看到一個公共的、一致的版本圖;
  • GIT 里 branch 名字準確說是 branch head 的名字,跟 tag 一樣,都是特定開發者打下的私人的標記,標記本身不在版本歷史裡。其它人由於承認他的權威,接受他的標記,而標記本身並不影響版本圖裡 commit 的父子關係,所以去掉這些標記也是沒關係的,只是大家要以 SHA1 值來稱呼 branch head 和 tag 了。不同人起同樣的 branch head 和 tag name 是沒有關係的,對於 branch head,由於有 remote 名字空間區分,所以不會衝突,對於 tag,可以簽名,所以衝突了也能識別出來。(這個地方跟 Mercurial 的做法有本質區別,Mercurial 里 branch head 的名字,tag 名字都是計入版本歷史的,一個 branch head 名字被使用後,其它人不能再用同名的 branch,除非不合併)。
  • GIT 里兩個 branch head 合併後,兩個 branch head 名字指向同一個 branch head,其實就成了一個分支,這種分支合併而非僅僅修改合併,對於 DVCS 是有本質意義的,否則分散式開發里 branch head 會越來越多,無法收拾。對於非權威開發者,合併後要麼丟棄自己的標記(因為它是多餘的),要麼繼續保留作為自己的本地私人標記,方便自己識別,總之,不會公開出去。

又說了一大段。。。。核心想法是 GIT 里版本圖是「修改」構成的,不受「標記」的影響,因為技術上別人只關心你改了什麼,不關心你打了什麼標記,標記只是社會地位的體現,跟技術無關,所以 GIT 的版本圖裡是沒有「中心」一說的,比如 Linus Torvalds 很長時間沒修改 Linux 內核,然後合併了其它人的分支頭,那麼 Linus Torvalds 打的 branch head 標記會被 fast forward 機制修改成直接指向其它人標記的 branch head,並不產生新的 commit,從這個 branch head 往回看,其 commit 的 first parent 構成的線並不是被 Linus Torvalds 主導,也就是說:你貢獻的多,你就是主導,而非你權威,你就是主導。這跟 Subversion 的做法是有本質區別的。

laf,還是意識流了。。。。。

繼續羅嗦一句,有人可能誤解 DVCS 不能用在公司裡頭做集中式版本管理,其實是完全可以的,參考 linux kernel 的分支發展歷史,有權威就有集中控制。

回到正題。。。。公司里應該選擇什麼 VCS:

  • 開發人員太弱,不求上進,或者代碼規模很小,並且開發團隊不大,並且代碼維護時間不會很長,那麼可以用 Subversion,伊的 TortoiseSVN 目前是 Tortoise 系列最成功的一款。Subversion 的弊病在於建立私有分支不便、合併支持較弱、不支持重複文件去重從而版本庫體積容易增長迅速(有本地直接 copy 然後 svn add 的兄弟嗎?)。
  • 否則如果很重視 Windows 支持或者受不了 GIT 的「豐富」命令行 UI,那麼用 Mercurial,否則用 GIT。Mercurial 的主要缺陷是 branch name、tag 計入版本歷史的設計。當然,肯定有人會爭論這不是缺點,但從 Mercurial 對 branch 特性的改進以及 bookmark 擴展的提出,說明抱怨是頗有一些的,反觀 GIT,blob/commit/tree/tag/branch 的設計從一開始到現在,思路完全沒變,怪不得 Linus 說"In other words, Im right. Im always right, but sometimes Im more right than other times. And dammit, when I say "files dont matter", Im really really Right(tm). ",而且 GIT 現任保姆 Junio 說 "For people new to the list, the message is: thread.gmane.org/gmane., I think Ive quoted this link at least three times on this list;I consider it is _the_ most important message in the whole list archive. If you havent read it, read it now, print it out, read it three more times, place it under the pillow before you sleep tonight. Repeat that until you can recite the whole message. It should not take more than a week."

至於 bazaar,儘可能躲開吧,大概半年前我體驗的時候依然很慢,另外 bazaar 的設計很有問題,bazaar 和 git 開發者早期有大量大量的口水戰,git 開發者認為 bazaar 的 branch 設計壓根就不是 DVCS 的設計。我記得 bazaar 的存儲方式頻繁的修改了好幾版,最後改成跟 git 的 object store 很類似的方式。。。個人偏見,bazaar 表面偽裝的很易用,骨子裡設計的很繁複。

雖然我很推崇 GIT,它當然也是有煩人的地方:命令行 UI 終是難讓人滿意,不是說命令行不好,而是其命令功能、選項設計不夠簡潔,這點 Mercurial 做的不錯;沒有 hg serve 那樣的內置 web server,git instaweb 需要外置 web server;git submodule 不好用,Google 的 repo 工具改進的很有創意。

BTW,提兩個 GIT 里容易被人忽略的節約帶寬和磁碟的特性:git clone 的 --depth 選項,可以不用 clone 整個版本歷史;git help read-tree 提到的 Sparse checkout(GIT >= 1.7.0),可以讓 read-tree, checkout, merge 只操作某些目錄,而非整個文件樹。

服務 GIT 代碼庫,現在最流行的方案是 Gitolite,基於 ssh 協議提供可寫訪問,之前有一個 Gitosis,已然過氣了。另外 git 新版有一個 http-backend 命令,可以提供高效的 git over http 傳輸,但用的不廣泛,原因不明。

轉一篇 bazaar 相關的討論,原帖轉移到 wordpress 不好訪問了。

bzr 的分支管理

在 NewSMTH Emacs 版跟 SuperMMX 討論了下 Bazaar,之前看其手冊沒能理解透徹

的地方明白了許多。

1. Bazaar 的三段式版本號是取決於參照哪個分支說的,命名規則是(SuperMMX 語):

第一個數字是哪個版本上開始拉出的分支。第二個數字表示是從這個版本上拉出來的

分支是第幾個合併過來的,只要祖先是這個版本的都算。第三個數字就是在拉出來的

分之上第幾次提交。

1 2 3
| | |
a
|
b
|
c e
| |
d f g
| |/
| h
|/
i

時間線從上往下,直線表示一個分支上的提交, 表示拉出分支的第一次提交,/ 表示合併

上面的 1、2、3 表示不同的分支,以後把它們叫做 b1、b2、b3,分別代表 a、

e 和 g 所在的分支。

a、b、c、d 分別是 1、2、3、4,這個比較好理解。

那麼 e 呢?這得看你站在哪個角度上去看,站在 e 所在的分支本身 b2 去看,

那麼 e 就應該是 3,f 是 4,h 是 5。

同樣,g 在自己所在的分支上 b3 看是 4,那當合併到 b2 上以後,就不應該是

4 了,因為已經有了 f。注意,現在已經是站在 e 所在分支 b2 的角度去看問題了,

g 所在的分支 b3 是從 e 拉出來的,所以 g 的第一個數字應該是 3,而 b3 是

第 1 個在 b2 上所合併的分支,所以第二個數字是 1,g 又是 b3 上的第一次

提交,所以那麼 g 在 b2 的版本號就是 3.1.1。

然後 d 和 h 合併成 i 之後,站在 a 所在的分支 b1 角度去看 e、f、h 和 g

分別應該是什麼。很明顯,e、f、h 的第一個數字和第二個數字是一樣的,因為它

們都是同一個分支上的,而這個分支是從 b 拉出來的,b 的版本號是 2,那麼第

一個數字就是 2。因為 b2 是 b1 上第一個合併的分支,所以第二個數字是 1。

最後的數字他們分別是 1、2、3 了,所以 e、f、h 分別是 2.1.1、2.1.2

以及 2.1.3。

再看 g,它跟 e、f、h 都是從 b 出來的,那麼第一個數字也是 2。第二個數字

因為 b1 已經包含了 b2 了,所以它應該是 b1 上第二個合併的分支,所以應該是 2。

第三個數字是 1。所以 g 最後在 b1 上就是 2.2.1。

單純從數字上看是沒有辦法區分是分支還是分支的分支的。

2. init-repo 和 init 的區別

倉庫(Repository)就是存儲版本(Revision)的,預設情況 bzr init 是把

一個目錄變成 Bazaar 所控制的分支,至於真正的倉庫在哪裡,就看是不是在

共享倉庫(Shared Repository)裡面,而共享倉庫必須由 bzr init-repo

來創建。如果所創建的分支是在共享倉庫里,那麼這個分支的版本數據就存在

共享倉庫中,否則就是在自己的分支倉庫中。也就是說,在共享倉庫中,

所有分支的所有版本都只有一份。

舉個例子,現在遠端倉庫里 main branch 的版本是 100,我在本地需要三個分支

同時來干不同的事。如果用一般的倉庫的話,bzr branch 三次,那麼前 100 個

版本在三個分支中都存在,但如果用共享倉庫的話,就只有一份。

// bzr init-repo 是建立一個地方供多個分支存放版本記錄,bzr init 是將一個目錄

// 納入版本管理。

3. checkout 和 branch 的關係

checkout 就是和遠端綁定的 working tree,直接提交到遠端,而不需要再

push 了,通過 bind/unbind 可以在 checkout 和 branch 之間轉換。

預設的 checkout 是 heavy checkout,也就是歷史數據都拿得到,diff 之類

的命令都不要和遠端打交道,只是 update/commit 才連接遠端。而 lightweight

checkout 可就跟 CVS/SVN 基本上就一樣了,幹個啥都要連遠端。

所以 Bazaar 推薦使用共享倉庫,所有的分支都在共享倉庫下,包括遠端

和本地的。

// checkout 是 CVS 式,區別在於 heavy checkout 時可以本地緩存版本記錄,

// 提交到遠程伺服器上,除非 commit –local 或者 unbind 以轉成一個 branch。

// branch 是 DVCS 式, 提交到本地,然後 push 到遠程伺服器,用 bind 將一個

// branch 轉成 checkout。說白了,checkout 和 branch 就是指一個工作目錄

// 是跟本地分支關聯還是跟遠程分支關聯。

4. bzr 有 switch 命令,這樣能一個工作目錄對應多個分支。

推薦閱讀:

TAG:Linux |