Thursday, July 2, 2009

CVS Subversion - 版本控制系統的基礎觀念

CVS Subversion - 版本控制系統的基礎觀念

蔡煥麟
huanlin.tsai at msa.hinet.net
Revision: 1.0 (Jun-13-2004)
前言

這是我學習使用 CVS(Concurrent Versions System)和 Subversion 的過程當中,陸續整理的一些筆記,裡面的內容大部分可以在參考文獻中找到。整理這份文件的目的,主要是提供一份比較簡短的觀念說明,讓想要學習使用版本控制系統的人,可以先懂一些必要的觀念和術語,就立刻學習工具的使用,而不是 K 完一堆手冊和 FAQ 之後才有辦法使用工具。我不是說只要閱讀這份文件就夠了,而是當你有了基本的觀念讓你足以使用工具的一些基礎功能之後,就應該去閱讀比較完整的手冊或書籍,以了解其他細節或進階的用法。這時候因為已經有了基礎,再去閱讀其他文件時,就會覺得容易些了。
摘要

本文介紹版本控制系統的基礎觀念及術語,以及導入版本控制系統時應考慮的事項。
1 簡介

在開發過程中,你是否碰到過以下幾種情形:

* 檔案被別人(或自己)覆蓋;
* 檔案遺失(拖放檔案時誤動作...);
* 想要比對各版本之間的程式碼有何不同;
* 想要回到之前修改的版本(需求反覆變更、自己改錯了...);
* 這些 code 不是我改的,是誰碰過我的程式碼?
* 軟體發行之後,必須凍結共用的程式碼一段時間,免得其他人在改 bug 的同時,因為你修改了共用的程式而增加更多新的問題。

如果有以上情形,你需要的是對專案進行版本控制(version control)。版本控制也有人稱它為原始碼控制(source code control),它的目的就在於解決上述的各種問題,讓你可以:

* 隨時復原錯誤,就好像是專案的時光回溯器,可以將檔案恢復到以前的任何時候的版本;
* 多人同時修改同一份程式碼,不會有相互覆蓋的情況;
* 保留所有修改的歷程,如果你發現自己的程式碼有被別人更動過,可以很容易找到是誰更改的,以及何時更改的;
* 在發行正式版的同時,還能繼續發展新版本,無須下令凍結所有程式碼。

版本控制系統則是提供上述功能的軟體系統,它提供了一個地方讓你集中存放開發過程中的所有程式檔案及文件,以便達到集中控管的目的。
版本控制與軟體建構管理

軟體建構管理(Software Configuration Management)簡稱 SCM 或 CM,是軟體工程領域中的一環。SCM 的傳統定義是原始碼的版本管理,後來則逐漸演進擴大,將軟體開發的一些標準和程序納進來。你可以將 SCM 視為軟體演進的過程中,用來管理改變的標準程序,這個改變的來源包括:程式碼的改變、支援多種作業平台、提供多種版本(例如:標準版、專業版)....等等,而版本控制就是用來實現 SCM 的主要工具。

SCM 這個主題很大,這裡主要是點出版本控制與 SCM 的關係,若有興趣進一步了解 SCM,請參考相關的書籍或到 Google 搜尋關鍵字 "software configuration management"。
2 版本控制系統
2.1 檔案庫(Repository)

前面提到,版本控制系統有一個集中存放檔案的地方,這個地方有個正式名稱,叫做「檔案庫(repository)」。檔案庫裡面儲存了專案檔案的所有歷史版本(包括目前開發中的版本),有的版本控制系統是以資料庫的方式儲存,有的是以檔案的方式儲存,不論儲存的方式為何,對使用者來說,最重要的就是要把檔案庫放在一台穩定、安全的機器上,並且還要定期備份。
主從式架構

現在我們知道,檔案庫既然是檔案的集中營,那麼一定是放在某台機器上,供所有開發人員存取,其作業方式如下圖所示,是一種主從式(Client/Server)的架構:

