svn主幹以及分支的開發模式(詳細說明)
解決版本衝突-使用SVN主幹與分支功能
1 前言
大多數產品開發存在這樣一個生命週期:編碼、測試、釋出,然後不斷重複。通常是這樣的開發步驟:
1) 開發人員開發完畢某一版本(如版本A)功能後,提交測試;
2) 測試人員對待發布版本A進行測試,同時開發人員繼續開發新功能(如版本B);
3) 測試人員提交bug,研發人員修復bug,同時繼續開發新功能;
4) 重複第3步驟,直到待發布版本A測試通過測試後,釋出第一版本
這樣就會存在以下問題:
1) 如何從程式碼庫中(A+B)分離出待發布版本A,進行測試和釋出;
2) 如果單獨存放待發布版本A,那麼開發組必須同時維護此版本庫A以及當前最新程式碼庫(A+B),操作冗餘且容易出錯。
在SVN中,通常採用主幹(trunk)與分支(branches)的方法,解決以上問題。
2 相關概念和原理
在SVN中建立程式碼庫時,通常會建立trunk、branches、tags三個子目錄,當然,你也可以用其他名稱來實現主幹和分支的功能
trunk-主幹,或稱主線,顧名思義,是開發的主線。
branches-分支,是從主線上分出來,獨立於主線的另一條線。可以建立多個分支。一個分支總是從主幹一個備份開始的,從那裡開始,發展自己獨有的歷史(如下圖所示)。在版本控制的系統中,我們經常需要對開發週期中的單獨生命線作單獨的修改,這條單獨的開發生命線就可以稱為Branches,即分支。分支經常用於新增新的功能以及產品釋出後的bug修復等,這樣可以不影響主要的產品開發線以及避免編譯錯誤等。當我們新增的新功能完成後可以將其合併到主幹中。
tags-標記,主要用於專案開發中的里程碑,比如開發到一定階段可以單獨一個版本作為釋出等,它往往代表一個可以固定的完整的版本。即主幹和分支都是用來進行開發,而標記是用來進行階段釋出的。安全公司的配置庫有專門的釋出區,所以tags並不需要建立,在這裡只是提供說明,不推薦使用。
branches以及tags在TortoiseSVN中建立方法是一致的,它們都是通過儲存類似Linux中的lunch快捷方式一樣,只是建立了指向某個版本的連結,而不會真正將此版本的內容複製到分支或者標記中,這樣既可以節省空間,也可以很快速的建立,被稱為“廉價的拷貝”。
為了便於建立分支和標記,通常習慣於將Repository版本庫的結構佈置為:/branches,/tags,/trunk。分別代表分支,標記以及主幹。
還有一點值得注意的是,SVN不推薦在建立的tag基礎上Revision,這種情況應用branches,因為tag一般保持不變不作任何修改。
3 程式碼的分支管理策略
關於程式碼管理的分支和釋出策略,目前主要有兩種:一種是主幹作為新功能開發主線,分支用作釋出。另一種是分支用作新功能開發,主幹作為穩定版的釋出。
3.1 分支用來發布
典型操作步驟如下:
1) 開發者提交所有的新特性到主幹。 每日的修改提交到/trunk:新特性,bug修正和其他。
2) 這個主幹被拷貝到“待發布”分支。 當小組認為軟體已經做好釋出的準備(如,版本1.0)然後/trunk會被拷貝到/branches/1.0。
3) 專案組繼續並行工作,一個小組開始對分支進行嚴酷的測試,同時另一個小組在/trunk繼續新的工作(如,準備2.0),如果一個bug在任何一個位置被發現,錯誤修正需要來回運送。然而這個過程有時候也會結束,例如分支已經為釋出前的最終測試“停滯”了。
4) 分支已經作了標記並且釋出,當測試結束,/branches/1.0作為引用快照已經拷貝到/tags/1.0.0,這個標記被打包釋出給客戶。
5) 分支多次維護。當繼續在/trunk上為版本2.0工作,bug修正繼續從/trunk運送到/branches/1.0,如果積累了足夠的bug修正,管理部門決定釋出1.0.1版本:拷貝/branches/1.0到/tags/1.0.1,標記被打包釋出。
整個過程隨著軟體的成熟不斷重複:當2.0完成,一個新的2.0分支被建立,測試、打標記和最終釋出,經過許多年,版本庫結束了許多版本釋出,進入了“維護”模式,許多標記代表了最終的釋出版本。
這種分支管理策略被廣泛的應用於開源專案。比如freebsd的釋出就是一個典型的例子。
freebsd的主幹永遠是current,也就是包括所有最新特性的不穩定版本。然後隨著新特性的逐步穩定,達到一個釋出的里程碑以後,從主幹分出來一個stable分支。freebsd是每個大版本一個分支。也就是說4.x,5.x,6,x各一個分支。每個釋出分支上只有bug修改和現有功能的完善,而不會再增加新特性。新特性會繼續在主幹上開發。當穩定分支上發生的修改積累到一定程度以後,就會有一次釋出。釋出的時候會在穩定分支上再分出來一個 release分支。以6.x為例,就會有6.0,6.1,6.2…等釋出分支。
這種釋出方法非常適用於產品線的釋出管理。產品是要賣的,以前賣給客戶的版本仍需要繼續維護,而為了以後的市場,新功能也不斷地在增加。這種管理方法對已釋出產品的維護工作和下一代產品的開發工作進行了隔離。對於已經發布的產品,只有維護的補丁釋出。而新發行的產品不僅包括了所有的bug修改,還包括了新功能。
這種方法具有如下缺點:首先,必須對主幹上的新功能增加進行控制。只能增加下一個釋出裡面計劃整合進去的新特性。而且,已經在主幹上整合的新特性中的任何一個,如果達不到里程碑的要求,穩定分支就不能建立,這很有可能影響下一個釋出的計劃。開源專案可能這方面的壓力小一些,但是商業產品開發如果碰到這種情況就危險了。還有一個缺點就是bug修改必須在各個分支之間合併。從分支和合並的一些實踐經驗上看,各個長期存在的分支之間必須要週期性的進行合併,否則很容易引發合併衝突。可是各個stable分支以及release分支之間恰好是不能進行合併而且還要長期存在的。因此,採用這種分支策略可能碰到的最大問題就是某個分支上的bug修改內容往其它分支merge的時候出現的衝突。而且一旦發現一個bug,調查這個bug影響哪些分支的工作會隨著維護的釋出分支的數量而增加。
在非產品開發的外包軟體專案裡面,這種釋出方法的好處體現不出來,而缺點仍然存在。外包專案的特點是客戶永遠需要“最新”的程式碼,因此對已經發布的某個分支進行維護的情況很少出現(在測試的時候會出現)。而且釋出的方法和產品的釋出也不一樣。產品的釋出,只要把釋出分支上的程式碼編譯成安裝盤就可以了,而外包的釋出往往是把上一次釋出和這一次釋出之間發生變化的程式碼送給客戶。如果每次釋出都是一個分支的話,將會出現兩個分支上的比較。強大的版本控制工具當然支援這種比較,但是很多版本工具不支援分支之間的比較,而只支援分支內的不同版本之間的比較。因此為了避免釋出方法受工具的限制,就要避免出現分支間比較的情況。針對外包開發的特殊情況,只有採用另外一種分支管理策略。
3.2 主幹用來發布
與第一種分支策略正好相反,主幹上永遠是穩定版本,可以隨時釋出。bug的修改和新功能的增加,全部在分支上進行。而且每個bug和新功能都有不同的開發分支,完全分離。而對主幹上的每一次釋出都做一個標記而不是分支。分支上的開發和測試完畢以後才合併到主幹。
這種釋出方法的好處是每次釋出的內容調整起來比較容易。如果某個新功能或者bug在下一次釋出之前無法完成,就不可能合併到主幹,也就不會影響其他變更的釋出。另外,每個分支的生命期比較短,唯一長期存在的就是主幹,這樣每次合併的風險很小。每次釋出之前,只要比較主幹上的最新版本和上一次釋出的版本就能夠知道這次釋出的檔案範圍了。
這種釋出模式也有缺點。如果某個開發分支因為功能比較複雜,或者應釋出計劃的要求而長期沒有合併到主幹上,很可能在最後合併的時候出現衝突。因此必須時刻注意分支離開主幹的時間。如果有的分支確實因為特殊的需要必須長期存在,那就必須定期把主幹的更新往這個分支上合併。為了減少這種合併發生的次數,並且限定合併的範圍,要為每次釋出預先建立一個釋出分支,然後所有的開發分支根據自己的釋出計劃向各個釋出分支合併。當下一次釋出的分支上已經集成了所有的變更並且測試完畢以後,把這個釋出分支內容合併到主幹,釋出主幹,然後鎖定或者刪除這個分支。然後把主幹上的所有更新合併到後面幾個釋出分支裡面去。外包專案的釋出週期一般都比較短,往往客戶驗收測試的週期就是釋出週期。所以這種方法就夠用了。如果釋出週期很長,各個釋出分支之間還要定期的從前向後合併。這種釋出方法還有一個缺點就是測試。不像第一種分支策略,釋出的分支就是測試的分支。這種釋出模式的測試分支往往是各個釋出分支,在正式釋出之前才把下一個釋出分支上的更新合併到主幹,這就引入了合併出錯的風險,而主幹上的程式是沒有經過測試的。幸好從這個釋出模式上看,下一個釋出分支的合併基礎應該和主幹上一次釋出內容相同,所以引入合併錯誤的風險很低。還有一種建議就是不設定主幹,下一個釋出分支就是主幹,直接釋出下一個釋出分支的變更內容,然後把變更合併到再下一個釋出分支上去。以此類推。
3.3 注意事項
1) 做分支上做開發的時候,必須定期使分支與主幹同步,避免開發完成後合併(merge)回主幹時出現嚴重衝突(confict);
2) 進行合併前,處理掉工作副本上的所有本地修改,方便合併失敗時進行回滾(revert);
3) 進行合併時,特別注意 新增/刪除 操作,因為很多衝突都是這類操作引起的;
4) 完成一個分支的功能併合並回主幹後,拋棄該分支,後續其它功能的開發使用新建的分支。當然,也有辦法繼續使用該分支;
5) 輔助文件是必需的。為了觀察分支的建立和合並的過程,至少需要一份類似泳道圖的文件標記每一次分支建立和合並的過程;
6) 開發分支往主幹或者釋出分支合併的次數應該儘可能少。一般來講應該在單體測試結束合併到主幹或者釋出分支,然後進行結合測試。如果結合測試裡發現bug不應該在原來的開發分支上繼續修改,而應該建立新的分支進行修改;
7) 分支建立和合並的log必須規範。便於以後查詢。基本的log資訊應該包括從哪個分支的哪個版本建立分支;把哪個分支的從哪版本到哪個版本範圍內的變更合併到了哪個分支的哪個版本,合併後的版本號。這些資訊有一些是版本控制工具本身可以很方便查詢到的,就可以省略
4 操作步驟
在程式碼庫中建立trunk、branches、tags目錄,分別為主幹、分支和標記,這樣的佈局是為了更清晰的區別主線、分支和標記三者的位置。在主幹上提交程式碼,到可釋出的程度時,建立分支。
為便於比較結果,我們在主幹中上傳一個檔案readme.txt(版本為659):
4.1 建立分支(標記)
將主幹trunk簽出(checkout)到本地,在本地checkout的trunk目錄上單擊滑鼠右鍵,在彈出選單中選擇“TortoiseSVN” →“Branch/tag…”
在下圖彈出的視窗中,將“To URL” 指向branches目錄並輸入分支的具體目錄名。預設的目標URL將會是你當前工作拷貝所處的源URL,必須給分支/標記編輯一個新路徑。SVN不會自動遞迴建立目錄,要自己先建立好父目錄。比如想建立分支/branches/V1.0,那麼V1.0可以不用自己建立,但是/branches要先建立好。這裡是branches/V1.0,我們即將建立的分支便存放於此處,點選OK
上圖中紅色方框內Create copy revision in the repository下的選項:
u HEAD revision in the repository:拷貝當前主幹中的最新版本。不需要從你的工作副本中傳輸任何資料,這個分支的建立是非常快的。
u Specific revision in repository:拷貝主幹中的某個指定版本。假如你在上週釋出了專案時忘記了做標記,這將非常有用。如果記不起來版本號,通過點選滑鼠右鍵來顯示版本日誌,同時從這裡選取版本號。和上次一樣不需要從你的工作副本中傳輸任何資料,這個分支建立起來是非常快的。
u Working copy:新的分支是一個完全等同於你的本地工作副本的一個拷貝。如果你更新了一些檔案到你的工作副本的某個舊版本里,或者你在本地做出了修改,這些改變將準確無誤地進入拷貝中。自然而然地這種綜合的標記會包含正在從工作副本傳輸到版本庫中的資料,如果這些資料還不存在的話。
選擇完畢後單擊【OK】按鈕,則分支建立完畢。再次檢視配置庫,可以看到剛才建立的分支中包括主幹中的文件“readme.txt”,版本為659,同主幹一致。
標記的建立方法同分支一樣,都是對主幹的拷貝操作(實際是對某一版本的連結)。
4.2 合併分支
分支用來維護獨立的開發支線,在一些階段,你可能需要將分支上的修改合併到最新版本,或者將最新版本的修改合併到分支
為便於比較結果,我們修改分支中的readme檔案(此時版本為664),同時新增一個檔案:
如果想將分支合併到主幹上,在本地checkout出的主幹(trunk)目錄上單擊滑鼠右鍵,在彈出選單中選擇“TortoisesSVN”→“Merge”
在彈出的“Merge”選單中選擇類別:
在“URL to merge form”輸入框中選擇分支的URL,在“Reverse range to merge”填入版本,可點選【show log】按鈕選擇需要合併的版本。需要注意的是Merge並非字面上所示的將兩個分支歸併到一起,而是diff-and-apply的意思,比較兩個分支的差異並歸併差異。輸入完畢後單擊【Next】:
選擇合併選項後(如“Compare whitespaces”),單擊【Merge】,完成合並操作。
如果在合併過程中發生衝突,SVN會進行提示:
進行合併後,在本地的trunk目錄會顯示以下檔案:
衝突的檔案圖示中會有一個歎號,同時系統自動生成3個檔案:
u readme.txt為合併前主幹中的版本
u readme.txt.merge-left.r.664:為664版本,即建立分支時主幹中的版本
u readme.txt.merge-right.r665:為665版本,即合併前分支中的版本
可以直接開啟檔案進行手動修改,衝突的內容會以議<<<<<<<…………>>>>>>>標識
也可以選中該檔案,右鍵→TortoiseSVN→Edit conflicts,TortoiseMerge視窗會顯示衝突檔案對比,可以在merged對話方塊中進行編輯:
修改完畢後,右鍵→TortoiseSVN→Resolved,此時系統自動生成的3個檔案會自動刪除,衝突檔案的圖示會變為未提交狀態,右鍵→SVN commit,提交到配置庫。
當有多個檔案conflict時,需要逐個resolve。
如果合併後的內容不滿意,可以通過撤銷來取消這次的合併操作,前提是未對合並後的檔案做提交操作。
總結如下:
- 如果是需要將分支的改動合併到主線上,需要在主線的工作副本下進行合併,合併的範圍是需要從分支上上次合併的版本到當前分支上最新的版本,如果主線和分支都修改了相同的檔案,合併後會出現衝突,然後解決衝突,提交,如果是第一次合併,則起始版本號是上次建立分支的版本號;
- 相反,如果是需要將主線的改動合併到分支上,需要在分支的工作副本下進行合併,合併的範圍是需要從主線上上次合併的版本到當前主線上最新的版本,合併後會出現衝突(衝突的前提如上種情況),然後解決衝突,如果主線修改但是分支沒有修改,則主線上合併的變更內容會增加到當前副本中,提交,如果是第一次合併,則起始版本號是上次建立分支的版本號
合併的工作是把主線或者分支上合併範圍內的所有改動列出,並對比當前副本的內容,由合併者手工修改衝突。如果當前工作副本是主線的,則合併的範圍是分支上的改動,如果工作副本是分支的,則合併範圍是主線上的改動。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
經常有人會說,樹衝突是很難解決的一類衝突,其實一旦瞭解了其原理,要解決也不難。先回顧下對於樹衝突的定義。
樹衝突:當一名開發人員移動、重新命名、刪除一個檔案或資料夾,而另一名開發人員也對它們進行了移動、重新命名、刪除或者僅僅是修改時就會發生樹衝突。
出現衝突時,一般會提示衝突的資訊是什麼。過後我們可以使用svn st來檢視當前狀態。svn st的各種狀態代表什麼,請參考此博文svn st狀態詳解。
先介紹一下概念
Delete : 其中目錄結構變化,都認為是Delete
Edit: 是指修改檔案
Local : 是你本地修改
Incoming :是別人修改,你要Update或Merge進來。
這樣應該有4個組合,但是Edit對Edit的組合應該是File Conflict,這個容易解決,不在Tree Conflict 討論範圍,所以有3種組合。再需要區別Update和Merge,就有了6種情況。分別是
Local delete, incoming edit upon update
Local edit, incoming delete upon update
Local delete, incoming delete upon update
Local missing, incoming edit upon merge
Local edit, incoming delete upon merge
Local delete, incoming delete upon merge
分別對這幾種情形解釋如下:
1.Local delete, incoming edit upon update(本地刪除,更新後傳入修改)
產生原因:1.A修改檔案Foo.c後提交到版本庫中,B將Foo.c重新命名為Bar.c或者刪除了Foo.c或者直接將Foo.c的父目錄Foo直接刪除 2.B更新工作副本會提示該衝突,在working copy顯示為Foo.c在本地刪除,被標記為衝突。如果是重新命名,則Bar.c被標記為新增,但是不包括A的修改。
解決:A與B要確認是否採用A的修改與是否重新命名。如果採用A的修改,並且要重新命名則修改後,標記衝突解決,svn resolved,最後提交;如果不採用A的修改,直接標記衝突解決提交即可。
2.Local edit, incoming delete upon update (本地編輯,更新後傳入刪除)
產生原因:1.A對Foo.c重新命名為Bar.c並提交到版本庫(或者A將Foo.c的上級目錄Foo修改為Bar),B在他的工作副本中對Foo.c進行修改。2.B提交前更新,會提示如此錯誤。
解決:同樣需要兩個人進行協商後修改。
3.Local delete, incoming delete upon update (本地刪除,更新後傳入刪除)
產生原因:1.A將Foo.c重新命名為Bar.c後提交,B對Foo.c重新命名為Bix.c。2.B更新本地工作副本是會提示該樹衝突。
解決:通過日誌查詢檔案被刪除即重新命名的原因,A與B協商後最終確認採用哪個名稱。
4.Local missing, incoming edit upon merge (本地丟失,合併後傳入修改)
產生原因:1.A在主幹上修改Foo.c,B在分支上將Foo.c重新命名為Bar.c。2.B合併A在主幹上的修改。
解決:B先標記衝突解決,然後將Foo.c拷貝至本地,將A的修改合併至自己的檔案中或者直接放棄A的修改,採用自己的修改。
5.Local edit, incoming delete upon merge (本地修改,合併後傳入刪除)
產生原因:1.A將Foo.c重新命名為Bar.c(或者將Foo.c的父目錄Foo改為Bar),B在分支上修改Foo.c。2.B合併A的修改時提示該衝突。Bar.c被標記為增加,Foo.c被標記為衝突。
解決:同樣根據日誌查詢到修改的源頭,兩人協商後解決。
6.Local delete, incoming delete upon merge (本地刪除,合併後傳入刪除)
產生原因:1.A在主幹上將Foo.c重新命名為Bar.c,B在分支上將Foo.c重新命名為Bix.c。2.B合併A的修改時會提示衝突。重新命名後的檔案被標記為新增,原來檔案被標記為樹衝突。
解決:通過日誌查詢到檔案被改名的時刻,兩人協商後解決。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
svn merge部分總是在用的時候要搜資料,於是特意把這一部分弄出來,以備以後使用
為了做實驗,要下載subversion,安裝伺服器,和TortoiseSVN客戶端
subversion下載地址 http://subversion.apache.org/
下載下來之後如下的包
安裝
成功後在命令下看
建立倉庫
目錄 E:\svn\repository
於是在目錄 E:\svn\repository下可以看到如下的目錄結構
我們安裝進入subversion的安裝目錄可以看到如下的結構
安裝目錄:C:\Program Files\Subversion
其中bin裡邊就是subversion的所有命令
建一個庫,並弄出一個branch和trunk,這兩個最好是不一樣的,然後我們才可以做以下的例子,不然也不用merge了,我做的庫如下圖所示
然後再隨便找一個目錄把其中睥一個branch或著trunk拉下來,我這裡建了一個目錄
C:\Documents and Settings\alecyan\桌面\test\abc
並把branch的程式碼拉了下來,下面我們開始做merge的一些例子
首先進入我們建好的目錄中
進入C:\Documents and Settings\alecyan\桌面\test\abc
點空白處
開始merge
merge有三個選項,很多人對這個三個選項有點迷糊,我們這裡就針對這三個選項進行詳細的說明
第一個選項
這裡是這個意思,這裡可以把trunk的某個版本或著某個版本到某個版本的一個範圍都可以merge到本地
點下一步後
下一步
下一步
在這個時候,可以先點一下test看看會出現什麼情況,這個對我們的本地檔案沒有影響的
測試的時候可以發現檔案有衝突
然後點merge
這裡點resove all later就是merge之後一個檔案一個檔案的解決衝突
開始解決衝突
預設的解決衝突的工具,這個東西很好用,用一下就熟悉了
可以看到我們的本地多了很多檔案
解決完衝突之後,點那個三角,意思是resoved已解決
第一選項完成
開始第二選項
第一個選項的意思 就是把某一個主動或著分支的某個版本merge到本地
下面的一些流程和第一選項基本一樣
這裡要注意,這個說明,如果選了這個選項,那麼我們本地的檔案必須不能有變化,要和版本庫上一樣才行
不然會如圖所示
我們重新更新程式碼 ,繼續
下面的操作就和第一選項一樣了
我們說說第在個選項
第三個選是說可以merge不同的版本樹到本地
再往下面就又和第一個第二個一樣了
好了,三個選項都說明完了,以後,要是有點陌生的話,可以再看看這裡就能馬上想起來,心中有數就不會操作的時候猶猶豫豫的了。