### 在分支間拷貝修改
現在你與Sally在同一個項目的并行分支上工作:你在私有分支上,而Sally在主干(*trunk*)或者叫做開發主線上。
由于有眾多的人參與項目,大多數人擁有主干拷貝是很正常的,任何人如果進行一個長周期的修改會使得主干陷入混亂,所以通常的做法是建立一個私有分支,提交修改到自己的分支,直到這階段工作結束。
所以,好消息就是你和Sally不會互相打擾,壞消息是有時候分離會*太*遠。記住“閉門造車”策略的問題,當你完成你的分支后,可能因為太多沖突,已經無法輕易合并你的分支和主干的修改。
相反,在你工作的時候你和Sally仍然可以繼續分享修改,這依賴于你決定什么值得分享,Subversion給你在分支間選擇性“拷貝”修改的能力,當你完成了分支上的所有工作,所有的分支修改可以被拷貝回到主干。
### 拷貝特定的修改
在上一章節,我們提到你和Sally對`integer.c`在不同的分支上做過修改,如果你看了Sally的344版本的日志信息,你會知道她修正了一些拼寫錯誤,毋庸置疑,你的拷貝的文件也一定存在這些拼寫錯誤,所以你以后的對這個文件修改也會保留這些拼寫錯誤,所以你會在將來合并時得到許多沖突。最好是現在接收Sally的修改,而不是作了許多工作之后才來做。
是時間使用**svn merge**命令,這個命令的結果非常類似**svn diff**命令(在第3章的內容),兩個命令都可以比較版本庫中的任何兩個對象并且描述其區別,舉個例子,你可以使用**svn diff**來查看Sally在版本344作的修改:
~~~
$ svn diff -r 343: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 -r 343:344 http://svn.example.com/repos/calc/trunk
U integer.c
$ svn status
M integer.c
~~~
**svn merge**的輸出告訴你的`integer.c`文件已經作了補丁(patched),現在已經保留了Sally修改―修改從主干“拷貝”到你的私有分支的工作拷貝,現在作為一個本地修改,在這種情況下,要靠你審查本地的修改來確定它們工作正常。
在另一種情境下,事情并不會運行得這樣正常,也許`integer.c`也許會進入沖突狀態,你必須使用標準過程(見第三章)來解決這種狀態,或者你認為合并是一個錯誤的決定,你只需要運行**svn revert**放棄。
但是當你審查過你的合并結果后,你可以使用**svn commit**提交修改,在那一刻,修改已經合并到你的分支上了,在版本控制術語中,這種在分支之間拷貝修改的行為叫做*搬運*修改。
當你提交你的修改時,確定你的日志信息中說明你是從某一版本搬運了修改,舉個例子:
~~~
$ svn commit -m "integer.c: ported r344 (spelling fixes) from trunk."
Sending integer.c
Transmitting file data .
Committed revision 360.
~~~
你將會在下一節看到,這是一條非常重要的“最佳實踐”。
**為什么不使用補丁?**
也許你的腦中會出現一個問題,特別如果你是Unix用戶,為什么非要使用**svn merge**?為什么不簡單的使用操作系統的**patch**命令來進行相同的工作?舉個例子:
~~~
$ svn diff -r 343:344 http://svn.example.com/repos/calc/trunk > patchfile
$ patch -p0 < patchfile
Patching file integer.c using Plan A...
Hunk #1 succeeded at 147.
Hunk #2 succeeded at 164.
Hunk #3 succeeded at 241.
Hunk #4 succeeded at 249.
done
~~~
在這種情況下,確實沒有區別,但是**svn merge**有超越**patch**的特別能力,使用**patch**對文件格式有一定的限制,它只能針對文件內容,沒有方法表現*目錄樹*的修改,例如添加、刪除或是改名。如果Sally的修改包括增加一個新的目錄,**svn diff**不會注意到這些,**svn diff**只會輸出有限的補丁格式,所以有些問題無法表達。 但是**svn merge**命令會通過直接作用你的工作拷貝來表示目錄樹的修改。
一個警告:為什么**svn diff**和**svn merge**在概念上是很接近,但語法上有許多不同,一定閱讀第9章來查看其細節或者使用**svn help**查看幫助。舉個例子,**svn merge**需要一個工作拷貝作為目標,就是一個地方來施展目錄樹修改,如果一個目標都沒有指定,它會假定你要做以下某個普通的操作:
1.
你希望合并目錄修改到工作拷貝的當前目錄。
1.
你希望合并修改到你的當前工作目錄的相同文件名的文件。
如果你合并一個目錄而沒有指定特定的目標,**svn merge**假定第一種情況,在你的當前目錄應用修改。如果你合并一個文件,而這個文件(或是一個有相同的名字文件)在你的當前工作目錄存在,**svn merge**假定第二種情況,你想對這個同名文件使用合并。
如果你希望修改應用到別的目錄,你需要說出來。舉個例子,你在工作拷貝的父目錄,你需要指定目標目錄:
~~~
$ svn merge -r 343: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**,這是發生的所有事件:首先兩個版本庫樹比較,然后將區別應用到本地拷貝。
這個命令包括三個參數:
1.
初始的版本樹(通常叫做比較的*左邊*),
1.
最終的版本樹(通常叫做比較的*右邊*),
1.
一個接收區別的工作拷貝(通常叫做合并的*目標*)。
一旦這三個參數指定以后,兩個目錄樹將要做比較,比較結果將會作為本地修改應用到目標工作拷貝,當命令結束后,結果同你手工修改或者是使用**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的不同版本的簡略寫法,最后一種語法表示工作拷貝是可選的,如果省略,默認是當前目錄。
### 合并的最佳實踐
#### 手工追蹤合并
合并修改聽起來很簡單,但是實踐起來會是很頭痛的事,如果你重復合并兩個分支,你也許會合并*兩次*同樣的修改。當這種事情發生時,有時候事情會依然正常,當對文件打補丁時,Subversion如果注意到這個文件已經有了相應的修改,而不會作任何操作,但是如果已經應用的修改又被修改了,你會得到沖突。
理想情況下,你的版本控制系統應該會阻止對一個分支做兩次改變操作,必須自動的記住那一個分支的修改已經接收了,并且可以顯示出來,用來盡可能幫助自動化的合并。
不幸的是,Subversion不是這樣一個系統,類似于CVS,Subversion并不記錄任何合并操作,當你提交本地修改,版本庫并不能判斷出你是通過**svn merge**還是手工修改得到這些文件。
這對你這樣的用戶意味著什么?這意味著除非Subversion以后發展這個特性,你必須手工的記錄這些信息。最佳的方式是使用提交日志信息,像前面的例子提到的,推薦你在日志信息中說明合并的特定版本號(或是版本號的范圍),之后,你可以運行**svn log**來查看你的分支包含哪些修改。這可以幫助你小心的依序運行**svn merge**命令而不會進行多余的合并。
在下一小節,我們要展示一些這種技巧的例子。
#### 預覽合并
因為合并只是導致本地修改,它不是一個高風險的操作,如果你在第一次操作錯誤,你可以運行**svn revert**來再試一次。
有時候你的工作拷貝很可能已經改變了,合并會針對存在的那一個文件,這時運行**svn revert**不會恢復你在本地作的修改,兩部分的修改無法識別出來。
在這個情況下,人們很樂意能夠在合并之前預測一下,一個簡單的方法是使用運行**svn merge**同樣的參數運行**svn diff**,另一種方式是傳遞`--dry-run`選項給merge命令:
~~~
$ svn merge --dry-run -r 343:344 http://svn.example.com/repos/calc/trunk
U integer.c
$ svn status
# nothing printed, working copy is still unchanged.
~~~
`--dry-run`選項實際上并不修改本地拷貝,它只是顯示實際合并時的狀態信息,對于得到“整體”的印象,這個命令很有用,因為**svn diff**包括太多細節。
**Subversion與修改集**
每一個人對于“修改集”的概念都有些不一樣,至少對于版本控制系統的“修改集特性”這一概念有著不同的期望,根據我們的用途,可以說修改集只是一個有唯一名字的一系列修改集合,修改也許包括文件內容的修改,目錄樹結構的修改,或是元數據的調整,更通常的說法,一個修改集就是我們可以引用的有名字的補丁。
在Subversion里,一個全局的修訂版本號N標示一個版本庫中的樹:它代表版本庫在N次提交后的樣子,它也是一個修改集的隱含名稱:如果你比較樹N與樹N-1,你可以得到你提交的補丁。出于這個原因,想象“版本N”并不只是一棵樹,也是一個修改集。如果你使用一個問題追蹤工具來管理bug,你可以使用版本號來表示特定的補丁修正了bug―舉個例子,“這個問題是在版本9238修正的”,然后其他人可以運行**svn log -r9238**來查看修正這個bug的修改集,或者使用**svn diff -r9237:9238**來看補丁本身。Subversion合并命令也使用版本號作為參數,可以將特定修改集從一個分支合到另一個分支:**svn merge -r9237:9238**將會合并修改集#9238到本地拷貝。
#### 合并沖突
就像**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,你會看到行為基礎的區別,區別命令只是盲目的比較兩條路徑,但是如果你使用**svn merge**是比較同樣的兩個對象,它會注意到他們是不關聯的,而且首先嘗試刪除舊文件,然后添加新文件,你會看到`A foo.c`后面緊跟`D foo.c`。
大多數合并包括比較包括祖先關聯的兩條樹,因此**svn merge**這樣運作,然而,你也許會希望合并命令能夠比較兩個不相關的目錄樹,舉個例子,你有兩個目錄樹分別代表了賣主軟件項目的不同版本(見[“賣主分支”一節]( "賣主分支")),如果你使用**svn merge**進行比較,你會看到第一個目錄樹被刪除,而第二個樹添加上!
在這個情況下,你只是希望**svn merge**能夠做一個以路徑為基礎的比較,忽略所有文件和目錄的關系,增加`--ignore-ancestry`選項會導致命令象**svn diff**一樣。(相應的,`--notice-ancestry`選項會使**svn diff**象合并命令一樣行事。)
在將來,Subversion項目將會計劃(或者發明)一種擴展補丁格式來描述目錄樹改變。
- 第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
- 術語表