檔案庫所在的機器上,必須要安裝版本控制系統,以便提供檔案存取的服務給各個用戶端;而圖中的「開發人員A」和「開發人員B」則代表了用戶端,用戶端機器上必須安裝版本控制系統的用戶端工具,才能存取檔案庫。

在連線方式上,用戶端可以透過各種網路協定來存取檔案庫,某些版本控制系統要求你一定要隨時與檔案庫保持連線,才能修改檔案內容;某些版本控制系統(例如 Subversion)則採用比較寬鬆的方式,你可以在沙灘上用筆記型電腦修改程式,等到回辦公室時再將檔案同步。
2.2 哪些東西要放進檔案庫?

很顯然的,程式碼當然要存放在檔案庫中,以便進行版本控制,那麼,還有哪些東西也要版本控管?

基本上,你在開發一個專案的過程當中,需要用來建置軟體的檔案,都可能要放到檔案庫裡面,例如:建置專案的組態檔或 makefile、測試資料等等。在決定哪些檔案要放進檔案庫時,你可以問自己一個問題:「如果少了這個檔案,能夠建置和發行軟體嗎?」
衍生的檔案(Generated Artifacts)

有些檔案是建置過程中產生的一些附屬產出文件,例如開發 Java 應用程式時,可以利用 JavaDoc 工具來產生原始碼的說明文件。像這類從某個檔案衍生出來的檔案,該不該放到檔案庫裡面?

簡單的回答是「不要」。因為 :(1) 它不是建置軟體的必備條件;(2) 它是重複的資訊,而且容易造成檔案的不一致,我們經常會修改了程式碼卻沒有立刻產生 JavaDoc 文件;(3) 當我們需要它的最新版本時,可以隨時產生。

以上是針對 JavaDoc 這個例子來說明,但是專案開發過程中,還是有一些其他的衍生檔案,並不像 JavaDoc 文件一樣可以隨時產生。例如,你可能有一些檔案是所有開發人員都要共同存取的,或者需要花數個小時才能重建的,這些檔案還是應該放進檔案庫裡面。
非程式碼的檔案(Non-code Artifacts)

除了建置專案所需的檔案之外,有些非程式碼的檔案也應該納入檔案庫,例如:專案管理的文件、團隊成員的討論信件、會議記錄、FAQ....等等,任何對專案開發有貢獻的資訊都可以放到檔案庫裡。
匯入(Import)

這個動作指的是把一個完整的目錄結構匯入檔案庫。專案一開始的時候,檔案庫裡面並沒有專案的檔案和文件,因此當我們決定要把一個新的專案放進檔案庫進行版本控管時,第一個動作就是建立好一個專案的初始目錄結構(其中可能包含一些必要的檔案),然後執行匯入。
提示

要在 CVS 裡面執行檔案目錄的搬移會有些麻煩,因此最好一開始多考慮一下要放入檔案庫的專案目錄結構,免得造成日後的困擾。Subversion 雖然改善了 CVS 的這個缺點,連目錄的變動都可以進行歷史版本的記錄,但是經常搬動目錄仍然會有些副作用,匯入專案之前還是多想一下比較好。
匯出(Export)

匯出是把整個專案或模組從檔案庫中取出來,取出來的檔案不包含版本控制系統的管理檔案,也就是說,匯出的模組將不再由 版本控制系統控管。
2.3 工作區(Workspace)與管理的檔案
工作區(Workspace)

檔案庫存放了專案開發所需的所有檔案及其歷史版本,但是對於團隊成員個人而言,並不需要全部的檔案,我們只需要自己負責的部分就夠了。因此,我們會從檔案庫中取出(複製)一部分自己需要的檔案到自己本機的硬碟裡,這些存放在本機的檔案,就稱為本地複本(local copy),而存放本地複本的地方,則稱為工作區(workspace)。相對於本地複本,儲存在檔案庫中的版本,則稱為主拷貝(master copy)。

