分支和svn merge有很多不同的用法,這個小節描述了最常見的用法。
為了完成這個例子,我們將時間往前推進,假定已經過了幾天,在主幹和你的分支上都有許多更改,假定你完成了分支上的工作,已經完成了特性或bug修正,你想合併所有分支的修改到主幹上,讓別人也可以使用。
這種場景下如何使用svn merge?記住這個命令比較兩個目錄樹,然後應用比較結果到工作副本,所以要接受這種變化,你需要主幹的工作副本,我們假設你有一個最初的主幹工作副本(完全更新),或者是你最近取出了/calc/trunk的一個乾淨的工作副本。
但是要哪兩個樹進行比較呢?乍一看,回答很明確,只要比較最新的主幹與分支。但是你要意識到—這個想法是錯誤的,傷害了許多入門使用者!因為svn merge的操作很像svn diff,比較最新的主幹和分支樹不僅僅會描述你在分支上所作的修改,這樣的比較會展示太多的不同,不僅包括分支上的增加,也包括了主幹上的刪除操作,而這些刪除根本就沒有在分支上發生過。
為了表示你的分支上的修改,你只需要比較分支的初始狀態與最終狀態,在你的分支上使用svn log命令,你可以看到你的分支在341版本建立,你的分支最終的狀態用HEAD版本表示,這意味著你希望能夠比較版本341和HEAD的分支目錄,然後應用這些分支的修改到主幹目錄的工作副本。
查找分支產生的版本(分支的「基準」)的最好方法是在svn log中使用--stop-on-copy選項,log子命令通常會顯示所有關於分支的變化,包括建立分支的過程,就好像你在主幹上一樣,--stop-on-copy會在svn log檢測到目標拷貝或者改名時中止日誌輸出。
所以,在我們的例子裡,
$ svn log -v --stop-on-copy \
http://svn.example.com/repos/calc/branches/my-calc-branch
…
------------------------------------------------------------------------
r341 | user | 2002-11-03 15:27:56 -0600 (Thu, 07 Nov 2002) | 2 lines
Changed paths:
A /calc/branches/my-calc-branch (from /calc/trunk:340)
$
正如所料,最後打印出的版本正是由my-calc-branch拷貝生成的版本。
如下是最終的合併過程,然後:
$ cd calc/trunk $ svn update At revision 405. $ svn merge -r 341:405 http://svn.example.com/repos/calc/branches/my-calc-branch U integer.c U button.c U Makefile $ svn status M integer.c M button.c M Makefile # ...examine the diffs, compile, test, etc... $ svn commit -m "Merged my-calc-branch changes r341:405 into the trunk." Sending integer.c Sending button.c Sending Makefile Transmitting file data ... Committed revision 406.
再次說明,日誌訊息中詳細描述了合併到主幹的的修改範圍,記住一定要這麼做,這是你以後需要的重要訊息。
舉個例子,你希望在分支上繼續工作一周,來進一步加強你的修正,這時版本庫的HEAD版本是480,你準備好了另一次合併,但是我們在「合併的最佳實踐」一節提到過,你不想合併已經合併的內容,你只想合併新的東西,技巧就是指出什麼是「新」的。
第一步是在主幹上運行svn log察看最後一次與分支合併的日誌訊息:
$ cd calc/trunk $ svn log … ------------------------------------------------------------------------ r406 | user | 2004-02-08 11:17:26 -0600 (Sun, 08 Feb 2004) | 1 line Merged my-calc-branch changes r341:405 into the trunk. ------------------------------------------------------------------------ …
阿哈!因為分支上341到405之間的所有修改已經在版本406合併了,現在你只需要合併分支在此之後的修改—通過比較406和HEAD。
$ cd calc/trunk $ svn update At revision 480. # We notice that HEAD is currently 480, so we use it to do the merge: $ svn merge -r 406:480 http://svn.example.com/repos/calc/branches/my-calc-branch U integer.c U button.c U Makefile $ svn commit -m "Merged my-calc-branch changes r406:480 into the trunk." Sending integer.c Sending button.c Sending Makefile Transmitting file data ... Committed revision 481.
現在主幹有了分支上第二波修改的完全結果,此刻,你可以刪除你的分支(我們會在以後討論),或是繼續在你分支上工作,重複這個步驟。
svn merge另一個常用的做法是取消已經做得提交,假設你愉快的在/calc/trunk工作,你發現303版本對integer.c的修改完全錯了,它不應該被提交,你可以使用svn merge來「取消」這個工作副本上所作的操作,然後提交本地修改到版本庫,你要做得只是指定一個相反的區別。(你可以通過指定--revision 303:302--change -303
$ svn merge -c -303 http://svn.example.com/repos/calc/trunk U integer.c $ svn status M integer.c $ svn diff … # verify that the change is removed … $ svn commit -m "Undoing change committed in r303." Sending integer.c Transmitting file data . Committed revision 350.
我們可以把版本庫修訂版本想像成一組修改(一些版本控制系統叫做異動清單(Changesets)),通過-r選項,你可以告訴svn merge來應用異動清單(Changesets)或是一個異動清單(Changesets)範圍到你的工作副本,在我們的情況例子裡,我們使用svn merge合併異動清單(Changesets)#303到工作副本。
記住回滾修改和任何一個svn merge命令都一樣,所以你應該使用svn status或是svn diff來確定你的工作處於期望的狀態中,然後使用svn commit來提交,提交之後,這個特定異動清單(Changesets)不會反映到HEAD版本了。
繼續,你也許會想:好吧,這不是真的取消提交吧!是吧?版本303還依然存在著修改,如果任何人取出calc的303-349版本,他還會得到錯誤的修改,對吧?
是的,這是對的。當我們說「刪除」一個修改時,我們只是說從HEAD刪除,原始的修改還保存在版本庫歷史中,在多數情況下,這是足夠好的。大多數人只是對追蹤HEAD版本感興趣,在一些特定情況下,你也許希望毀掉所有提交的證據(或許某個人提交了一個秘密文件),這不是很容易的,因為Subversion設計用來不丟失任何訊息,每個修訂版本都是依賴其它修訂版本的不可變目錄樹 ,從歷史刪除一個版本會導致多米諾效應,會在後面的版本導致混亂甚至會影響所有的工作副本。 [22]
版本控制系統非常重要的一個特性就是它的訊息從不丟失,即使當你刪除了文件或目錄,它也許從HEAD版本消失了 ,但這個對象依然存在於歷史的早期版本 ,一個新手經常問到的問題是「怎樣找回我的文件和目錄?」。
第一步首先要知道需要拯救的項目是什麼,這裡有個很有用的比喻:你可以認為任何存在於版本庫的對象生活在一個二維的坐標系統裡,第一維是一個特定的版本樹,第二維是在樹中的路徑,所以你的文件或目錄的任何版本可以通過這樣一對坐標定義。(記住常見的「peg修訂版本」語法— foo.c@224 — 在前面的「Peg 和實施修訂版本」一節提到過。 )
首先,你需要svn log來察看你需要找回的坐標對,一個好的策略是使用svn log --verbose來察看包含刪除項目的目錄,--verbose選項顯示所有改變的項目的每一個版本 ,你只需要找出你刪除文件或目錄的那一個版本。你可以通過目測找出這個版本,也可以使用另一種工具來檢查日誌的輸出 (通過grep或是在編輯器裡增量查找)。
$ cd parent-dir $ svn log -v … ------------------------------------------------------------------------ r808 | joe | 2003-12-26 14:29:40 -0600 (Fri, 26 Dec 2003) | 3 lines Changed paths: D /calc/trunk/real.c M /calc/trunk/integer.c Added fast fourier transform functions to integer.c. Removed real.c because code now in double.c. …
在這個例子裡,你可以假定你正在找已經刪除了的文件real.c,通過查找父目錄的歷史 ,你知道這個文件在808版本被刪除,所以存在這個對象的版本在此之前 。結論:你想從版本807找回/calc/trunk/real.c。
以上是最重要的部分—重新找到你需要恢復的對象。現在你已經知道該恢復的文件,而你有兩種選擇。
一種是對版本反向使用svn merge到808(我們已經學會了如何取消修改,見「取消修改」一節),這樣會重新新增real.c,這個文件會列入增加的計劃,經過一次提交,這個文件重新回到HEAD。
在這個例子裡,這不是一個好的策略,這樣做不僅把real.c加入新增到計劃,也取消了對integer.c的修改,而這不是你期望的。確實,你可以恢復到版本808,然後對integer.c執行取消svn revert操作,但這樣的操作無法擴大使用,因為如果從版本808修改了90個文件怎麼辦?
所以第二個方法不是使用svn merge,而是使用svn copy命令,精確的拷貝版本和路徑「坐標對」到你的工作副本:
$ svn copy -r 807 \
http://svn.example.com/repos/calc/trunk/real.c ./real.c
$ svn status
A + real.c
$ svn commit -m "Resurrected real.c from revision 807, /calc/trunk/real.c."
Adding real.c
Transmitting file data .
Committed revision 1390.
加號標誌表明這個項目不僅僅是計劃增加中,而且還包含了歷史,Subversion記住了它是從哪個拷貝過來的。在將來,對這個文件運行svn log會看到這個文件在版本807之前的歷史,換句話說,real.c不是新的,而是原先刪除的那一個的後代。
儘管我們的例子告訴我們如何找回文件,對於恢復刪除的目錄也是一樣的。
版本控制在軟體開發中廣泛使用,這裡是團隊里程式設計師最常用的兩種分支/合併模式的介紹,如果你不是使用Subversion軟體開發,可隨意跳過本小節,如果你是第一次使用版本控制的軟體開發者,請更加注意,以下模式被許多老兵當作最佳實踐,這個過程並不只是針對Subversion,在任何版本控制系統中都一樣,但是在這裡使用Subversion術語會感覺更方便一點。
大多數軟體存在這樣一個生命週期:編碼、測試、發佈,然後重複。這樣有兩個問題,第一,開發者需要在質量保證小組測試假定穩定版本時繼續開發新特性,新工作在軟體測試時不可以中斷,第二,小組必須一直支持老的發佈版本和軟體;如果一個bug在最新的代碼中發現,它一定也存在已發佈的版本中,客戶希望立刻得到錯誤修正而不必等到新版本發佈。
這是版本控制可以做的幫助,典型的過程如下:
開發者提交所有的新特性到主幹。 每日的修改提交到/trunk:新特性,bug修正和其他。
這個主幹被拷貝到「發佈」分支。 當小組認為軟體已經做好發佈的準備(如,版本1.0)然後/trunk會被拷貝到/branches/1.0。
項目組繼續並行工作,一個小組開始對分支進行嚴酷的測試,同時另一個小組在/trunk繼續新的工作(如,準備2.0),如果一個bug在任何一個位置被發現,錯誤修正需要來回運送。然而這個過程有時候也會結束,例如分支已經為發佈前的最終測試「停滯」了。
分支已經作了標籤並且發佈,當測試結束,/branches/1.0作為引用快照已經拷貝到/tags/1.0.0,這個標籤被打包發佈給客戶。
分支多次維護。當繼續在/trunk上為版本2.0工作,bug修正繼續從/trunk運送到/branches/1.0,如果積累了足夠的bug修正,管理部門決定發佈1.0.1版本:拷貝/branches/1.0到/tags/1.0.1,標籤被打包發佈。
整個過程隨著軟體的成熟不斷重複:當2.0完成,一個新的2.0分支被建立,測試、打標籤和最終發佈,經過許多年,版本庫結束了許多版本發佈,進入了「維護」模式,許多標籤代表了最終的發佈版本。
一個特性分支是本章中那個重要例子中的分支,你正在那個分支上工作,而Sally還在/trunk繼續工作,這是一個臨時分支,用來作複雜的修改而不會干擾/trunk的穩定性,不像發佈分支(也許要永遠支持),特性分支出生,使用了一段時間,合併到主幹,然後最終被刪除掉,它們在有限的時間裡有用。
還有,關於是否建立特性分支的項目政策也變化廣泛,一些項目永遠不使用特性分支:大家都可以提交到/trunk,好處是系統的簡單—沒有人需要知道分支和合併,壞處是主幹會經常不穩定或者不可用,另外一些項目使用分支達到極限:沒有修改曾經直接提交到主幹,即使最細小的修改都要建立短暫的分支,然後小心的審核合併到主幹,然後刪除分支,這樣系統保持主幹一直穩定和可用,但是造成了巨大的負擔。
許多項目採用折中的方式,堅持每次編譯/trunk並進行回歸測試,只有需要多次不穩定提交時才需要一個特性分支,這個規則可以用這樣一個問題檢驗:如果開發者在好幾天裡獨立工作,一次提交大量修改(這樣/trunk就不會不穩定。),是否會有太多的修改要來回顧?如果答案是「是」,這些修改應該在特性分支上進行,因為開發者增量的提交修改,你可以容易的回頭檢查。
最終,有一個問題就是怎樣保持一個特性分支「同步」於工作中的主幹,在前面提到過,在一個分支上工作數周或幾個月是很有風險的,主幹的修改也許會持續湧入,因為這一點,兩條線的開發會區別巨大,合併分支回到主幹會成為一個噩夢。
這種情況最好通過有規律的將主幹合併到分支來避免,制定這樣一個政策:每週將上周的修改合併到分支,注意這樣做時需要小心,需要手工記錄合併的過程,以避免重複的合併(在「手工追蹤合併」一節描述過),你需要小心的撰寫合併的日誌訊息,精確的描述合併包括的範圍(在「合併分支到另一分支」一節中描述過),這看起來像是脅迫,可是實際上是容易做到的。
在一些時候,你已經準備好了將「同步的」特性分支合併回到主幹,為此,開始做一次將主幹最新修改和分支的最終合併,這樣以後,除了你的分支修改的部分,最新的分支和主幹將會絕對一致,所以在這個特別的例子裡,你會通過直接比較分支和主幹來進行合併:
$ cd trunk-working-copy
$ svn update
At revision 1910.
$ svn merge http://svn.example.com/repos/calc/trunk@1910 \
http://svn.example.com/repos/calc/branches/mybranch@1910
U real.c
U integer.c
A newdirectory
A newdirectory/newfile
…
通過比較HEAD修訂版本的主幹和HEAD修訂版本的分支,你確定了只在分支上的增量訊息,兩條開發線都有了分枝的修改。
可以用另一種考慮這種模式,你每週按時同步分支到主幹,類似於在工作副本執行svn update的命令,最終的合併操作類似於在工作副本運行svn commit,畢竟,工作副本不就是一個非常淺的分支嗎?只是它一次只可以保存一個修改。