本篇文章給大家?guī)砝?a href="http://m.babyishan.com/?golink=aHR0cHM6Ly93d3cucGhwLmNuL3Rvb2wvZ2l0Lw==" target="_blank" rel="noopener">git的相關(guān)知識,其中主要介紹了高級合并,包括了合并沖突、撤銷合并等相關(guān)問題,希望對大家有幫助。
推薦學(xué)習(xí):《git》
高級合并
在 git 中合并是相當(dāng)容易的。 因為 Git 使多次合并另一個分支變得很容易,這意味著你可以有一個始終保持最新的長期分支, 經(jīng)常解決小的沖突,比在一系列提交后解決一個巨大的沖突要好。
然而,有時也會有棘手的沖突。 不像其他的版本控制系統(tǒng),Git 并不會嘗試過于聰明的合并沖突解決方案。 Git 的哲學(xué)是聰明地決定無歧義的合并方案,但是如果有沖突,它不會嘗試智能地自動解決它。 因此,如果很久之后才合并兩個分叉的分支,你可能會撞上一些問題。
在本節(jié)中,我們將會仔細(xì)查看那些問題是什么以及 Git 給了我們什么工具來幫助我們處理這些更難辦的情形。 我們也會了解你可以做的不同的、非標(biāo)準(zhǔn)類型的合并,也會看到如何后退到合并之前。
合并沖突
我們在?遇到?jīng)_突時的分支合并?介紹了解決合并沖突的一些基礎(chǔ)知識, 對于更復(fù)雜的沖突,Git 提供了幾個工具來幫助你指出將會發(fā)生什么以及如何更好地處理沖突。
首先,在做一次可能有沖突的合并前盡可能保證工作目錄是干凈的。 如果你有正在做的工作,要么提交到一個臨時分支要么儲藏它。 這使你可以撤消在這里嘗試做的?任何事情?。 如果在你嘗試一次合并時工作目錄中有未保存的改動,下面的這些技巧可能會使你丟失那些工作。
讓我們通過一個非常簡單的例子來了解一下。 我們有一個超級簡單的打印?hello world?的 Ruby 文件。
#! /usr/bin/env ruby def hello puts 'hello world' end hello()
在我們的倉庫中,創(chuàng)建一個名為?whitespace?的新分支并將所有 Unix 換行符修改為 DOS 換行符, 實質(zhì)上雖然改變了文件的每一行,但改變的都只是空白字符。 然后我們修改行 “hello world” 為 “hello mundo”。
$ git checkout -b whitespace Switched to a new branch 'whitespace' $ unix2dos hello.rb unix2dos: converting file hello.rb to DOS format ... $ git commit -am 'converted hello.rb to DOS' [whitespace 3270f76] converted hello.rb to DOS 1 file changed, 7 insertions(+), 7 deletions(-) $ vim hello.rb $ git diff -b diff --git a/hello.rb b/hello.rb index ac51efd..e85207e 100755 --- a/hello.rb +++ b/hello.rb @@ -1,7 +1,7 @@ #! /usr/bin/env ruby def hello - puts 'hello world' + puts 'hello mundo'^M end hello() $ git commit -am 'hello mundo change' [whitespace 6d338d2] hello mundo change 1 file changed, 1 insertion(+), 1 deletion(-)
現(xiàn)在我們切換回我們的?master?分支并為函數(shù)增加一些注釋。
$ git checkout master Switched to branch 'master' $ vim hello.rb $ git diff diff --git a/hello.rb b/hello.rb index ac51efd..36c06c8 100755 --- a/hello.rb +++ b/hello.rb @@ -1,5 +1,6 @@ #! /usr/bin/env ruby +# prints out a greeting def hello puts 'hello world' end $ git commit -am 'document the function' [master bec6336] document the function 1 file changed, 1 insertion(+)
現(xiàn)在我們嘗試合并入我們的?whitespace?分支,因為修改了空白字符,所以合并會出現(xiàn)沖突。
$ git merge whitespace Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Automatic merge failed; fix conflicts and then commit the result.
中斷一次合并
我們現(xiàn)在有幾個選項。 首先,讓我們介紹如何擺脫這個情況。 你可能不想處理沖突這種情況,完全可以通過?git merge –abort?來簡單地退出合并。
$ git status -sb ## master UU hello.rb $ git merge --abort $ git status -sb ## master
git merge –abort?選項會嘗試恢復(fù)到你運行合并前的狀態(tài)。 但當(dāng)運行命令前,在工作目錄中有未儲藏、未提交的修改時它不能完美處理,除此之外它都工作地很好。
如果出于某些原因你想要重來一次,也可以運行?git reset –hard HEAD?回到上一次提交的狀態(tài)。 請牢記此時任何未提交的工作都會丟失,所以請確認(rèn)你不需要保留任何改動。
忽略空白
在這個特定的例子中,沖突與空白有關(guān)。 我們知道這點是因為這個例子很簡單,但是在實際的例子中發(fā)現(xiàn)這樣的沖突也很容易, 因為每一行都被移除而在另一邊每一行又被加回來了。 默認(rèn)情況下,Git 認(rèn)為所有這些行都改動了,所以它不會合并文件。
默認(rèn)合并策略可以帶有參數(shù),其中的幾個正好是關(guān)于忽略空白改動的。 如果你看到在一次合并中有大量關(guān)于空白的問題,你可以直接中止它并重做一次, 這次使用?-Xignore-all-space?或?-Xignore-space-change?選項。 第一個選項在比較行時?完全忽略?空白修改,第二個選項將一個空白符與多個連續(xù)的空白字符視作等價的。
$ git merge -Xignore-space-change whitespace Auto-merging hello.rb Merge made by the 'recursive' strategy. hello.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
因為在本例中,實際上文件修改并沒有沖突,一旦我們忽略空白修改,每一行都能被很好地合并。
如果你的團隊中的某個人可能不小心重新格式化空格為制表符或者相反的操作,這會是一個救命稻草。
手動文件再合并
雖然 Git 對空白的預(yù)處理做得很好,還有很多其他類型的修改,Git 也許無法自動處理,但是腳本可以處理它們。 例如,假設(shè) Git 無法處理空白修改因此我們需要手動處理。
我們真正想要做的是對將要合并入的文件在真正合并前運行?dos2unix?程序。 所以如果那樣的話,我們該如何做?
首先,我們進入到了合并沖突狀態(tài)。 然后我們想要我的版本的文件,他們的版本的文件(從我們將要合并入的分支)和共同的版本的文件(從分支叉開時的位置)的拷貝。 然后我們想要修復(fù)任何一邊的文件,并且為這個單獨的文件重試一次合并。
獲得這三個文件版本實際上相當(dāng)容易。 Git 在索引中存儲了所有這些版本,在 “stages” 下每一個都有一個數(shù)字與它們關(guān)聯(lián)。 Stage 1 是它們共同的祖先版本,stage 2 是你的版本,stage 3 來自于?MERGE_HEAD,即你將要合并入的版本(“theirs”)。
通過?git show?命令與一個特別的語法,你可以將沖突文件的這些版本釋放出一份拷貝。
$ git show :1:hello.rb > hello.common.rb $ git show :2:hello.rb > hello.ours.rb $ git show :3:hello.rb > hello.theirs.rb
如果你想要更專業(yè)一點,也可以使用?ls-files -u?底層命令來得到這些文件的 Git blob 對象的實際 SHA-1 值。
$ git ls-files -u 100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1 hello.rb 100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2 hello.rb 100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3 hello.rb
:1:hello.rb?只是查找那個 blob 對象 SHA-1 值的簡寫。
既然在我們的工作目錄中已經(jīng)有這所有三個階段的內(nèi)容,我們可以手工修復(fù)它們來修復(fù)空白問題,然后使用鮮為人知的?git merge-file?命令來重新合并那個文件。
$ dos2unix hello.theirs.rb dos2unix: converting file hello.theirs.rb to Unix format ... $ git merge-file -p hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb $ git diff -b diff --cc hello.rb index 36c06c8,e85207e..0000000 --- a/hello.rb +++ b/hello.rb @@@ -1,8 -1,7 +1,8 @@@ #! /usr/bin/env ruby +# prints out a greeting def hello - puts 'hello world' + puts 'hello mundo' end hello()
在這時我們已經(jīng)漂亮地合并了那個文件。 實際上,這比使用?ignore-space-change?選項要更好,因為在合并前真正地修復(fù)了空白修改而不是簡單地忽略它們。 在使用?ignore-space-change?進行合并操作后,我們最終得到了有幾行是 DOS 行尾的文件,從而使提交內(nèi)容混亂了。
如果你想要在最終提交前看一下我們這邊與另一邊之間實際的修改, 你可以使用?git diff?來比較將要提交作為合并結(jié)果的工作目錄與其中任意一個階段的文件差異。 讓我們看看它們。
要在合并前比較結(jié)果與在你的分支上的內(nèi)容,換一句話說,看看合并引入了什么,可以運行?git diff –ours
$ git diff --ours * Unmerged path hello.rb diff --git a/hello.rb b/hello.rb index 36c06c8..44d0a25 100755 --- a/hello.rb +++ b/hello.rb @@ -2,7 +2,7 @@ # prints out a greeting def hello - puts 'hello world' + puts 'hello mundo' end hello()
這里我們可以很容易地看到在我們的分支上發(fā)生了什么,在這次合并中我們實際引入到這個文件的改動,是修改了其中一行。
如果我們想要查看合并的結(jié)果與他們那邊有什么不同,可以運行?git diff –theirs。 在本例及后續(xù)的例子中,我們會使用?-b?來去除空白,因為我們將它與 Git 中的, 而不是我們清理過的?hello.theirs.rb?文件比較。
$ git diff --theirs -b * Unmerged path hello.rb diff --git a/hello.rb b/hello.rb index e85207e..44d0a25 100755 --- a/hello.rb +++ b/hello.rb @@ -1,5 +1,6 @@ #! /usr/bin/env ruby +# prints out a greeting def hello puts 'hello mundo' end
最終,你可以通過?git diff –base?來查看文件在兩邊是如何改動的。
$ git diff --base -b * Unmerged path hello.rb diff --git a/hello.rb b/hello.rb index ac51efd..44d0a25 100755 --- a/hello.rb +++ b/hello.rb @@ -1,7 +1,8 @@ #! /usr/bin/env ruby +# prints out a greeting def hello - puts 'hello world' + puts 'hello mundo' end hello()
在這時我們可以使用?git clean?命令來清理我們?yōu)槭謩雍喜⒍鴦?chuàng)建但不再有用的額外文件。
$ git clean -f Removing hello.common.rb Removing hello.ours.rb Removing hello.theirs.rb
檢出沖突
也許有時我們并不滿意這樣的解決方案,或許有時還要手動編輯一邊或者兩邊的沖突,但還是依舊無法正常工作,這時我們需要更多的上下文關(guān)聯(lián)來解決這些沖突。
讓我們來稍微改動下例子。 對于本例,我們有兩個長期分支,每一個分支都有幾個提交,但是在合并時卻創(chuàng)建了一個合理的沖突。
$ git log --graph --oneline --decorate --all * f1270f7 (HEAD, master) update README * 9af9d3b add a README * 694971d update phrase to hola world | * e3eb223 (mundo) add more tests | * 7cff591 add testing script | * c3ffff1 changed text to hello mundo |/ * b7dcc89 initial hello world code
現(xiàn)在有只在?master?分支上的三次單獨提交,還有其他三次提交在?mundo?分支上。 如果我們嘗試將?mundo分支合并入?master?分支,我們得到一個沖突。
$ git merge mundo Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Automatic merge failed; fix conflicts and then commit the result.
我們想要看一下合并沖突是什么。 如果我們打開這個文件,我們將會看到類似下面的內(nèi)容:
#! /usr/bin/env ruby def hello <<<<<<< HEAD puts 'hola world' ======= puts 'hello mundo' >>>>>>> mundo end hello()
合并的兩邊都向這個文件增加了內(nèi)容,但是導(dǎo)致沖突的原因是其中一些提交修改了文件的同一個地方。
讓我們探索一下現(xiàn)在你手邊可用來查明這個沖突是如何產(chǎn)生的工具。 應(yīng)該如何修復(fù)這個沖突看起來或許并不明顯。 這時你需要更多上下文。
一個很有用的工具是帶?–conflict?選項的?git checkout。 這會重新檢出文件并替換合并沖突標(biāo)記。 如果想要重置標(biāo)記并嘗試再次解決它們的話這會很有用。
可以傳遞給?–conflict?參數(shù)?diff3?或?merge(默認(rèn)選項)。 如果傳給它?diff3,Git 會使用一個略微不同版本的沖突標(biāo)記: 不僅僅只給你 “ours” 和 “theirs” 版本,同時也會有 “base” 版本在中間來給你更多的上下文。
$ git checkout --conflict=diff3 hello.rb
一旦我們運行它,文件看起來會像下面這樣:
#! /usr/bin/env ruby def hello <<<<<<< ours puts 'hola world' ||||||| base puts 'hello world' ======= puts 'hello mundo' >>>>>>> theirs end hello()
如果你喜歡這種格式,可以通過設(shè)置?merge.conflictstyle?選項為?diff3?來做為以后合并沖突的默認(rèn)選項。
$ git config --global merge.conflictstyle diff3
git checkout?命令也可以使用?–ours?和?–theirs?選項,這是一種無需合并的快速方式,你可以選擇留下一邊的修改而丟棄掉另一邊修改。
當(dāng)有二進制文件沖突時這可能會特別有用,因為可以簡單地選擇一邊,或者可以只合并另一個分支的特定文件——可以做一次合并然后在提交前檢出一邊或另一邊的特定文件。
合并日志
另一個解決合并沖突有用的工具是?git log。 這可以幫助你得到那些對沖突有影響的上下文。 回顧一點歷史來記起為什么兩條線上的開發(fā)會觸碰同一片代碼有時會很有用。
為了得到此次合并中包含的每一個分支的所有獨立提交的列表, 我們可以使用之前在?三點?學(xué)習(xí)的“三點”語法。
$ git log --oneline --left-right HEAD...MERGE_HEAD < f1270f7 update README < 9af9d3b add a README < 694971d update phrase to hola world > e3eb223 add more tests > 7cff591 add testing script > c3ffff1 changed text to hello mundo
這個漂亮的列表包含 6 個提交和每一個提交所在的不同開發(fā)路徑。
我們可以通過更加特定的上下文來進一步簡化這個列表。 如果我們添加?–merge?選項到?git log?中,它會只顯示任何一邊接觸了合并沖突文件的提交。
$ git log --oneline --left-right --merge < 694971d update phrase to hola world > c3ffff1 changed text to hello mundo
如果你運行命令時用?-p?選項代替,你會得到所有沖突文件的區(qū)別。 快速獲得你需要幫助理解為什么發(fā)生沖突的上下文,以及如何聰明地解決它,這會?非常?有用。
組合式差異格式
因為 Git 暫存合并成功的結(jié)果,當(dāng)你在合并沖突狀態(tài)下運行?git diff?時,只會得到現(xiàn)在還在沖突狀態(tài)的區(qū)別。 當(dāng)需要查看你還需要解決哪些沖突時這很有用。
在合并沖突后直接運行的?git diff?會給你一個相當(dāng)獨特的輸出格式。
$ git diff diff --cc hello.rb index 0399cd5,59727f0..0000000 --- a/hello.rb +++ b/hello.rb @@@ -1,7 -1,7 +1,11 @@@ #! /usr/bin/env ruby def hello ++<<<<<<< HEAD + puts 'hola world' ++======= + puts 'hello mundo' ++>>>>>>> mundo end hello()
這種叫作“組合式差異”的格式會在每一行給你兩列數(shù)據(jù)。 第一列為你顯示 “ours” 分支與工作目錄的文件區(qū)別(添加或刪除), 第二列顯示 “theirs” 分支與工作目錄的拷貝區(qū)別。
所以在上面的例子中可以看到?>>>>>>?行在工作拷貝中但是并不在合并的任意一邊中。 這很有意義,合并工具因為我們的上下文被困住了,它期望我們?nèi)ヒ瞥鼈儭?/p>
如果我們解決沖突再次運行?git diff,我們將會看到同樣的事情,但是它有一點幫助。
$ vim hello.rb $ git diff diff --cc hello.rb index 0399cd5,59727f0..0000000 --- a/hello.rb +++ b/hello.rb @@@ -1,7 -1,7 +1,7 @@@ #! /usr/bin/env ruby def hello - puts 'hola world' - puts 'hello mundo' ++ puts 'hola mundo' end hello()
這里顯示出 “hola world” 在我們這邊但不在工作拷貝中,那個 “hello mundo” 在他們那邊但不在工作拷貝中, 最終 “hola mundo” 不在任何一邊但是現(xiàn)在在工作拷貝中。在提交解決方案前這對審核很有用。
也可以在合并后通過?git log?來獲取相同信息,查看沖突是如何解決的。 如果你對一個合并提交運行?git show?命令 Git 將會輸出這種格式, 或者你也可以在?git log -p(默認(rèn)情況下該命令只會展示還沒有合并的補丁)命令之后加上?–cc?選項。
$ git log --cc -p -1 commit 14f41939956d80b9e17bb8721354c33f8d5b5a79 Merge: f1270f7 e3eb223 Author: Scott Chacon <schacon@gmail.com> Date: Fri Sep 19 18:14:49 2014 +0200 Merge branch 'mundo' Conflicts: hello.rb diff --cc hello.rb index 0399cd5,59727f0..e1d0799 --- a/hello.rb +++ b/hello.rb @@@ -1,7 -1,7 +1,7 @@@ #! /usr/bin/env ruby def hello - puts 'hola world' - puts 'hello mundo' ++ puts 'hola mundo' end hello()
撤消合并
雖然你已經(jīng)知道如何創(chuàng)建一個合并提交,但有時出錯是在所難免的。 使用 Git 最棒的一件事情是犯錯是可以的,因為有可能(大多數(shù)情況下都很容易)修復(fù)它們。
合并提交并無不同。 假設(shè)現(xiàn)在在一個主題分支上工作,不小心將其合并到?master?中,現(xiàn)在提交歷史看起來是這樣:
Figure 138. 意外的合并提交
有兩種方法來解決這個問題,這取決于你想要的結(jié)果是什么。
修復(fù)引用
如果這個不想要的合并提交只存在于你的本地倉庫中,最簡單且最好的解決方案是移動分支到你想要它指向的地方。 大多數(shù)情況下,如果你在錯誤的?git merge?后運行?git reset –hard HEAD~,這會重置分支指向所以它們看起來像這樣:
Figure 139. 在?git reset –hard HEAD~?之后的歷史
我們之前在?重置揭密?已經(jīng)介紹了?reset,所以現(xiàn)在指出這里發(fā)生了什么并不是很困難。 讓我們快速復(fù)習(xí)下:reset –hard?通常會經(jīng)歷三步:
-
移動 HEAD 指向的分支。 在本例中,我們想要移動?master?到合并提交(C6)之前所在的位置。
-
使索引看起來像 HEAD。
-
使工作目錄看起來像索引。
這個方法的缺點是它會重寫歷史,在一個共享的倉庫中這會造成問題的。 查閱?變基的風(fēng)險?來了解更多可能發(fā)生的事情; 用簡單的話說就是如果其他人已經(jīng)有你將要重寫的提交,你應(yīng)當(dāng)避免使用?reset。 如果有任何其他提交在合并之后創(chuàng)建了,那么這個方法也會無效;移動引用實際上會丟失那些改動。
還原提交
如果移動分支指針并不適合你,Git 給你一個生成一個新提交的選項,提交將會撤消一個已存在提交的所有修改。 Git 稱這個操作為“還原”,在這個特定的場景下,你可以像這樣調(diào)用它:
$ git revert -m 1 HEAD [master b1d8379] Revert "Merge branch 'topic'"
-m 1?標(biāo)記指出 “mainline” 需要被保留下來的父結(jié)點。 當(dāng)你引入一個合并到?HEAD(git merge topic),新提交有兩個父結(jié)點:第一個是?HEAD(C6),第二個是將要合并入分支的最新提交(C4)。 在本例中,我們想要撤消所有由父結(jié)點 #2(C4)合并引入的修改,同時保留從父結(jié)點 #1(C6)開始的所有內(nèi)容。
有還原提交的歷史看起來像這樣:
Figure 140. 在?git revert -m 1?后的歷史
新的提交?^M?與?C6?有完全一樣的內(nèi)容,所以從這兒開始就像合并從未發(fā)生過,除了“現(xiàn)在還沒合并”的提交依然在?HEAD?的歷史中。 如果你嘗試再次合并?topic?到?master?Git 會感到困惑:
$ git merge topic Already up-to-date.
topic?中并沒有東西不能從?master?中追蹤到達(dá)。 更糟的是,如果你在?topic?中增加工作然后再次合并,Git 只會引入被還原的合并?之后?的修改。
Figure 141. 含有壞掉合并的歷史
解決這個最好的方式是撤消還原原始的合并,因為現(xiàn)在你想要引入被還原出去的修改,然后?創(chuàng)建一個新的合并提交:
$ git revert ^M [master 09f0126] Revert "Revert "Merge branch 'topic'"" $ git merge topic
Figure 142. 在重新合并一個還原合并后的歷史
在本例中,M?與?^M?抵消了。?^^M?事實上合并入了?C3?與?C4?的修改,C8?合并了?C7?的修改,所以現(xiàn)在?topic?已經(jīng)完全被合并了。
其他類型的合并
到目前為止我們介紹的都是通過一個叫作 “recursive” 的合并策略來正常處理的兩個分支的正常合并。 然而還有其他方式來合并兩個分支到一起。 讓我們來快速介紹其中的幾個。
我們的或他們的偏好
首先,有另一種我們可以通過 “recursive” 合并模式做的有用工作。 我們之前已經(jīng)看到傳遞給?-X?的?ignore-all-space?與?ignore-space-change?選項, 但是我們也可以告訴 Git 當(dāng)它看見一個沖突時直接選擇一邊。
默認(rèn)情況下,當(dāng) Git 看到兩個分支合并中的沖突時,它會將合并沖突標(biāo)記添加到你的代碼中并標(biāo)記文件為沖突狀態(tài)來讓你解決。 如果你希望 Git 簡單地選擇特定的一邊并忽略另外一邊而不是讓你手動解決沖突,你可以傳遞給?merge?命令一個?-Xours?或?-Xtheirs?參數(shù)。
如果 Git 看到這個,它并不會增加沖突標(biāo)記。 任何可以合并的區(qū)別,它會直接合并。 任何有沖突的區(qū)別,它會簡單地選擇你全局指定的一邊,包括二進制文件。
如果我們回到之前我們使用的 “hello world” 例子中,我們可以看到合并入我們的分支時引發(fā)了沖突。
$ git merge mundo Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Resolved 'hello.rb' using previous resolution. Automatic merge failed; fix conflicts and then commit the result.
然而如果我們運行時增加?-Xours?或?-Xtheirs?參數(shù)就不會有沖突。
$ git merge -Xours mundo Auto-merging hello.rb Merge made by the 'recursive' strategy. hello.rb | 2 +- test.sh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 test.sh
在上例中,它并不會為 “hello mundo” 與 “hola world” 標(biāo)記合并沖突,它只會簡單地選取 “hola world”。 然而,在那個分支上所有其他非沖突的改動都可以被成功地合并入。
這個選項也可以傳遞給我們之前看到的?git merge-file?命令, 通過運行類似?git merge-file –ours?的命令來合并單個文件。
如果想要做類似的事情但是甚至并不想讓 Git 嘗試合并另外一邊的修改, 有一個更嚴(yán)格的選項,它是 “ours” 合并?策略。 這與 “ours” recursive 合并?選項?不同。
這本質(zhì)上會做一次假的合并。 它會記錄一個以兩邊分支作為父結(jié)點的新合并提交,但是它甚至根本不關(guān)注你正合并入的分支。 它只會簡單地把當(dāng)前分支的代碼當(dāng)作合并結(jié)果記錄下來。
$ git merge -s ours mundo Merge made by the 'ours' strategy. $ git diff HEAD HEAD~ $
你可以看到合并后與合并前我們的分支并沒有任何區(qū)別。
當(dāng)再次合并時從本質(zhì)上欺騙 Git 認(rèn)為那個分支已經(jīng)合并過經(jīng)常是很有用的。 例如,假設(shè)你有一個分叉的?release?分支并且在上面做了一些你想要在未來某個時候合并回?master?的工作。 與此同時?master?分支上的某些 bugfix 需要向后移植回?release?分支。 你可以合并 bugfix 分支進入?release?分支同時也?merge -s ours?合并進入你的?master?分支 (即使那個修復(fù)已經(jīng)在那兒了)這樣當(dāng)你之后再次合并?release?分支時,就不會有來自 bugfix 的沖突。
子樹合并
子樹合并的思想是你有兩個項目,并且其中一個映射到另一個項目的一個子目錄,或者反過來也行。 當(dāng)你執(zhí)行一個子樹合并時,Git 通常可以自動計算出其中一個是另外一個的子樹從而實現(xiàn)正確的合并。
我們來看一個例子如何將一個項目加入到一個已存在的項目中,然后將第二個項目的代碼合并到第一個項目的子目錄中。
首先,我們將 Rack 應(yīng)用添加到你的項目里。 我們把 Rack 項目作為一個遠(yuǎn)程的引用添加到我們的項目里,然后檢出到它自己的分支。
$ git remote add rack_remote https://github.com/rack/rack $ git fetch rack_remote --no-tags warning: no common commits remote: Counting objects: 3184, done. remote: Compressing objects: 100% (1465/1465), done. remote: Total 3184 (delta 1952), reused 2770 (delta 1675) Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done. Resolving deltas: 100% (1952/1952), done. From https://github.com/rack/rack * [new branch] build -> rack_remote/build * [new branch] master -> rack_remote/master * [new branch] rack-0.4 -> rack_remote/rack-0.4 * [new branch] rack-0.9 -> rack_remote/rack-0.9 $ git checkout -b rack_branch rack_remote/master Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master. Switched to a new branch "rack_branch"
現(xiàn)在在我們的?rack_branch?分支里就有 Rack 項目的根目錄,而我們的項目則在?master?分支里。 如果你從一個分支切換到另一個分支,你可以看到它們的項目根目錄是不同的:
$ ls AUTHORS KNOWN-ISSUES Rakefile contrib lib COPYING README bin example test $ git checkout master Switched to branch "master" $ ls README
這個是一個比較奇怪的概念。 并不是倉庫中的所有分支都是必須屬于同一個項目的分支. 這并不常見,因為沒啥用,但是卻是在不同分支里包含兩條完全不同提交歷史的最簡單的方法。
在這個例子中,我們希望將 Rack 項目拉到?master?項目中作為一個子目錄。 我們可以在 Git 中執(zhí)行?git read-tree?來實現(xiàn)。 你可以在?Git 內(nèi)部原理?中查看更多?read-tree?的相關(guān)信息,現(xiàn)在你只需要知道它會讀取一個分支的根目錄樹到當(dāng)前的暫存區(qū)和工作目錄里。 先切回你的?master?分支,將?rack_back?分支拉取到我們項目的?master?分支中的?rack?子目錄。
$ git read-tree --prefix=rack/ -u rack_branch
當(dāng)我們提交時,那個子目錄中擁有所有 Rack 項目的文件 —— 就像我們直接從壓縮包里復(fù)制出來的一樣。 有趣的是你可以很容易地將一個分支的變更合并到另一個分支里。 所以,當(dāng) Rack 項目有更新時,我們可以切換到那個分支來拉取上游的變更。
$ git checkout rack_branch $ git pull
接著,我們可以將這些變更合并回我們的?master?分支。 使用?–squash?選項和使用?-Xsubtree?選項(它采用遞歸合并策略), 都可以用來可以拉取變更并且預(yù)填充提交信息。 (遞歸策略在這里是默認(rèn)的,提到它是為了讓讀者有個清晰的概念。)
$ git checkout master $ git merge --squash -s recursive -Xsubtree=rack rack_branch Squash commit -- not updating HEAD Automatic merge went well; stopped before committing as requested
Rack 項目中所有的改動都被合并了,等待被提交到本地。 你也可以用相反的方法——在?master?分支上的?rack?子目錄中做改動然后將它們合并入你的?rack_branch?分支中,之后你可能將其提交給項目維護著或者將它們推送到上游。
這給我們提供了一種類似子模塊工作流的工作方式,但是它并不需要用到子模塊 (有關(guān)子模塊的內(nèi)容我們會在?子模塊?中介紹)。 我們可以在自己的倉庫中保持一些和其他項目相關(guān)的分支,偶爾使用子樹合并將它們合并到我們的項目中。 某些時候這種方式很有用,例如當(dāng)所有的代碼都提交到一個地方的時候。 然而,它同時也有缺點,它更加復(fù)雜且更容易讓人犯錯,例如重復(fù)合并改動或者不小心將分支提交到一個無關(guān)的倉庫上去。
另外一個有點奇怪的地方是,當(dāng)你想查看?rack?子目錄和?rack_branch?分支的差異—— 來確定你是否需要合并它們——你不能使用普通的?diff?命令。 取而代之的是,你必須使用?git diff-tree?來和你的目標(biāo)分支做比較:
$ git diff-tree -p rack_branch
或者,將你的?rack?子目和最近一次從服務(wù)器上抓取的?master?分支進行比較,你可以運行:
$ git diff-tree -p rack_remote/master
推薦學(xué)習(xí):《git》