### 常見用例
分支和**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 --verbose --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**來“取消”這個工作拷貝上所作的操作,然后提交本地修改到版本庫,你要做得只是指定一個相反的區別:
~~~
$ svn merge -r 303:302 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.
~~~
我們可以把版本庫修訂版本想象成一組修改(一些版本控制系統叫做*修改集*),通過`-r`選項,你可以告訴**svn merge**來應用修改集或是一個修改集范圍到你的工作拷貝,在我們的情況例子里,我們使用**svn merge**合并修改集#303到工作拷貝。
記住回滾修改和任何一個**svn merge**命令都一樣,所以你應該使用**svn status**或是**svn diff**來確定你的工作處于期望的狀態中,然后使用**svn commit**來提交,提交之后,這個特定修改集不會反映到`HEAD`版本了。
繼續,你也許會想:好吧,這不是真的取消提交吧!是吧?版本303還依然存在著修改,如果任何人取出`calc`的303-349版本,他還會得到錯誤的修改,對吧?
是的,這是對的。當我們說“刪除”一個修改時,我們只是說從`HEAD`刪除,原始的修改還保存在版本庫歷史中,在多數情況下,這是足夠好的。大多數人只是對追蹤`HEAD`版本感興趣,在一些特定情況下,你也許希望毀掉所有提交的證據(或許某個人提交了一個秘密文件),這不是很容易的,因為Subversion設計用來不丟失任何信息,每個修訂版本都是不可變的目錄樹 ,從歷史刪除一個版本會導致多米諾效應,會在后面的版本導致混亂甚至會影響所有的工作拷貝。
### 找回刪除的項目
版本控制系統非常重要的一個特性就是它的信息從不丟失,即使當你刪除了文件或目錄,它也許從HEAD版本消失了 ,但這個對象依然存在于歷史的早期版本 ,一個新手經常問到的問題是“怎樣找回我的文件和目錄?”
第一步首先要知道需要拯救的項目是**什么**,這里有個很有用的比喻:你可以認為任何存在于版本庫的對象生活在一個二維的坐標系統里,第一維是一個特定的版本樹,第二維是在樹中的路徑,所以你的文件或目錄的任何版本可以有這樣一對坐標定義。
Subversion沒有向CVS一樣的`古典`目錄, 所以你需要**svn log**來察看你需要找回的坐標對,一個好的策略是使用**svn log --verbose**來察看你刪除的項目,--verbose選項顯示所有改變的項目的每一個版本 ,你只需要找出你刪除文件或目錄的那一個版本。你可以通過目測找出這個版本,也可以使用另一種工具來檢查日志的輸出 (通過**grep**或是在編輯器里增量查找)。
~~~
$ cd parent-dir
$ svn log --verbose
…
------------------------------------------------------------------------
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 --revision 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**,畢竟,工作拷貝不就是一個非常淺的分支嗎?只是它一次只可以保存一個修改。
Subversion項目有計劃,不管用什么方式,總有一天要實現**svnadmin obliterate**命令來進行永久刪除操作,而此時可以看[“svndumpfilter”一節]( "svndumpfilter")。
因為CVS沒有版本樹,它會在每個版本庫目錄創建一個`古典`區域用來保存增量數據。
- 第1章介紹
- Subversion的歷史
- Subversion的特性
- Subversion的架構
- 安裝Subversion
- Subversion的組件
- 快速入門
- 第2章基本概念
- 版本模型
- Subversion實戰
- 摘要
- 第3章指導教程
- 導入
- 修訂版本: 號碼、關鍵字和日期,噢,我的!
- 初始化的Checkout
- 基本的工作周期
- 檢驗歷史
- 其他有用的命令
- 摘要
- 第4章分支與合并
- 使用分支
- 在分支間拷貝修改
- 常見用例
- 轉換工作拷貝
- 標簽
- 分支維護
- 摘要
- 第5章版本庫管理
- 版本庫的創建和配置
- 版本庫維護
- 添加項目
- 摘要
- 第6章配置服務器
- 網絡模型
- svnserve,一個自定義的服務器
- httpd,Apache的HTTP服務器
- 支持多種版本庫訪問方法
- 第7章高級主題
- 屬性
- Peg和實施修訂版本
- 外部定義
- 賣主分支
- 本地化
- Subversion版本庫URL
- 第8章開發者信息
- 使用API
- 進入工作拷貝的管理區
- WebDAV
- 使用內存池編程
- 為Subversion做貢獻
- 第9章Subversion完全參考
- svn add
- svn blame
- svn cat
- svn checkout
- svn cleanup
- svn commit
- svn copy
- svn delete
- svn diff
- svn export
- svn help
- svn import
- svn info
- svn list
- svn log
- svn merge
- svn mkdir
- svn move
- svn propdel
- svn propedit
- svn propget
- svn proplist
- svn propset
- svn resolved
- svn revert
- svn status
- svn switch
- svn update
- svnadmin
- svnadmin create
- svnadmin deltify
- svnadmin dump
- svnadmin help
- svnadmin hotcopy
- svnadmin list-dblogs
- svnadmin list-unused-dblogs
- svnadmin load
- svnadmin lstxns
- svnadmin recover
- svnadmin rmtxns
- svnadmin setlog
- svnadmin verify
- svnlook
- svnlook author
- svnlook cat
- svnlook changed
- svnlook date
- svnlook diff
- svnlook dirs-changed
- svnlook help
- svnlook history
- svnlook info
- svnlook log
- svnlook propget
- svnlook proplist
- svnlook tree
- svnlook uuid
- svnlook youngest
- svnserve
- svnversion
- mod_dav_svn Configuration Directives
- 附錄A.Subversion對于CVS用戶
- 目錄的版本
- 更多離線操作
- 區分狀態和更新
- 分支和標簽
- 元數據屬性
- 沖突解決
- 二進制文件和轉化
- 版本化的模塊
- 認證
- 轉化CVS版本庫到Subversion
- 附錄C.WebDAV和自動版本化
- 自動版本化交互性
- Subversion和DeltaV
- 術語表