對於小型專案而言,本地複本可能就是專案的所有原始碼和文件,而大型專案可能會切割成數個子系統或模組,所以開發人員只要取出自己負責的子系統就行了。

工作區有時候也稱為工作目錄(working directory)或程式碼的工作複本(working copy)。
取出(Check Out)

一開始,我們個人的工作區都沒有任何檔案,因此第一個動作就是要從檔案庫中取出我們需要的工作複本,這個動作稱為:取出(check out)。當你執行 check out 時,版本控制系統就會從檔案庫拷貝一份你需要的工作複本到你的工作目錄,這個工作複本的所有檔案目錄結構都會跟檔案庫裡面的目錄結構一模一樣。
存入(Commit or Check In)

當你取出工作複本之後,就可以修改檔案內容,等到你覺得修改得差不多了,或者已經改完了,就可以把修改過的檔案存入檔案庫,這個動作稱為:存入(commit,或者 check in)。
更新(Update)

當你修改自己的工作複本時,當然其他的團隊成員也可能正在修改一些檔案,每個人的修改作業都是獨立進行且不會相互影響的,別人修改的結果也不會立即反映在你本機的工作複本上。如果你要看到別人修改的最新版本,你就必須執行更新(update)這個動作。當你的同僚執行 update 時,他們也會取得你最近 check in 的版本。

Check out 和 update 的行為有些相似,儘管他們的使用時機和目的不盡相同,有時候我們還是會交互使用這兩個術語。
2.4 專案、模組、與檔案(Projects, Modules, and Files)

大部分的版本控制系統允許你針對單一檔案進行取出與存入的動作,但是大部分的專案會有數十個到數百個以上的檔案,如果要對每一個檔案執行取出與存入,就太麻煩了,因此版本控制系統提供了不同層級的操作,讓我們能夠以邏輯組成的檔案群組來執行版本控制。

最上層的邏輯單位,就是專案(projects),在專案底下又可分成幾個模組(modules)以及子模組(submodules),你得為這些模組命名,以便團隊成員透過名稱來存取它們。例如一個汽車保養場的資訊系統,可能會分成進廠維護管理模組、庫存零件管理模組、結帳模組...等等,各開發人員只需要取出自己負責的模組就行了。當然,如果你要的話,也可以把整個專案都取回自己的工作區。

你可以把專案看成是某個目錄階層的根目錄,而模組和子模組則是底下的一堆子目錄和檔案。但是記住,模組只是檔案的邏輯組成單位,相同的檔案可以出現在不同的模組裡面,而模組也可以跨專案共享,你只要將一些共享的檔案歸納到一個共用的模組裡面就行了。
2.5 版本從何而來?

到目前為止,我們討論的都是檔案的取出和存入動作,那版本呢?

其實版本控制系統在我們每次執行 check in 時,就會把這次存入的檔案視為一個新的版本,而每個版本都會記錄在檔案庫裡面。也就是說,當你從檔案庫取出一個檔案,修改它,然後存入檔案庫,在檔案庫裡面就會保留一份原始的版本,以及你修改後的版本(註1)。 每次修改的新版本都會被賦予一個修訂版次(revision number),檔案的修訂版次在每次存入檔案庫時就會累加。跟版次號碼一起儲存的 資訊可能還包括了檔案的修改時間,以及由開發人員額外加註的說明。

某些版本控制系統會在你每次 check in 時,為所有的檔案指定一個新的修訂版次;某些工具則是針對個別檔案的變更來記錄版次,例如:

file1.java 1.10
file2.java 1.7
file3.java 1.9

這表示你不能用修訂版次來代表專案的發行版本,而應該用標記(tags)來作為專案的版本號碼。
2.6 標記(Tags)

前面提過,修訂版次(revision numbers)是用來表示個別檔案的版本,它會由版本控制系統自動累加,不適合用來表示專案的發行版本。當我們要為專案訂一個好記的版本名稱時,例如:Pre-Relase2, 應該使用標記(tags)。

