7.8 高級合并

2018-02-24 15:22 更新

高級合并

在 Git 中合并是相當(dāng)容易的。 因為 Git 使多次合并另一個分支變得很容易,這意味著你可以有一個始終保持最新的長期分支,經(jīng)常解決小的沖突,比在一系列提交后解決一個巨大的沖突要好。

然而,有時也會有棘手的沖突。 不像其他的版本控制系統(tǒng),Git 并不會嘗試過于聰明的合并沖突解決方案。 Git 的哲學(xué)是聰明地決定無歧義的合并方案,但是如果有沖突,它不會嘗試智能地自動解決它。 因此,如果很久之后才合并兩個分叉的分支,你可能會撞上一些問題。

在本節(jié)中,我們將會仔細查看那些問題是什么以及 Git 給了我們什么工具來幫助我們處理這些更難辦的情形。我們也會了解你可以做的不同的、非標準類型的合并,也會看到如何后退到合并之前。

合并沖突

我們在?遇到?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)運行命令前,在工作目錄中有未儲藏、未提交的修改時它不能完美處理,除此之外它都工作地很好。

如果因為某些原因你發(fā)現(xiàn)自己處在一個混亂的狀態(tài)中然后只是想要重來一次,也可以運行?git reset --hard HEAD?回到之前的狀態(tài)或其他你想要恢復(fù)的狀態(tài)。 請牢記這會將清除工作目錄中的所有內(nèi)容,所以確保你不需要保存這里的任意改動。

忽略空白

在這個特定的例子中,沖突與空白有關(guān)。 我們知道這點是因為這個例子很簡單,但是在實際的例子中發(fā)現(xiàn)這樣的沖突也很容易,因為每一行都被移除而在另一邊每一行又被加回來了。 默認情況下,Git 認為所有這些行都改動了,所以它不會合并文件。

默認合并策略可以帶有參數(shù),其中的幾個正好是關(guān)于忽略空白改動的。 如果你看到在一次合并中有大量的空白問題,你可以簡單地中止它并重做一次,這次使用?-Xignore-all-space?或?-Xignore-space-change?選項。 第一個選項忽略任意?數(shù)量?的已有空白的修改,第二個選項忽略所有空白修改。

$ 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。 這會重新檢出文件并替換合并沖突標記。 如果想要重置標記并嘗試再次解決它們的話這會很有用。

可以傳遞給?--conflict?參數(shù)?diff3?或?merge(默認選項)。 如果傳給它?diff3,Git 會使用一個略微不同版本的沖突標記:不僅僅只給你 “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?來做為以后合并沖突的默認選項。

$ 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(默認情況下該命令只會展示還沒有合并的補丁)命令之后加上?--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 7-21.?在?git reset --hard HEAD~?之后的歷史

我們之前在?重置揭密?已經(jīng)介紹了?reset,所以現(xiàn)在指出這里發(fā)生了什么并不是很困難。 讓我們快速復(fù)習(xí)下:reset --hard?通常會經(jīng)歷三步:

  1. 移動 HEAD 指向的分支。 在本例中,我們想要移動?master?到合并提交(C6)之前所在的位置。

  2. 使索引看起來像 HEAD。

  3. 使工作目錄看起來像索引。

這個方法的缺點是它會重寫歷史,在一個共享的倉庫中這會造成問題的。 查閱?變基的風(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?標記指出 “mainline” 需要被保留下來的父結(jié)點。 當(dāng)你引入一個合并到?HEADgit merge topic),新提交有兩個父結(jié)點:第一個是?HEADC6),第二個是將要合并入分支的最新提交(C4)。 在本例中,我們想要撤消所有由父結(jié)點 #2(C4)合并引入的修改,同時保留從父結(jié)點 #1(C4)開始的所有內(nèi)容。

有還原提交的歷史看起來像這樣:

Figure 7-23.?含有壞掉合并的歷史

解決這個最好的方式是撤消還原原始的合并,因為現(xiàn)在你想要引入被還原出去的修改,然后?創(chuàng)建一個新的合并提交:

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號