範例講解!Git工具之高階合併

2022-03-01 19:00:23
本篇文章給大家帶來啦的相關知識,其中主要介紹了高階合併,包括了合併衝突、復原合併等相關問題,希望對大家有幫助。

推薦學習:《》

高階合併

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

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

在本節中,我們將會仔細檢視那些問題是什麼以及 Git 給了我們什麼工具來幫助我們處理這些更難辦的情形。 我們也會了解你可以做的不同的、非標準型別的合併,也會看到如何後退到合併之前。

合併衝突

我們在 遇到衝突時的分支合併 介紹瞭解決合併衝突的一些基礎知識, 對於更復雜的衝突,Git 提供了幾個工具來幫助你指出將會發生什麼以及如何更好地處理衝突。

首先,在做一次可能有衝突的合併前儘可能保證工作目錄是乾淨的。 如果你有正在做的工作,要麼提交到一個臨時分支要麼儲藏它。 這使你可以撤消在這裡嘗試做的 任何事情 。 如果在你嘗試一次合併時工作目錄中有未儲存的改動,下面的這些技巧可能會使你丟失那些工作。

讓我們通過一個非常簡單的例子來了解一下。 我們有一個超級簡單的列印 hello world 的 Ruby 檔案。

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

在我們的倉庫中,建立一個名為 whitespace 的新分支並將所有 Unix 換行符修改為 DOS 換行符, 實質上雖然改變了檔案的每一行,但改變的都只是空白字元。 然後我們修改行 「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(-)

現在我們切換回我們的 master 分支併為函數增加一些註釋。

$ 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(+)

現在我們嘗試合併入我們的 whitespace 分支,因為修改了空白字元,所以合併會出現衝突。

$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

中斷一次合併

我們現在有幾個選項。 首先,讓我們介紹如何擺脫這個情況。 你可能不想處理衝突這種情況,完全可以通過 git merge --abort 來簡單地退出合併。

$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master

git merge --abort 選項會嘗試恢復到你執行合併前的狀態。 但當執行命令前,在工作目錄中有未儲藏、未提交的修改時它不能完美處理,除此之外它都工作地很好。

如果出於某些原因你想要重來一次,也可以執行 git reset --hard HEAD 回到上一次提交的狀態。 請牢記此時任何未提交的工作都會丟失,所以請確認你不需要保留任何改動。

忽略空白

在這個特定的例子中,衝突與空白有關。 我們知道這點是因為這個例子很簡單,但是在實際的例子中發現這樣的衝突也很容易, 因為每一行都被移除而在另一邊每一行又被加回來了。 預設情況下,Git 認為所有這些行都改動了,所以它不會合並檔案。

預設合併策略可以帶有引數,其中的幾個正好是關於忽略空白改動的。 如果你看到在一次合併中有大量關於空白的問題,你可以直接中止它並重做一次, 這次使用 -Xignore-all-space-Xignore-space-change 選項。 第一個選項在比較行時 完全忽略 空白修改,第二個選項將一個空白符與多個連續的空白字元視作等價的。

$ 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 對空白的預處理做得很好,還有很多其他型別的修改,Git 也許無法自動處理,但是指令碼可以處理它們。 例如,假設 Git 無法處理空白修改因此我們需要手動處理。

我們真正想要做的是對將要合併入的檔案在真正合並前執行 dos2unix 程式。 所以如果那樣的話,我們該如何做?

首先,我們進入到了合併衝突狀態。 然後我們想要我的版本的檔案,他們的版本的檔案(從我們將要合併入的分支)和共同的版本的檔案(從分支叉開時的位置)的拷貝。 然後我們想要修復任何一邊的檔案,並且為這個單獨的檔案重試一次合併。

獲得這三個檔案版本實際上相當容易。 Git 在索引中儲存了所有這些版本,在 「stages」 下每一個都有一個數位與它們關聯。 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

如果你想要更專業一點,也可以使用 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 值的簡寫。

既然在我們的工作目錄中已經有這所有三個階段的內容,我們可以手工修復它們來修復空白問題,然後使用鮮為人知的 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()

在這時我們已經漂亮地合併了那個檔案。 實際上,這比使用 ignore-space-change 選項要更好,因為在合併前真正地修復了空白修改而不是簡單地忽略它們。 在使用 ignore-space-change 進行合併操作後,我們最終得到了有幾行是 DOS 行尾的檔案,從而使提交內容混亂了。

如果你想要在最終提交前看一下我們這邊與另一邊之間實際的修改, 你可以使用 git diff 來比較將要提交作為合併結果的工作目錄與其中任意一個階段的檔案差異。 讓我們看看它們。

要在合併前比較結果與在你的分支上的內容,換一句話說,看看合併引入了什麼,可以執行 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()

這裡我們可以很容易地看到在我們的分支上發生了什麼,在這次合併中我們實際引入到這個檔案的改動,是修改了其中一行。

如果我們想要檢視合併的結果與他們那邊有什麼不同,可以執行 git diff --theirs。 在本例及後續的例子中,我們會使用 -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 命令來清理我們為手動合併而建立但不再有用的額外檔案。

$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb

檢出衝突

也許有時我們並不滿意這樣的解決方案,或許有時還要手動編輯一邊或者兩邊的衝突,但還是依舊無法正常工作,這時我們需要更多的上下文關聯來解決這些衝突。

讓我們來稍微改動下例子。 對於本例,我們有兩個長期分支,每一個分支都有幾個提交,但是在合併時卻建立了一個合理的衝突。

$ 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

現在有隻在 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.

我們想要看一下合併衝突是什麼。 如果我們開啟這個檔案,我們將會看到類似下面的內容:

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

合併的兩邊都向這個檔案增加了內容,但是導致衝突的原因是其中一些提交修改了檔案的同一個地方。

讓我們探索一下現在你手邊可用來查明這個衝突是如何產生的工具。 應該如何修復這個衝突看起來或許並不明顯。 這時你需要更多上下文。

一個很有用的工具是帶 --conflict 選項的 git checkout。 這會重新檢出檔案並替換合併衝突標記。 如果想要重置標記並嘗試再次解決它們的話這會很有用。

可以傳遞給 --conflict 引數 diff3merge(預設選項)。 如果傳給它 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()

如果你喜歡這種格式,可以通過設定 merge.conflictstyle 選項為 diff3 來做為以後合併衝突的預設選項。

$ git config --global merge.conflictstyle diff3

git checkout 命令也可以使用 --ours--theirs 選項,這是一種無需合併的快速方式,你可以選擇留下一邊的修改而丟棄掉另一邊修改。

當有二進位制檔案衝突時這可能會特別有用,因為可以簡單地選擇一邊,或者可以只合並另一個分支的特定檔案——可以做一次合併然後在提交前檢出一邊或另一邊的特定檔案。

合併紀錄檔

另一個解決合併衝突有用的工具是 git log。 這可以幫助你得到那些對衝突有影響的上下文。 回顧一點歷史來記起為什麼兩條線上的開發會觸碰同一片程式碼有時會很有用。

為了得到此次合併中包含的每一個分支的所有獨立提交的列表, 我們可以使用之前在 三點 學習的「三點」語法。

$ 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 個提交和每一個提交所在的不同開發路徑。

我們可以通過更加特定的上下文來進一步簡化這個列表。 如果我們新增 --merge 選項到 git log 中,它會只顯示任何一邊接觸了合併衝突檔案的提交。

$ git log --oneline --left-right --merge
< 694971d update phrase to hola world
> c3ffff1 changed text to hello mundo

如果你執行命令時用 -p 選項代替,你會得到所有衝突檔案的區別。 快速獲得你需要幫助理解為什麼發生衝突的上下文,以及如何聰明地解決它,這會 非常 有用。

組合式差異格式

因為 Git 暫存合併成功的結果,當你在合併衝突狀態下執行 git diff 時,只會得到現在還在衝突狀態的區別。 當需要檢視你還需要解決哪些衝突時這很有用。

在合併衝突後直接執行的 git diff 會給你一個相當獨特的輸出格式。

$ 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()

這種叫作「組合式差異」的格式會在每一行給你兩列資料。 第一列為你顯示 「ours」 分支與工作目錄的檔案區別(新增或刪除), 第二列顯示 「theirs」 分支與工作目錄的拷貝區別。

所以在上面的例子中可以看到 <<<<<<<>>>>>>> 行在工作拷貝中但是並不在合併的任意一邊中。 這很有意義,合併工具因為我們的上下文被困住了,它期望我們去移除它們。

如果我們解決衝突再次執行 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」 不在任何一邊但是現在在工作拷貝中。在提交解決方案前這對稽核很有用。

也可以在合併後通過 git log 來獲取相同資訊,檢視衝突是如何解決的。 如果你對一個合併提交執行 git show 命令 Git 將會輸出這種格式, 或者你也可以在 git log -p(預設情況下該命令只會展示還沒有合併的修補程式)命令之後加上 --cc 選項。

$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <[email protected]>
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()

撤消合併

雖然你已經知道如何建立一個合併提交,但有時出錯是在所難免的。 使用 Git 最棒的一件事情是犯錯是可以的,因為有可能(大多數情況下都很容易)修復它們。

合併提交併無不同。 假設現在在一個主題分支上工作,不小心將其合併到 master 中,現在提交歷史看起來是這樣:

Figure 138. 意外的合併提交

有兩種方法來解決這個問題,這取決於你想要的結果是什麼。

修復參照

如果這個不想要的合併提交只存在於你的本地倉庫中,最簡單且最好的解決方案是移動分支到你想要它指向的地方。 大多數情況下,如果你在錯誤的 git merge 後執行 git reset --hard HEAD~,這會重置分支指向所以它們看起來像這樣:

Figure 139. 在 git reset --hard HEAD~ 之後的歷史

我們之前在 重置揭密 已經介紹了 reset,所以現在指出這裡發生了什麼並不是很困難。 讓我們快速複習下:reset --hard 通常會經歷三步:

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

  2. 使索引看起來像 HEAD。

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

這個方法的缺點是它會重寫歷史,在一個共用的倉庫中這會造成問題的。 查閱 變基的風險 來了解更多可能發生的事情; 用簡單的話說就是如果其他人已經有你將要重寫的提交,你應當避免使用 reset。 如果有任何其他提交在合併之後建立了,那麼這個方法也會無效;移動參照實際上會丟失那些改動。

還原提交

如果移動分支指標並不適合你,Git 給你一個生成一個新提交的選項,提交將會撤消一個已存在提交的所有修改。 Git 稱這個操作為「還原」,在這個特定的場景下,你可以像這樣呼叫它:

$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"

-m 1 標記指出 「mainline」 需要被保留下來的父結點。 當你引入一個合併到 HEADgit merge topic),新提交有兩個父結點:第一個是 HEADC6),第二個是將要合併入分支的最新提交(C4)。 在本例中,我們想要撤消所有由父結點 #2(C4)合併引入的修改,同時保留從父結點 #1(C6)開始的所有內容。

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

Figure 140. 在 git revert -m 1 後的歷史

新的提交 ^MC6 有完全一樣的內容,所以從這兒開始就像合併從未發生過,除了「現在還沒合併」的提交依然在 HEAD 的歷史中。 如果你嘗試再次合併 topicmaster Git 會感到困惑:

$ git merge topic
Already up-to-date.

topic 中並沒有東西不能從 master 中追蹤到達。 更糟的是,如果你在 topic 中增加工作然後再次合併,Git 只會引入被還原的合併 之後 的修改。

Figure 141. 含有壞掉合併的歷史

解決這個最好的方式是撤消還原原始的合併,因為現在你想要引入被還原出去的修改,然後 建立一個新的合併提交:

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

Figure 142. 在重新合併一個還原合併後的歷史

在本例中,M^M 抵消了。 ^^M 事實上合併入了 C3C4 的修改,C8 合併了 C7 的修改,所以現在 topic 已經完全被合併了。

其他型別的合併

到目前為止我們介紹的都是通過一個叫作 「recursive」 的合併策略來正常處理的兩個分支的正常合併。 然而還有其他方式來合併兩個分支到一起。 讓我們來快速介紹其中的幾個。

我們的或他們的偏好

首先,有另一種我們可以通過 「recursive」 合併模式做的有用工作。 我們之前已經看到傳遞給 -Xignore-all-spaceignore-space-change 選項, 但是我們也可以告訴 Git 當它看見一個衝突時直接選擇一邊。

預設情況下,當 Git 看到兩個分支合併中的衝突時,它會將合併衝突標記新增到你的程式碼中並標記檔案為衝突狀態來讓你解決。 如果你希望 Git 簡單地選擇特定的一邊並忽略另外一邊而不是讓你手動解決衝突,你可以傳遞給 merge 命令一個 -Xours-Xtheirs 引數。

如果 Git 看到這個,它並不會增加衝突標記。 任何可以合併的區別,它會直接合並。 任何有衝突的區別,它會簡單地選擇你全域性指定的一邊,包括二進位制檔案。

如果我們回到之前我們使用的 「hello world」 例子中,我們可以看到合併入我們的分支時引發了衝突。

$ 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 引數就不會有衝突。

$ 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」 標記合併衝突,它只會簡單地選取 「hola world」。 然而,在那個分支上所有其他非衝突的改動都可以被成功地合併入。

這個選項也可以傳遞給我們之前看到的 git merge-file 命令, 通過執行類似 git merge-file --ours 的命令來合併單個檔案。

如果想要做類似的事情但是甚至並不想讓 Git 嘗試合併另外一邊的修改, 有一個更嚴格的選項,它是 「ours」 合併 策略。 這與 「ours」 recursive 合併 選項 不同。

這本質上會做一次假的合併。 它會記錄一個以兩邊分支作為父結點的新合併提交,但是它甚至根本不關注你正合併入的分支。 它只會簡單地把當前分支的程式碼當作合併結果記錄下來。

$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$

你可以看到合併後與合併前我們的分支並沒有任何區別。

當再次合併時從本質上欺騙 Git 認為那個分支已經合併過經常是很有用的。 例如,假設你有一個分叉的 release 分支並且在上面做了一些你想要在未來某個時候合併回 master 的工作。 與此同時 master 分支上的某些 bugfix 需要向後移植回 release 分支。 你可以合併 bugfix 分支進入 release 分支同時也 merge -s ours 合併進入你的 master 分支 (即使那個修復已經在那兒了)這樣當你之後再次合併 release 分支時,就不會有來自 bugfix 的衝突。

子樹合併

子樹合併的思想是你有兩個專案,並且其中一個對映到另一個專案的一個子目錄,或者反過來也行。 當你執行一個子樹合併時,Git 通常可以自動計算出其中一個是另外一個的子樹從而實現正確的合併。

我們來看一個例子如何將一個專案加入到一個已存在的專案中,然後將第二個專案的程式碼合併到第一個專案的子目錄中。

首先,我們將 Rack 應用新增到你的專案裡。 我們把 Rack 專案作為一個遠端的參照新增到我們的專案裡,然後檢出到它自己的分支。

$ 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"

現在在我們的 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 中執行 git read-tree 來實現。 你可以在 Git 內部原理 中檢視更多 read-tree 的相關資訊,現在你只需要知道它會讀取一個分支的根目錄樹到當前的暫存區和工作目錄裡。 先切回你的 master 分支,將 rack_back 分支拉取到我們專案的 master 分支中的 rack 子目錄。

$ git read-tree --prefix=rack/ -u rack_branch

當我們提交時,那個子目錄中擁有所有 Rack 專案的檔案 —— 就像我們直接從壓縮包裡複製出來的一樣。 有趣的是你可以很容易地將一個分支的變更合併到另一個分支裡。 所以,當 Rack 專案有更新時,我們可以切換到那個分支來拉取上游的變更。

$ git checkout rack_branch
$ git pull

接著,我們可以將這些變更合併回我們的 master 分支。 使用 --squash 選項和使用 -Xsubtree 選項(它採用遞迴合併策略), 都可以用來可以拉取變更並且預填充提交資訊。 (遞迴策略在這裡是預設的,提到它是為了讓讀者有個清晰的概念。)

$ 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 分支中,之後你可能將其提交給專案維護著或者將它們推播到上游。

這給我們提供了一種類似子模組工作流的工作方式,但是它並不需要用到子模組 (有關子模組的內容我們會在 子模組 中介紹)。 我們可以在自己的倉庫中保持一些和其他專案相關的分支,偶爾使用子樹合併將它們合併到我們的專案中。 某些時候這種方式很有用,例如當所有的程式碼都提交到一個地方的時候。 然而,它同時也有缺點,它更加複雜且更容易讓人犯錯,例如重複合併改動或者不小心將分支提交到一個無關的倉庫上去。

另外一個有點奇怪的地方是,當你想檢視 rack 子目錄和 rack_branch 分支的差異—— 來確定你是否需要合併它們——你不能使用普通的 diff 命令。 取而代之的是,你必須使用 git diff-tree 來和你的目標分支做比較:

$ git diff-tree -p rack_branch

或者,將你的 rack 子目和最近一次從伺服器上抓取的 master 分支進行比較,你可以執行:

$ git diff-tree -p rack_remote/master

推薦學習:《》

以上就是範例講解!Git工具之高階合併的詳細內容,更多請關注TW511.COM其它相關文章!