標記不只可以作為專案的版本命名,你也可以為特定模組或某些檔案訂一個標記名稱,例如前面舉的例子:file1.java 1.10、file2.java 1.7、file3.java 1.9,你可以為這三個檔案訂一個標記名稱,以後可以使用這個標記名稱來一次取出這三個檔案。

總之,標記代表了專案開發過程中,某一個時間點的狀態,或者里程碑。
2.7 分支(Branches)

在開發過程中,通常所有的程式設計師都是工作在同一個程式碼基礎(code base)上,他們雖然各自負責撰寫不同部分的程式碼,但主要都依循「從檔案庫取出,修改,然後存入」的工作模式,這些目前大家所修改的檔案庫中的程式碼,就稱為專案的主線(mainline)。參考下圖以了解主線的概念(取自 [1])。

然而有些情況不允許我們共同修改主線的程式碼,例如,當專案上線之後,Mary 負責修正使用者陸續反映的程式臭蟲,這段期間可能要維持一兩個月左右,可是這時候負責撰寫程式主架構與共用元件的 John 發現了一些必須改進的地方,John 不能等 Mary 把所有臭蟲都解決了才進行,他必須立刻著手改進現有的主架構和共用元件。如果 John 依照以往的方式,修改了主架構或共用元件之後,執行 check in 的動作,這樣勢必造成其他已經上線的程式產生新的問題,甚至無法運作,而必須全部都修改一遍,如此一來,Mary 的負擔就更重了,她得一邊處理臭蟲,還要應付新的架構和元件所帶來的問題。你或許會想,John 可以一直修改他自己的工作複本,但是都不要執行 check in 就行了,但是這並不符合一般人的工作習慣,我們通常修改程式到某個階段時,就會執行 check in 以確保程式存在一個安全的地方,而且萬一 John 哪天忘了,習慣性地執行 check in,那就糟了。

分支(branches)的用處就在這裡,以上個例子而言,Mary 可以建立一個分支,以繼續修改程式上線後的臭蟲,而 John 則可以繼續維護目前的產品主線。此時 John 和 Mary 都一樣可以執行 check in 的動作,只是 Mary 取出和存入的都是檔案庫中的一個獨立分支,跟 Mary 維護的主線是完全分離的。下圖描繪了上述的作業方式(取自 [1])。

分支就好像是另一個獨立的檔案庫一樣,運用分支的技巧,你就不用因為發行軟體而將目前的的程式碼凍結起來。

關於分支的其他事項:

* 分支是以標記(tag)來識別。
* 即使你的版本控制系統允許你建立分支的分支,但是最好不要這麼做,以免橫生枝節。

提示

對於初學者來說,標記和分支可能不會太快用到,可以先嘗試把一個專案放進版本控制系統,運行順利一陣子之後,再逐漸學習使用這些進階的功能。
2.8 合併(Merging)

當你要 check in 某個檔案時,如果檔案已經先被其他人修改過並且 check in 了,版本控制系統便會偵測到,並且不允許你 check in 這個檔案。此時便需要使用合併(merge)的技術,將兩個人修改的內容進行合併,以確保彼此不會相互覆蓋,又能保留各自修改的內容。由於兩個人修改同一個檔案時,通常不會碰巧都修改到相同的部分,因此版本控制系統會幫我們自動完成合併的動作 ;萬一兩個人修改的部分正好重疊,這種情況稱為衝突(conflict),此時就必須由後來 check in 的那個人手動解決衝突的部分(可能會和另一個修改此檔案的人討論為什麼會發生這種情況,以及應如何修改)。這部分的處理過程在稍後還會有進一步的討論。

除了解決衝突的情況,合併還有另一個用途:用來合併分支和主線。例如當你為正式發行的版本建立一個分支以後,再這個分支裡面修正了一些臭蟲,而你發現這些臭蟲也存在主線的程式裡,此時就可以用合併的方式,讓版本控制系統把你在分支裡面做的修正套用到主線的程式碼。
2.9 鎖定機制(Locking Options)

