-
1. 起步
-
2. Git 基礎
-
3. Git 分支
-
4. 伺服器上的 Git
- 4.1 協議
- 4.2 在伺服器上部署 Git
- 4.3 生成 SSH 公鑰
- 4.4 架設伺服器
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方託管服務
- 4.10 小結
-
5. 分散式 Git
-
A1. 附錄 A: Git 在其他環境
- A1.1 圖形介面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小結
-
A2. 附錄 B: 在應用程式中嵌入 Git
-
A3. 附錄 C: Git 命令
7.8 Git 工具 - 高階合併
高階合併
在 Git 中,合併通常相當容易。由於 Git 可以輕鬆地多次合併另一個分支,這意味著你可以有一個長期存在的分支,但可以隨時保持其最新狀態,頻繁地解決小衝突,而不是在最後遇到一個巨大的衝突。
然而,有時也會出現棘手的衝突。與其他一些版本控制系統不同,Git 不會試圖過度聰明地解決合併衝突。Git 的理念是,在確定合併解析度是否明確方面要做到明智,但如果存在衝突,它不會試圖自動解決它。因此,如果你等待太久才合併兩個快速分叉的分支,你可能會遇到一些問題。
在本節中,我們將探討可能出現的一些問題以及 Git 提供的工具來幫助處理這些更棘手的情況。我們還將介紹一些可以進行的不同的、非標準的合併型別,以及如何撤銷已完成的合併。
合併衝突
雖然我們在 基本合併衝突 中介紹了一些解決合併衝突的基礎知識,但對於更復雜的衝突,Git 提供了一些工具來幫助你弄清楚發生了什麼以及如何更好地處理衝突。
首先,如果可能的話,在執行可能發生衝突的合併之前,儘量確保你的工作目錄是乾淨的。如果你有未完成的工作,要麼將其提交到一個臨時分支,要麼將其暫存。這樣,你就可以撤銷你在此處嘗試的任何操作。如果你在執行合併時工作目錄中有未儲存的更改,以下一些技巧可能會幫助你保留這些工作。
讓我們透過一個非常簡單的例子來講解。我們有一個非常簡單的 Ruby 檔案,它會列印“hello world”。
#! /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 'Convert hello.rb to DOS'
[whitespace 3270f76] Convert 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 'Use Spanish instead of English'
[whitespace 6d338d2] Use Spanish instead of English
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 'Add comment documenting the function'
[master bec6336] Add comment documenting 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 將所有這些版本儲存在索引中,按“階段”劃分,每個階段都有關聯的數字。階段 1 是共同祖先,階段 2 是你的版本,階段 3 是來自 MERGE_HEAD 的版本,也就是你正在合併的版本(“對方”)。
你可以使用 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 Create README
* 694971d Update phrase to 'hola world'
| * e3eb223 (mundo) Add more tests
| * 7cff591 Create initial testing script
| * c3ffff1 Change text to 'hello mundo'
|/
* b7dcc89 Initial hello world code
現在我們有三個獨特的提交只存在於 master 分支上,另外三個存在於 mundo 分支上。如果我們嘗試合併 mundo 分支,我們會得到一個衝突。
$ 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 傳遞給 diff3 或 merge(這是預設值)。如果你傳遞 diff3,Git 會使用稍微不同的衝突標記版本,它不僅提供“我們的”和“他們的”版本,還會提供“基礎”版本,以便提供更多上下文。
$ 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 Create README
< 694971d Update phrase to 'hola world'
> e3eb223 Add more tests
> 7cff591 Create initial testing script
> c3ffff1 Change text to 'hello mundo'
這是一個很好的列表,包含總共六個提交,以及每個提交所在的開發線。
不過,我們可以進一步簡化它,以提供更具體的上下文。如果我們向 git log 新增 --merge 選項,它將只顯示合併中觸及當前有衝突檔案的提交。
$ git log --oneline --left-right --merge
< 694971d Update phrase to 'hola world'
> c3ffff1 Change text to 'hello mundo'
如果你改用 -p 選項執行它,你將只獲得導致衝突的檔案的 diff。這對於快速提供你所需的上下文,以幫助理解為什麼會出現衝突以及如何更智慧地解決它,將非常有用。
合併的組合 diff 格式
由於 Git 會暫存任何成功的合併結果,當你在衝突合併狀態下執行 git diff 時,你只會看到仍然處於衝突狀態的內容。這有助於瞭解你仍然需要解決什麼。
當你直接在合併衝突後執行 git diff 時,它會以一種相當獨特的 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()
這種格式稱為“Combined Diff”,並在每行旁邊提供兩列資料。第一列顯示該行在“我們的”分支和你的工作目錄檔案之間是否有差異(新增或刪除),第二列顯示“他們的”分支和你的工作目錄副本之間是否有差異。
所以在這個例子中,你可以看到 <<<<<<< 和 >>>>>>> 行存在於工作副本中,但並未存在於合併的任何一方。這是有道理的,因為合併工具為了我們的上下文放入了它們,但我們應該刪除它們。
如果我們解決了衝突並再次執行 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 log -p 新增 --cc 選項(它預設只顯示非合併提交的補丁),Git 將輸出此格式。
$ 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()
撤銷合併
現在你知道了如何建立一個合併提交,你可能會不小心建立一些。Git 的一個優點是犯錯誤是可以的,因為可以(並且在許多情況下很容易)修復它們。
合併提交也不例外。假設你開始在一個主題分支上工作,不小心將其合併到了 master,現在你的提交歷史看起來像這樣:
根據你期望的結果,有兩種方法可以解決這個問題。
修復引用
如果不需要的合併提交只存在於你的本地儲存庫中,最簡單也是最好的解決方案是移動分支,使其指向你想要的位置。在大多數情況下,如果你在錯誤的 git merge 之後立即執行 git reset --hard HEAD~,這將重置分支指標,使其看起來像這樣:
git reset --hard HEAD~ 後的歷史我們已經在 Git Reset 詳解 中介紹過 reset,所以理解這裡發生了什麼應該不難。這裡快速回顧一下:reset --hard 通常經過三個步驟:
-
移動 HEAD 指向的分支。在這種情況下,我們想將
master移回合併提交(C6)之前的位置。 -
使索引看起來像 HEAD。
-
使工作目錄看起來像索引。
這種方法的缺點是它會重寫歷史,這對於共享儲存庫來說可能是有問題的。請檢視 Rebase 的危險 以瞭解更多可能發生的情況;簡而言之,如果其他人擁有你正在重寫的提交,你應該避免 reset。如果自合併以來建立了任何其他提交,此方法也可能無效;移動引用將有效地丟失這些更改。
反轉提交
如果移動分支指標對你不起作用,Git 提供了一個選項,可以建立一個新的提交來撤銷現有提交的所有更改。Git 將此操作稱為“revert”,在這種特定情況下,你可以像這樣呼叫它:
$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"
-m 1 標誌指示哪個父項是“主線”並且應該保留。當你呼叫將 HEAD 合併(git merge topic)時,新提交有兩個父項:第一個是 HEAD(C6),第二個是被合併分支的尖端(C4)。在這種情況下,我們想撤銷透過合併父項 #2(C4)引入的所有更改,同時保留來自父項 #1(C6)的所有內容。
帶有 revert 提交的歷史看起來像這樣:
git revert -m 1 後的歷史新提交 ^M 的內容與 C6 完全相同,所以從這裡開始,就好像合併從未發生過一樣,只是現在未合併的提交仍然在 HEAD 的歷史中。如果你再次嘗試將 topic 合併到 master,Git 會感到困惑。
$ git merge topic
Already up-to-date.
topic 中沒有任何內容是 master 無法到達的。更糟糕的是,如果你在 topic 中新增工作並再次合併,Git 將只引入自反轉合併以來的更改。
解決這個問題最好的方法是反轉原始合併,因為你現在想將已反轉的更改重新引入,然後建立一個新的合併提交。
$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
在這個例子中,M 和 ^M 相互抵消。^^M 有效地合併了來自 C3 和 C4 的更改,而 C8 合併了來自 C7 的更改,所以現在 topic 已完全合併。
其他合併型別
到目前為止,我們已經討論了兩個分支的正常合併,通常使用所謂的“recursive”合併策略。然而,還有其他方法可以將分支合併在一起。讓我們快速介紹其中一些。
偏向我方或對方
首先,我們可以在正常的“recursive”合併模式下做另一件有用的事情。我們已經看到了使用 -X 傳遞的 ignore-all-space 和 ignore-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”遞迴合併*選項*不同。
這基本上會進行一個假的合併。它將記錄一個以兩個分支為父項的新合併提交,但它根本不會檢視你正在合併的分支。它只會將你當前分支中的程式碼作為合併結果記錄下來。
$ 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_branch 分支拉入我們主專案 master 分支的 rack 子目錄中。
$ git read-tree --prefix=rack/ -u rack_branch
當我們提交時,看起來就像 Rack 檔案都在該子目錄下——就像我們從一個 tar 包中複製進來一樣。有趣的是,我們可以相當容易地將一個分支中的更改合併到另一個分支。所以,如果 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——以檢視是否需要合併它們——你不能使用普通的 diff 命令。相反,你必須執行 git diff-tree 並指定要與之比較的分支。
$ git diff-tree -p rack_branch
或者,要將你的 rack 子目錄中的內容與伺服器上的 master 分支在你上次獲取時的內容進行比較,你可以執行:
$ git diff-tree -p rack_remote/master