現在你與Sally在同一個項目的並行分支上工作:你在私有分支上,而Sally在主幹(trunk)或者叫做開發主線上。
由於有眾多的人參與項目,大多數人擁有主幹拷貝是很正常的,任何人如果進行一個長週期的修改會使得主幹陷入混亂,所以通常的做法是建立一個私有分支,提交修改到自己的分支,直到這階段工作結束。
所以,好消息就是你和Sally不會互相打擾,壞消息是有時候分離會太遠。記住「閉門造車」策略的問題,當你完成你的分支後,可能因為太多衝突,已經無法輕易合併你的分支和主幹的修改。
相反,在你工作的時候你和Sally仍然可以繼續分享修改,這依賴於你決定什麼值得分享,Subversion給你在分支間選擇性「拷貝」修改的能力,當你完成了分支上的所有工作,所有的分支修改可以被拷貝回到主幹。
在上一章節,我們提到你和Sally對integer.c在不同的分支上做過修改,如果你看了Sally的344版本的日誌訊息,你會知道她修正了一些拼寫錯誤,毋庸置疑,你的拷貝的文件也一定存在這些拼寫錯誤,所以你以後的對這個文件修改也會保留這些拼寫錯誤,所以你會在將來合併時得到許多衝突。最好是現在接收Sally的修改,而不是作了許多工作之後才來做。
是時間使用svn merge命令,這個命令的結果非常類似svn diff命令(在第 2 章 基本使用的內容),兩個命令都可以比較版本庫中的任何兩個對象並且描述其區別,舉個例子,你可以使用svn diff來查看Sally在版本344作的修改:
$ svn diff -c 344 http://svn.example.com/repos/calc/trunk
Index: integer.c
===================================================================
--- integer.c (revision 343)
+++ integer.c (revision 344)
@@ -147,7 +147,7 @@
case 6: sprintf(info->operating_system, "HPFS (OS/2 or NT)"); break;
case 7: sprintf(info->operating_system, "Macintosh"); break;
case 8: sprintf(info->operating_system, "Z-System"); break;
- case 9: sprintf(info->operating_system, "CPM"); break;
+ case 9: sprintf(info->operating_system, "CP/M"); break;
case 10: sprintf(info->operating_system, "TOPS-20"); break;
case 11: sprintf(info->operating_system, "NTFS (Windows NT)"); break;
case 12: sprintf(info->operating_system, "QDOS"); break;
@@ -164,7 +164,7 @@
low = (unsigned short) read_byte(gzfile); /* read LSB */
high = (unsigned short) read_byte(gzfile); /* read MSB */
high = high << 8; /* interpret MSB correctly */
- total = low + high; /* add them togethe for correct total */
+ total = low + high; /* add them together for correct total */
info->extra_header = (unsigned char *) my_malloc(total);
fread(info->extra_header, total, 1, gzfile);
@@ -241,7 +241,7 @@
Store the offset with ftell() ! */
if ((info->data_offset = ftell(gzfile))== -1) {
- printf("error: ftell() retturned -1.\n");
+ printf("error: ftell() returned -1.\n");
exit(1);
}
@@ -249,7 +249,7 @@
printf("I believe start of compressed data is %u\n", info->data_offset);
#endif
- /* Set postion eight bytes from the end of the file. */
+ /* Set position eight bytes from the end of the file. */
if (fseek(gzfile, -8, SEEK_END)) {
printf("error: fseek() returned non-zero\n");
svn merge命令幾乎完全相同,但不是打印區別到你的終端,它會直接作為本地修改作用到你的本地拷貝:
$ svn merge -c 344 http://svn.example.com/repos/calc/trunk U integer.c $ svn status M integer.c
svn merge的輸出告訴你的integer.c文件已經作了補綴(Patch)(patched),現在已經保留了Sally修改—修改從主幹「拷貝」到你的私有分支的工作副本,現在作為一個本地修改,在這種情況下,要靠你審查本地的修改來確定它們工作正常。
在另一種情境下,事情並不會運行得這樣正常,也許integer.c也許會進入衝突狀態,你必須使用標準過程(見第 2 章 基本使用)來解決這種狀態,或者你認為合併是一個錯誤的決定,你只需要運行svn revert放棄本地修改。
但是當你審查過你的合併結果後,你可以使用svn commit提交修改,在那一刻,修改已經合併到你的分支上了,在版本控制術語中,這種在分支之間拷貝修改的行為叫做搬運修改。
當你提交你的修改時,確定你的日誌訊息中說明你是從某一版本搬運了修改,舉個例子:
$ svn commit -m "integer.c: ported r344 (spelling fixes) from trunk." Sending integer.c Transmitting file data . Committed revision 360.
你將會在下一節看到,這是一條非常重要的「最佳實踐」。
一個警告:為什麼svn diff和svn merge在概念上是很接近,但語法上有許多不同,一定閱讀第 9 章 Subversion 完全參考來查看其細節或者使用svn help查看幫助。舉個例子,svn merge需要一個工作副本作為目標,就是一個地方來施展目錄樹修改,如果一個目標都沒有指定,它會假定你要做以下某個普通的操作:
你希望合併目錄修改到工作副本的當前目錄。
你希望合併修改到你的當前工作目錄的相同文件名的文件。
如果你合併一個目錄而沒有指定特定的目標,svn merge假定第一種情況,在你的當前目錄應用修改。如果你合併一個文件,而這個文件(或是一個有相同的名字文件)在你的當前工作目錄存在,svn merge假定第二種情況,你想對這個同名文件使用合併。
如果你希望修改應用到別的目錄,你需要說出來。舉個例子,你在工作副本的父目錄,你需要指定目標目錄:
$ svn merge -c 344 http://svn.example.com/repos/calc/trunk my-calc-branch U my-calc-branch/integer.c
你已經看到了svn merge命令的例子,你將會看到更多,如果你對合併是如何工作的感到迷惑,這並不奇怪,很多人和你一樣。許多入門使用者(特別是對版本控制很陌生的用戶)會對這個命令的正確語法感到不知所措,不知道怎樣和什麼時候使用這個特性,不要害怕,這個命令實際上比你想像的簡單!有一個簡單的技巧來幫助你理解svn merge的行為。
迷惑的主要原因是這個命令的名稱,術語「合併」不知什麼原因被用來表明分支的組合,或者是其他什麼神奇的資料混合,這不是事實,一個更好的名稱應該是svn diff-and-apply,這是發生的所有事件:首先兩個版本庫樹比較,然後將區別應用到本地拷貝。
這個命令包括三個參數:
初始的版本樹(通常叫做比較的左邊),
最終的版本樹(通常叫做比較的右邊),
一個接收區別的工作副本(通常叫做合併的目標)。
一旦這三個參數指定以後,兩個目錄樹將要做比較,比較結果將會作為本地修改應用到目標工作副本,當命令結束後,結果同你手工修改或者是使用svn add或svn delete沒有什麼區別,如果你喜歡這結果,你可以提交,如果不喜歡,你可以使用svn revert恢復修改。
svn merge的語法允許非常靈活的指定三個必要的參數,如下是一些例子:
$ svn merge http://svn.example.com/repos/branch1@150 \
http://svn.example.com/repos/branch2@212 \
my-working-copy
$ svn merge -r 100:200 http://svn.example.com/repos/trunk my-working-copy
$ svn merge -r 100:200 http://svn.example.com/repos/trunk
第一種語法使用URL@REV的形式直接列出了所有參數,第二種語法可以用來作為比較同一個URL的不同版本的簡略寫法,最後一種語法表示工作副本是可選的,如果省略,默認是當前目錄。
合併修改聽起來很簡單,但是實踐起來會是很頭痛的事,如果你重複合併兩個分支,你也許會合併兩次同樣的修改。當這種事情發生時,有時候事情會依然正常,當對文件打補綴(Patch)時,Subversion如果注意到這個文件已經有了相應的修改,而不會作任何操作,但是如果已經應用的修改又被修改了,你會得到衝突。
理想情況下,你的版本控制系統應該會阻止對一個分支做兩次改變操作,必須自動的記住那一個分支的修改已經接收了,並且可以顯示出來,用來盡可能幫助自動化的合併。
不幸的是,Subversion不是這樣一個系統,類似於CVS,Subversion並不記錄任何合併操作,[21]當你提交本地修改,版本庫並不能判斷出你是通過svn merge還是手工修改得到這些文件。
這對你這樣的用戶意味著什麼?這意味著除非Subversion以後發展這個特性,你必須手工的記錄這些訊息。最佳的方式是使用提交日誌訊息,像前面的例子提到的,推薦你在日誌訊息中說明合併的特定版本號(或是版本號的範圍),之後,你可以運行svn log來查看你的分支包含哪些修改。這可以幫助你小心的依序運行svn merge命令而不會進行多餘的合併。
在下一小節,我們要展示一些這種技巧的例子。
首先,一定要記住合併的工作副本沒有本地更改,並且最近已更新過。如果你的工作副本用這樣的方法「清理」,你會發現一些頭痛的事情。
因為合併只是導致本地修改,它不是一個高風險的操作,如果你在第一次操作錯誤,你可以運行svn revert來再試一次。
有時候你的工作副本很可能已經改變了,合併會針對存在的那一個文件,這時運行svn revert不會恢復你在本地作的修改,兩部分的修改無法識別出來。
在這個情況下,人們很樂意能夠在合併之前預測一下,一個簡單的方法是使用運行svn merge同樣的參數運行svn diff,另一種方式是傳遞--dry-run選項給merge命令來預覽:
$ svn merge --dry-run -c 344 http://svn.example.com/repos/calc/trunk U integer.c $ svn status # nothing printed, working copy is still unchanged.
--dry-run選項實際上並不修改本地拷貝,它只是顯示實際合併時的狀態訊息,對於得到潛在合併的「整體」預覽,這個命令很有用,因為svn diff包括太多細節。
就像svn update命令,svn merge會把修改應用到工作副本,因此它也會造成衝突,因為svn merge造成的衝突有時候會有些不同,本小節會解釋這些區別。
作為開始,我們假定本地沒有修改,當你svn update到一個特定修訂版本時,修改會「乾淨的」應用到工作副本,伺服器產生比較兩樹的增量資料:一個工作副本和你關注的版本樹的虛擬快照,因為比較的左邊同你擁有的完全相同,增量資料確保你把工作副本轉換到右邊的樹。
但是svn merge沒有這樣的保證,會導致很多的混亂:用戶可以詢問伺服器比較任何兩個樹,即使一個與工作副本毫不相關的!這意味著有潛在的人為錯誤,用戶有時候會比較兩個錯誤的樹,建立的增量資料不會乾淨的應用,svn merge會盡力應用更多的增量資料,但是有一些部分也許會難以完成,就像Unix下patch命令有時候會報告「failed hunks」錯誤,svn merge會報告「skipped targets」:
$ svn merge -r 1288:1351 http://svn.example.com/repos/branch U foo.c U bar.c Skipped missing target: 'baz.c' U glub.c C glorb.h $
在前一個例子中,baz.c也許會存在於比較的兩個分支快照裡,但工作副本裡不存在,比較的增量資料要應用到這個文件,這種情況下會發生什麼?「skipped」訊息意味著用戶可能是在比較錯誤的兩棵樹,這是經典的用戶錯誤,當發生這種情況,可以使用迭代恢復(svn revert --recursive)合併所作的修改,刪除恢復後留下的所有未版本化的文件和目錄,並且使用另外的參數運行svn merge。
也應當注意前一個例子顯示glorb.h發生了衝突,我們已經規定本地拷貝沒有修改:衝突怎麼會發生呢?因為用戶可以使用svn merge將過去的任何變化應用到當前工作副本,變化包含的文字修改也許並不能乾淨的應用到工作副本文件,即使這些文件沒有本地修改。
另一個svn update和svn merge的小區別是衝突產生的文件的名字不同,在「解決衝突(合併別人的修改)」一節,我們看到過更新產生的文件名字為filename.mine、filename.rOLDREV和filename.rNEWREV,當svn merge產生衝突時,它產生的三個文件分別為 filename.working、filename.left和filename.right。在這種情況下,術語「left」和「right」表示了兩棵樹比較時的兩邊,在兩種情況下,不同的名字會幫助你區分衝突是因為更新造成的還是合併造成的。
當與Subversion開發者交談時你一定會聽到提及術語祖先,這個詞是用來描述兩個對象的關係:如果他們互相關聯,一個對象就是另一個的祖先,或者相反。
舉個例子,假設你提交版本100,包括對foo.c的修改,則foo.c@99是foo.c@100的一個「祖先」,另一方面,假設你在版本101刪除這個文件,而在102版本提交一個同名的文件,在這個情況下,foo.c@99與foo.c@102看起來是關聯的(有同樣的路徑),但是事實上他們是完全不同的對象,它們並不共享同一個歷史或者說「祖先」。
指出svn diff和svn merge區別的重要性在於,前一個命令忽略祖先,如果你詢問svn diff來比較文件foo.c的版本99和102,你會看到行為基礎的區別,diff命令只是盲目的比較兩條路徑,但是如果你使用svn merge是比較同樣的兩個對象,它會注意到他們是不關聯的,而且首先嘗試刪除舊文件,然後新增新文件,輸出會是一個刪除緊接著一個增加:
D foo.c A foo.c
大多數合併包括比較包括祖先關聯的兩條樹,因此svn merge這樣運作,然而,你也許會希望merge命令能夠比較兩個不相關的目錄樹,舉個例子,你有兩個目錄樹分別代表了供應方軟體項目的不同版本(見「供方分支」一節),如果你使用svn merge進行比較,你會看到第一個目錄樹被刪除,而第二個樹新增上!在這個情況下,你僅僅是希望svn merge以路徑為基礎比較兩棵樹,而忽略文件和目錄的不相關性,當為合併命令新增--ignore-ancestry選項時,就會像svn diff一樣工作。(相反,--notice-ancestry會導致svn diff像merge命令一樣工作。)
一個普遍的願望是重構原始碼程式,特別是Java軟體項目。在改名中文件和目錄變亂,通常導致每個項目成員的極大破壞。聽起來好像應該使用分支,不是嗎?只是建立分支,變亂事情,然後合併回主幹,不對嗎?
唉,這個場景下這樣並不正確,可以看作Subversion當前的弱點,這個問題是因為Subversion的update還不是足夠的強壯,特別是針對拷貝和移動操作。
當你使用svn copy複製文件時,版本庫會記住新文件的出處,但是它不能將這個訊息傳遞給使用svn update或svn merge的客戶端,不是告訴客戶端「 將文件拷貝到新的位置」,而是傳遞一整個新文件。這樣會導致問題,特別是因為這件事也發生在改名的文件。 一個鮮為人知的事實是Subversion缺乏真正的重命名—svn move命令只是一個svn copy和svn delete的組合。
例如,假定我們在一個私有分支工作,你將integer.c改名為whole.c,你這是在分支上建立了原來文件的一個拷貝,並且刪除了原來的文件。同時,回到trunk,Sally提交了一些integer.c的修改,所以你需要將分支合併到主幹:
$ cd calc/trunk $ svn merge -r 341:405 http://svn.example.com/repos/calc/branches/my-calc-branch D integer.c A whole.c
第一眼看起來不是很差,但是很可能這不是你和Sally希望的,合併操作已經刪除了最新版本的integer.c(包含了Sally最新的修改),而且盲目的新增了你的whole.c文件—是舊版本的integer.c複製品。最終的結果是將你的「rename」合併到分支,並且從最新修訂版本刪除了Sally最近的修改。
這不是真的資料丟失;Sally的修改還在版本庫的歷史中,但是。在Subversion改進之前,最好小心對分支進行合併和改名。