前面提到過兩個人修改同一個檔案所造成的衝突,是以合併的技術來解決,不過,各種版本控制系統採用的方式可能不盡相同,其相異之處,基本上只是對於檔案鎖定(locking)的處理方式不一樣而已。鎖定機制可分為兩種:嚴格鎖定(strict locking)與樂觀鎖定(optimistic locking)。

採用嚴格鎖定的版本控制系統,採用的是事先避免衝突的態度,也就是當一個人 check out 某個檔案時,該檔案就會被鎖定成唯讀狀態,此時別人可以讀取這個檔案的內容,或者用它來建置專案,但無法修改,這樣就不會造成衝突了 ,只是後來想要修改的人,必須等到取得鎖定的人 check in 檔案之後,才能修改。參考下面的圖例(取自 [2]):

嚴格鎖定可以避免衝突,但實際用起來卻不大方便,因為一個檔案同時間只有一個人能取得修改權,其他人得排隊等候,像上面的例子,Sally 就要等 Harry 執行 check in 之後才能修改檔案,萬一 Harry 一直改不完,或者他忘了,然後渡假去了,Sally 該怎麼辦?

嚴格鎖定還有可能發生死結(deadlock)的情況,例如 A 檔案和 B 檔案兩個是相關的程式,Harry 先取出 A 檔案,且 Sally 取出了 B 檔案;之後 Harry 跟 Sally 又分別要取出 B 檔案和 A 檔案進行修改時,兩個人都無法修改,因為檔案都被對方鎖住了。

樂觀鎖定就沒有這些問題,因為樂觀鎖定根本就不鎖定檔案,任誰都可以同時取出同一個檔案進行修改,當發生衝突時,再使用合併的方式解決。參考下面的圖例(修改自 [1]):

圖中顯示 Fred 和 Wilma 都取出了 File1.java,而 Fred 先修改完並且 check in(commit),當 Wilma 也修改好,要執行 check in 時,版本控制系統會告訴她,她本機上的 File1.java 複本過時了(out-of-date),也就是說,檔案庫裡的 File1.java 從她上次取出後已經被別人更動過了,因此她必須先更新本機的複本,然後再跟本機修改過的複本進行合併,於是最後 check in 的結果就包含了 Fred 和 Wilma 修改的內容。由於兩個人修改的是同一個檔案的不同部分,因此版本控制系統能夠順利完成合併的動作,如果兩個人修改到相同的部分,Wilma 就得自己解決這個衝突了(可能會和 Fred 討論如何修改)。

也許你會覺得樂觀鎖定有可能需要自己手動解決衝突,嫌它太過麻煩,而寧願採用嚴格鎖定的方式。但實際上你並不需要太擔心,因為在開發專案時,通常會事先劃分好個人負責的模組或子系統,因此會發生多人同時修改一個檔案的機會已經不多;即使有這種情況,兩人剛好修改到同一行程式碼的機會更低。所以比較起來,樂觀鎖定還是比嚴格鎖定方便許多。

最後,再將這兩種機制的特性整理成下表,方便參考:
  作業模式 優點 缺點
嚴格鎖定 鎖定-修改-解鎖(lock-modify-unlock) 可避免衝突。 同一時間只有一個人可取得修改權,其他人必須排隊等候,可能造成工作無法順利進行,甚至造成相互鎖住對方要修改的檔案的情況。
樂觀鎖定 複製-修改-合併(copy-modify-merge) 所有人可修改任何檔案。 當兩個人修改同一個檔案的相同部分時,需要手動解決衝突,但發生這種情況的機率很低。
3 導入版本控制系統

在了解版本控制系統的基本觀念之後,就可以挑選一個版本控制系統,把它安裝起來試試看了,如果是一人團隊,應該沒什麼問題;如果是多人團隊,要將版本控制系統導入現行的軟體開發流程,可能就要多花點準備的功夫了。以下簡單說明幾點可能的工作項目:

