Git 前時代:使用 CVS 進行版本控制

2018-12-06 18:50:00

GitHub 網站發布於 2008 年。如果你的軟體工程師職業生涯跟我一樣,也是晚於此時間的話,Git 可能是你用過的唯一版本控制軟體。雖然其陡峭的學習曲線和不直觀地使用者介面時常會遭人抱怨,但不可否認的是,Git 已經成為學習版本控制的每個人的選擇。Stack Overflow 2015 年進行的開發者調查顯示,69.3% 的被調查者在使用 Git,幾乎是排名第二的 Subversion 版本控制系統使用者數量的兩倍。1 2015 年之後,也許是因為 Git 太受歡迎了,大家對此話題不再感興趣,所以 Stack Overflow 停止了關於開發人員使用的版本控制系統的問卷調查。

GitHub 的發布時間距離 Git 自身發布時間很近。2005 年,Linus Torvalds 發布了 Git 的首個版本。現在的年經一代開發者可能很難想象“版本控制軟體”一詞所代表的世界並不僅僅只有 Git,雖然這樣的世界誕生的時間並不長。除了 Git 外,還有很多可供選擇。那時,開源開發者較喜歡 Subversion,企業和電動遊戲公司使用 Perforce (到如今有些仍在用),而 Linux 核心專案依賴於名為 BitKeeper 的版本控制系統。

其中一些系統,特別是 BitKeeper,會讓年經一代的 Git 使用者感覺很熟悉,上手也很快,但大多數相差很大。除了 BitKeeper,Git 之前的版本控制系統都是以不同的架構模型為基礎執行的。《Version Control By Example》一書的作者 Eric Sink 在他的書中對版本控制進行了分類,按其說法,Git 屬於第三代版本控制系統,而大多數 Git 的前身,即流行於二十世紀九零年代和二十一世紀早期的系統,都屬於第二代版本控制系統。2 第三代版本控制系統是分散式的,第二代是集中式。你們以前大概都聽過 Git 被描述為一款“分散式”版本控制系統。我一直都不明白分散式/集中式之間的區別,隨後自己親自安裝了一款第二代的集中式版本控制元件系統,並做了相關實驗,至少明白了一些。

我安裝的版本系統是 CVS。CVS,即 “並行版本系統Concurrent Versions System” 的縮寫,是最初的第二代版本控制系統。大約十年間,它是最為流行的版本控制系統,直到 2000 年被 Subversion 所取代。即便如此,Subversion 被認為是 “更好的 CVS”,這更進一步突出了 CVS 在二十世紀九零年代的主導地位。

CVS 最早是由一位名叫 Dick Grune 的荷蘭科學家在 1986 年開發的,當時有一個編譯器專案,他正在尋找一種能與其學生合作的方法。3 CVS 最初僅僅只是一個包裝了 RCS(修訂控制系統Revision Control System) 的 Shell 指令碼集合,Grune 想改進這個第一代的版本控制系統。 RCS 是按悲觀鎖模式工作的,這意味著兩個程式設計師不可以同時處理同一個檔案。需要編輯一個檔案話,首先得向 RCS 系統請求一個排它鎖,鎖定此檔案直到完成編輯,如果你想編輯的檔案有人正在編輯,你就必須等待。CVS 在 RCS 基礎上改進,並把悲觀鎖模型替換成樂觀鎖模型,迎來了第二代版本控制系統的時代。現在,程式設計師可以同時編輯同一個檔案、合併編輯部分,隨後解決合併衝突問題。(後來接管 CVS 專案的工程師 Brian Berliner 於 1990 年撰寫了一篇非常易讀的關於 CVS 創新的 論文。)

從這個意義上來講,CVS 與 Git 並無差異,因為 Git 也是執行於樂觀鎖模式的,但也僅僅只有此點相似。實際上,Linus Torvalds 開發 Git 時,他的一個指導原則是 WWCVSND,即 “CVS 不能做的What Would CVS Not Do”。每當他做決策時,他都會力爭選擇那些在 CVS 設計裡沒有使用的功能選項。4 所以即使 CVS 要早於 Git 十多年,但它對 Git 的影響是反面的。

我非常喜歡折騰 CVS。我認為要弄明白為什麼 Git 的分散式特性是對以前的版本控制系統的極大改善的話,除了折騰 CVS 外,沒有更好的辦法。因此,我邀請你跟我一起來一段激動人心的旅程,並在接下來的十分鐘內了解下這個近十年來無人使用的軟體。(可以看看文末“修正”部分)

CVS 入門

CVS 的安裝教學可以在其 專案主頁 上找到。MacOS 系統的話,可以使用 Homebrew 安裝。

由於 CVS 是集中式的,所以它有用戶端和伺服器端之區分,這種模式 Git 是沒有的。兩端分別有不同的可執行檔案,其區別不太明顯。但要開始使用 CVS 的話,即使只在你的本地機器上使用,也必須設定 CVS 的服務後端。

CVS 的後端,即所有程式碼的中央儲存區,被叫做儲存庫 repository。在 Git 中每一個專案都有一個儲存庫,而 CVS 中一個儲存庫就包含所有的專案。儘管有辦法保證一次只能存取一個專案,但一個中央儲存庫包含所有東西是改變不了的。

要在本地建立儲存庫的話,請執行 init 命令。你可以像如下所示在家目錄建立,也可以在你原生的任何地方建立。

$ cvs -d ~/sandbox init

CVS 允許你將選項傳遞給 cvs 命令本身或 init 子命令。出現在 cvs 命令之後的選項預設是全域性的,而出現在子命令之後的是子命令特有選項。上面所範例子中,-d 標誌是全域性選項。在這兒是告訴 CVS 我們想要建立儲存庫路徑在哪裡,但一般 -d 標誌指的是我們想要使用的且已經存在的儲存庫位置。一直使用 -d 標誌很單調乏味,所以可以設定 CVSROOT 環境變數來代替。

因為我們只是在本地操作,所以僅僅使用 -d 參考來傳遞路徑就可以,但也可以包含個主機名。

此命令在你的家目錄建立了一個名叫 sandbox 的目錄。 如果你列出 sandbox 內容,會發現下面包含有名為 CVSROOT 的目錄。請不要把此目錄與我們的環境變數混淆,它儲存儲存庫的管理檔案。

恭喜! 你剛剛建立了第一個 CVS 儲存庫。

檢入程式碼

假設你決定留存下自己喜歡的顏色清單。因為你是一個有藝術傾向但很健忘的人,所以你鍵入顏色列表清單,並儲存到一個叫 favorites.txt 的檔案中:

blueorangegreendefinitely not yellow

我們也假設你把檔案儲存到一個叫 colors 的目錄中。現在你想要把喜歡的顏色列表清單置於版本控制之下,因為從現在起的五十年間你會回顧下,隨著時間的推移自己的品味怎麼變化,這件事很有意思。

為此,你必須將你的目錄匯入為新的 CVS 專案。可以使用 import 命令:

$ cvs -d ~/sandbox import -m "" colors colors initialN colors/favorites.txtNo conflicts created by this import

這裡我們再次使用 -d 標誌來指定儲存庫的位置,其餘的引數是傳輸給 import 子命令的。必須要提供一條訊息,但這兒沒必要,所以留空。下一個引數 colors,指定了儲存庫中新目錄的名字,這兒給的名字跟檢入的目錄名稱一致。最後的兩個引數分別指定了 “vendor” 標籤和 “release” 標籤。我們稍後就會談論標籤。

我們剛將 colors 專案拉入 CVS 儲存庫。將程式碼引入 CVS 有很多種不同的方法,但這是 《Pragmatic Version Control Using CVS》 一書所推薦方法,這是一本關於 CVS 的程式設計師實用指導書籍。使用這種方法有點尷尬的就是你得重新檢出check out工作專案,即使已經存在有 colors 此專案了。不要使用該目錄,首先刪除它,然後從 CVS 中檢出剛才的版本,如下示:

$ cvs -d ~/sandbox co colorscvs checkout: Updating colorsU colors/favorites.txt

這個過程會建立一個新的目錄,也叫做 colors。此目錄裡會發現你的原始檔 favorites.txt,還有一個叫 CVS 的目錄。這個 CVS 目錄基本上與每個 Git 儲存庫的 .git 目錄等價。

做出改動

準備旅行。

和 Git 一樣,CVS 也有 status 命令:

$ cvs statuscvs status: Examining .===================================================================File: favorites.txt     Status: Up-to-date   Working revision:    1.1.1.1 2018-07-06 19:27:54 -0400   Repository revision: 1.1.1.1 /Users/sinclairtarget/sandbox/colors/favorites.txt,v   Commit Identifier:   fD7GYxt035GNg8JA   Sticky Tag:      (none)   Sticky Date:     (none)   Sticky Options:  (none)

到這兒事情開始陌生起來了。CVS 沒有提交物件這一概念。如上示,有一個叫 “提交識別符號Commit Identifier” 的東西,但這可能是一個較新版本的標識,在 2003 年出版的《Pragmatic Version Control Using CVS》一書中並沒有提到 “提交識別符號” 這個概念。 (CVS 的最新版本於 2008 年發布的。5

在 Git 中,我們所談論某檔案版本其實是在談論如 commit 45de392 相關的東西,而 CVS 中檔案是獨立版本化的。檔案的第一個版本為 1.1 版本,下一個是 1.2 版本,依此類推。涉及分支時,會在後面新增擴充套件數位。因此你會看到如上所示的 1.1.1.1 的內容,這就是範例的版本號,即使我們沒有建立分支,似乎預設的會給加上。

一個專案中會有很多的檔案和很多次的提交,如果你執行 cvs log 命令(等同於 git log),會看到每個檔案提交歷史資訊。同一個專案中,有可能一個檔案處於 1.2 版本,一個檔案處於 1.14 版本。

繼續,我們對 1.1 版本的 favorites.txt 檔案做些修改(LCTT 譯註:原文此處範例有誤):

blueorangegreencyandefinitely not yellow

修改完成,就可以執行 cvs diff 來看看 CVS 發生了什麼:

$ cvs diffcvs diff: Diffing .Index: favorites.txt===================================================================RCS file: /Users/sinclairtarget/sandbox/colors/favorites.txt,vretrieving revision 1.1.1.1diff -r1.1.1.1 favorites.txt3a4> cyan

CVS 識別出我們我在檔案中新增了一個包含顏色 “cyan” 的新行。(實際上,它說我們已經對 “RCS” 檔案進行了更改;你可以看到,CVS 底層使用的還是 RCS。) 此差異指的是當前工作目錄中的 favorites.txt 副本與儲存庫中 1.1.1.1 版本的檔案之間的差異。

為了更新儲存庫中的版本,我們必須提交更改。Git 中,這個操作要好幾個步驟。首先,暫存此修改,使其在索引中出現,然後提交此修改,最後,為了使此修改讓其他人可見,我們必須把此提交推播到源儲存庫中。

而 CVS 中,只需要執行 cvs commit 命令就搞定一切。CVS 會匯集它所找到的變化,然後把它們放到儲存庫中:

$ cvs commit -m "Add cyan to favorites."cvs commit: Examining ./Users/sinclairtarget/sandbox/colors/favorites.txt,v <-- favorites.txtnew revision: 1.2; previous revision: 1.1

我已經習慣了 Git,所以這種操作會讓我感到十分恐懼。因為沒有變更暫存區的機制,工作目錄下任何你動過的東西都會一股腦給提交到公共儲存庫中。你有過因為不爽,私下裡重寫了某個同事不佳的函數實現,但僅僅只是自我宣洩一下並不想讓他知道的時候嗎?如果不小心提交上去了,就太糟糕了,他會認為你是個混蛋。在推播它們之前,你也不能對提交進行編輯,因為提交就是推播。還是你願意花費 40 分鐘的時間來反復執行 git rebase -i 命令,以使得本地提交歷史記錄跟數學證明一樣清晰嚴謹?很遺憾,CVS 裡不支援,結果就是,大家都會看到你沒有先寫測試用例。

不過,到現在我終於理解了為什麼那麼多人都覺得 Git 沒必要搞那麼複雜。對那些早已經習慣直接 cvs commit 的人來說,進行暫存變更和推播變更操作確實是毫無意義的差事。

人們常談論 Git 是一個 “分散式” 系統,其中分散式與非分散式的主要區別為:在 CVS 中,無法進行本地提交。提交操作就是向中央儲存庫提交程式碼,所以沒有網路連線,就無法執行操作,你原生的那些只是你的工作目錄而已;在 Git 中,會有一個完完全全的本地儲存庫,所以即使斷網了也可以無間斷執行提交操作。你還可以編輯那些提交、回退、分支,並選擇你所要的東西,沒有任何人會知道他們必須知道的之外的東西。

因為提交是個大事,所以 CVS 使用者很少做提交。提交會包含很多的內容修改,就像如今我們能在一個含有十次提交的拉取請求中看到的一樣多。特別是在提交觸發了 CI 構建和自動測試程式時如此。

現在我們執行 cvs status,會看到產生了檔案的新版本:

$ cvs statuscvs status: Examining .===================================================================File: favorites.txt     Status: Up-to-date   Working revision:    1.2 2018-07-06 21:18:59 -0400   Repository revision: 1.2 /Users/sinclairtarget/sandbox/colors/favorites.txt,v   Commit Identifier:   pQx5ooyNk90wW8JA   Sticky Tag:      (none)   Sticky Date:     (none)   Sticky Options:  (none)

合併

如上所述,在 CVS 中,你可以同時編輯其他人正在編輯的檔案。這是 CVS 對 RCS 的重大改進。當需要將更改的部分重新組合在一起時會發生什麼?

假設你邀請了一些朋友來將他們喜歡的顏色新增到你的列表中。在他們新增的時候,你確定了不再喜歡綠色,然後把它從列表中刪除。

當你提交更新的時候,會發現 CVS 報出了個問題:

$ cvs commit -m "Remove green"cvs commit: Examining .cvs commit: Up-to-date check failed for `favorites.txt'cvs [commit aborted]: correct above errors first!

這看起來像是朋友們首先提交了他們的變化。所以你的 favorites.txt 檔案版本沒有更新到儲存庫中的最新版本。此時執行 cvs status 就可以看到,原生的 favorites.txt 檔案副本有一些本地變更且是 1.2 版本的,而儲存庫上的版本號是 1.3,如下示:

$ cvs statuscvs status: Examining .===================================================================File: favorites.txt     Status: Needs Merge   Working revision:    1.2 2018-07-07 10:42:43 -0400   Repository revision: 1.3 /Users/sinclairtarget/sandbox/colors/favorites.txt,v   Commit Identifier:   2oZ6n0G13bDaldJA   Sticky Tag:      (none)   Sticky Date:     (none)   Sticky Options:  (none)

你可以執行 cvs diff 來了解 1.2 版本與 1.3 版本的確切差異:

$ cvs diff -r HEAD favorites.txtIndex: favorites.txt===================================================================RCS file: /Users/sinclairtarget/sandbox/colors/favorites.txt,vretrieving revision 1.3diff -r1.3 favorites.txt3d2< green7,10d5<< pink< hot pink< bubblegum pink

看來我們的朋友是真的喜歡粉紅色,但好在他們編輯的是此檔案的不同部分,所以很容易地合併此修改。跟 git pull 類似,只要執行 cvs update 命令,CVS 就可以為我們做合併操作,如下示:

$ cvs updatecvs update: Updating .RCS file: /Users/sinclairtarget/sandbox/colors/favorites.txt,vretrieving revision 1.2retrieving revision 1.3Merging differences between 1.2 and 1.3 into favorites.txtM favorites.txt

此時檢視 favorites.txt 檔案內容的話,你會發現你的朋友對檔案所做的更改已經包含進去了,你的修改也在裡面。現在你可以自由的提交檔案了,如下示:

$ cvs commitcvs commit: Examining ./Users/sinclairtarget/sandbox/colors/favorites.txt,v <-- favorites.txtnew revision: 1.4; previous revision: 1.3

最終的結果就跟在 Git 中執行 git pull --rebase 一樣。你的修改是新增在你朋友的修改之後的,所以沒有 “合併提交” 這操作。

某些時候,對同一檔案的修改可能導致衝突。例如,如果你的朋友把 “green” 修改成 “olive”,同時你完全刪除 “green”,就會出現衝突。CVS 早期的時候,正是這種情況導致人們擔心 CVS 不安全,而 RCS 的悲觀鎖機制可以確保此情況永不會發生。但 CVS 提供了一個安全保障機制,可以確保不會自動的覆蓋任何人的修改。因此,當執行 cvs update 的時候,你必須告訴 CVS 想要保留哪些修改才能繼續下一步操作。CVS 會標記檔案的所有變更,這跟 Git 檢測到合併衝突時所做的方式一樣,然後,你必須手工編輯檔案,選擇需要保留的變更進行合併。

這兒需要注意的有趣事情就是在進行提交之前必須修復並合併衝突。這是 CVS 集中式特性的另一個結果。而在 Git 裡,在推播原生的提交內容之前,你都不用擔心合併衝突問題。

標記與分支

由於 CVS 沒有易於定址的提交物件,因此對變更集合進行分組的唯一方法就是對於特定的工作目錄狀態打個標記。

建立一個標記是很容易的:

$ cvs tag VERSION_1_0cvs tag: Tagging .T favorites.txt

稍後,執行 cvs update 命令並把標籤傳輸給 -r 標誌就可以把檔案恢復到此狀態,如下示:

$ cvs update -r VERSION_1_0cvs update: Updating .U favorites.txt

因為你需要一個標記來回退到早期的工作目錄狀態,所以 CVS 鼓勵建立大量的搶先標記。例如,在重大的重構之前,你可以建立一個 BEFORE_REFACTOR_01 標記,如果重構出錯,就可以使用此標記回退。你如果想生成整個專案的差異檔案的話,也可以使用標記。基本上,如今我們慣常使用提交的雜湊值完成的事情都必須在 CVS 中提前計劃,因為你必須首先有個標籤才行。

可以在 CVS 中建立分支。分支只是一種特殊的標記,如下示:

$ cvs rtag -b TRY_EXPERIMENTAL_THING colorscvs rtag: Tagging colors

這命令僅僅只是建立了分支(每個人都這樣覺得吧),所以還需要使用 cvs update 命令來切換分支,如下示:

$ cvs update -r TRY_EXPERIMENTAL_THING

上面的命令就會把你的當前工作目錄切換到新的分支,但《Pragmatic Version Control Using CVS》一書實際上是建議建立一個新的目錄來房子你的新分支。估計,其作者發現在 CVS 裡切換目錄要比切換分支來得更簡單吧。

此書也建議不要從現有分支建立分支,而只在主線分支(Git 中被叫做 master)上建立分支。一般來說,分支在 CVS 中主認為是 “高階” 技能。而在 Git 中,你幾乎可以任性建立新分支,但 CVS 中要在真正需要的時候才能建立,比如發布專案。

稍後可以使用 cvs update-j 標誌將分支合併回主線:

$ cvs update -j TRY_EXPERIMENTAL_THING

感謝歷史上的貢獻者

2007 年,Linus Torvalds 在 Google 進行了一場關於 Git 的 演講。當時 Git 是很新的東西,整場演講基本上都是在說服滿屋子都持有懷疑態度的程式設計師們:儘管 Git 是如此的與眾不同,也應該使用 Git。如果沒有看過這個視訊的話,我強烈建議你去看看。Linus 是個有趣的演講者,即使他有些傲慢。他非常出色地解釋了為什麼分散式的版本控制系統要比集中式的優秀。他的很多評論是直接針對 CVS 的。

Git 是一個 相當複雜的工具。學習起來是一個令人沮喪的經歷,但也不斷的給我驚喜:Git 還能做這樣的事情。相比之下,CVS 簡單明瞭,但是,許多我們認為理所當然的操作都做不了。想要對 Git 的強大功能和靈活性有全新的認識的話,就回過頭來用用 CVS 吧,這是種很好的學習方式。這很好的詮釋了為什麼理解軟體的開發歷史可以讓人受益匪淺。重拾過期淘汰的工具可以讓我們理解今天所使用的工具後面所隱藏的哲理。

如果你喜歡此博文的話,每兩周會有一次更新!請在 Twitter 上關注 @TwoBitHistory 或都通過 RSS feed 訂閱,新博文出來會有通知。

修正

有人告訴我,有很多組織企業,特別是像做醫療裝置軟體等這種規避風險類的企業,仍在使用 CVS。這些企業中的程式設計師通過使用一些小技巧來解決 CVS 的限制,例如為幾乎每個更改建立一個新分支以避免直接提交給 HEAD。 (感謝 Michael Kohne 指出這一點。)

(題圖:plasticscm