http://blog.sina.com.cn/s/blog_620eb3b20101hvz7.html
解決版本沖突-使用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。
如果合並后的內容不滿意,可以通過撤銷來取消這次的合並操作,前提是未對合並后的文件做提交操作。
總結如下:
- 如果是需要將分支的改動合並到主線上,需要在主線的工作副本下進行合並,合並的范圍是需要從分支上上次合並的版本到當前分支上最新的版本,如果主線和分支都修改了相同的文件,合並后會出現沖突,然后解決沖突,提交,如果是第一次合並,則起始版本號是上次建立分支的版本號;
- 相反,如果是需要將主線的改動合並到分支上,需要在分支的工作副本下進行合並,合並的范圍是需要從主線上上次合並的版本到當前主線上最新的版本,合並后會出現沖突(沖突的前提如上種情況),然后解決沖突,如果主線修改但是分支沒有修改,則主線上合並的變更內容會增加到當前副本中,提交,如果是第一次合並,則起始版本號是上次建立分支的版本號
合並的工作是把主線或者分支上合並范圍內的所有改動列出,並對比當前副本的內容,由合並者手工修改沖突。如果當前工作副本是主線的,則合並的范圍是分支上的改動,如果工作副本是分支的,則合並范圍是主線上的改動。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
經常有人會說,樹沖突是很難解決的一類沖突,其實一旦了解了其原理,要解決也不難。先回顧下對於樹沖突的定義。
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不同的版本樹到本地
再往下面就又和第一個第二個一樣了
好了,三個選項都說明完了,以后,要是有點陌生的話,可以再看看這里就能馬上想起來,心中有數就不會操作的時候猶猶豫豫的了。