1. 選擇合適的工具。目前市面上可以買到或免費取得的版本控制系統有很多,你可能要花一點時間比較一下各種產品的功能,並且根據自己的需求和預算,挑選最適合自己團隊的工具。
2. 安裝並測試版本控制系統的各項功能。
3. 選擇一名管理員。團隊中必須有一個人負責管理檔案庫、建立專案的初始目錄結構、定期備份檔案庫等工作。
4. 教育訓練。教導開發人員如何在日常的開發工作中使用版本控制系統,讓他們了解跟之前的作業方式有什麼差別、可以獲得哪些好處,以減少因為改變工作習慣而產生的阻力。
5. 正式將專案納入版本控管。

在選擇工具方面,這裡無法提供什麼有用的建議,因為我只接觸過 Visual SourceSafe、CVS、和 Subversion,不過如果要從這三個工具中挑選,我會選 Subversion,因為:

1. 喜新厭舊;
2. Subveriosn 改進了 CVS 的缺點,連目錄的變更也會記錄版本(檔案和目錄的搬移更方便);
3. 安裝 Subversion 的過程順利(安裝在 Windows 2000 Server 上);
4. 有很方便的用戶端工具:TortoiseSVN,可減少導入 Subversion 的阻力;
5. Subversion 的文件寫得不錯。(感謝 Plasma 提供繁體中文版的 Subverion 電子書)

另外,這裡有一個各家版本控制系統的比較表,也可以參考看看:

http://better-scm.berlios.de/comparison/comparison.html
4 總結

本文介紹了版本控制的一些基本觀念和術語,也大概提了一下導入版本控制系統所需的準備工作,在具備了這些基礎概念之後,便可以開始學習工具的使用,希望本文提供了足夠的基礎,作為您進一步學習使用版本控制系統的跳板。

註1:事實上,大部分的版本控制系統只儲存兩個版本之間有差異的部分,而不是儲存完整的兩份檔案內容。

 
術語整理
英文 中文 說明
check out 取出 從檔案庫中取出檔案。
commit/check in 存入 將檔案從本地端存入檔案庫。
export 匯出 把整個模組從檔案庫中取出來,取出來的檔案不包含版本控制系統的管理檔案,也就是匯出的模組將不再由 版本控制系統控管。
import 匯入 把整個目錄結構匯入檔案庫。當你要把一個新的專案放進檔案庫進行版本控管時,就需要執行這個動作。
local copy 本地複本 放在用戶端機器的工作目錄中的專案複本。
master copy 主拷貝 放在檔案庫裡的專案複本。
module 模組 一個目錄階層,通常一個專案就是一個模組。
release 發行版本 軟體產品的一個版本。為了區別產品的版本以及個別檔案的修訂版次,因此不使用 version,而用 release。
repository 檔案庫 存放所有檔案(包含歷史版本)的地方,用戶端執行 check out 時就是從這裡取出檔案。
revision number 修訂版次 一個檔案的修改版本,例如:1.1、1.3.2.2。
tag 標記 在開發過程的某個時間點上,為一組檔案提供的符號名稱。透過 tag 一群檔案,你可以很容易在某個 release 裡面找出這些檔案。
update 更新 從檔案庫中取得其他人修改的檔案,以更新本機的副本(local copy)。
workspace/
working directory 工作區/工作目錄 本機的工作目錄,又稱為沙盒(sandbox)。

 
參考文獻
[1] Pragmatic Version Control with CVS. Dave Thomas and Andy Hunt. The Pragmatic Programmers, LLC. 2003.
[2] Version Control with Subversion Draft version 9837. Ben Collins-Sussman, Brian W. Fitzpatrick, and C. Michael Pilato. http://svnbook.red-bean.com/(繁體中文版:http://freebsd.sinica.edu.tw/~plasma/svnbook/)
[3] CVS 入門。作者:臥龍小三。http://linux.tnc.edu.tw/techdoc/cvs/book1.html

No comments: