跳轉到內容

Git/分支與合併

來自華夏公益教科書
< Git

大多數版本控制系統都支援分支。例如,Subversion 強調“廉價複製”——也就是說,建立新分支並不意味著複製整個原始碼樹,因此速度很快。Git 的分支也同樣快。但是,Git 的真正優勢在於它在分支之間的合併,特別是減少處理合併衝突的痛苦。這就是它在支援協作軟體開發中如此強大的原因。

為什麼要分支?

[編輯 | 編輯原始碼]

在 Git 倉庫中建立多個分支有很多原因。

  • 你可能擁有代表“穩定”版本的支線,這些支線會繼續進行增量錯誤修復,但不會進行(重大)新功能的開發。與此同時,你可能擁有多個代表各種為下一個主要版本提出的新功能的“不穩定”支線,這些支線可能由不同的團隊並行開發。這些被接受的功能將需要合併到下一個穩定版本的支線中。
  • 你可以為個人實驗建立自己的私有分支。之後,如果程式碼變得足夠有趣,你可以與其他人分享,你可以將這些分支公開。或者,你可以將補丁傳送給上游公共分支的維護者,如果它們被接受,你可以將它們拉回自己的公共分支副本中,然後你可以退休或刪除你的私有分支。

事實上,你可能希望在不同的時間向不同的分支新增更新。在分支之間切換很容易。

檢視你的分支

[編輯 | 編輯原始碼]

使用git branch命令,不帶任何其他引數,就可以檢視你的倉庫有哪些分支。

$ git branch
* master

名為 "master" 的分支是預設的主開發線。你可以在需要時重新命名它,但通常使用預設名稱。當你提交一些更改時,這些更改將被新增到你檢出的分支中 - 在這種情況下是 master 分支。

建立新分支

[編輯 | 編輯原始碼]

讓我們建立一個新分支,我們可以用它來開發 - 命名為 "dev"

$ git branch dev
$ git branch
  dev
* master

這隻會建立新的分支,它不會改變你當前的 HEAD 的位置。你可以從 * 號看到 master 分支仍然是你檢出的分支。你現在可以使用git checkout dev切換到新分支。

或者,你也可以同時建立一個新分支並檢出它,使用以下命令:

$ git checkout -b newbranch

刪除分支

[編輯 | 編輯原始碼]

要刪除當前分支,再次使用git-branch,但這次傳送-d引數。

$ git branch -d <name>

如果該分支還沒有合併到 master 分支中,那麼它將失敗。

$ git branch -d foo
error: The branch 'foo' is not a strict subset of your current HEAD.
If you are sure you want to delete it, run 'git branch -D foo'.

Git 的提示可以幫助你避免可能丟失分支中的工作。如果你仍然確定要刪除該分支,可以使用git branch -D <name>命令。

有時會有很多本地分支,它們已經在伺服器上合併,因此已經變得無用。為了避免逐個刪除它們,只需使用以下命令:

git branch -D `git branch --merged | grep -v \* | xargs`

將分支推送到遠端倉庫

[編輯 | 編輯原始碼]

當你建立一個本地分支時,它不會自動與伺服器保持同步。與從伺服器拉取的分支不同,僅僅呼叫git push不足以將你的分支推送到伺服器。相反,你必須明確地告訴 git 推送該分支,以及要推送到的伺服器。

$ git push origin <branch_name>

從遠端倉庫刪除分支

[編輯 | 編輯原始碼]

要刪除已推送到遠端伺服器的分支,請使用以下命令:

$ git push origin :<branch_name>

這種語法並不直觀,但這裡發生的事情是,你正在發出一個類似於

$ git push origin <local_branch>:<remote_branch>

的命令,並在<local_branch>位置給出空分支,這意味著用空內容覆蓋該分支。

分支是 DVCS 的核心概念,但如果沒有良好的合併支援,分支將毫無用處。

git merge myBranch

該命令將給定的分支合併到當前分支。如果當前分支是給定分支的直接祖先,那麼將進行快進合併,並且當前分支頭將被重定向指向新分支。在其他情況下,將記錄一個合併提交,它將之前的提交和給定的分支尖端作為父提交。如果合併過程中存在任何衝突,則需要手動解決它們,然後才能記錄合併提交。

處理合併衝突

[編輯 | 編輯原始碼]

遲早,如果你經常進行合併,你會遇到這樣的情況:被合併的分支將包含對同一行程式碼的衝突更改。如何解決這種情況取決於你的判斷(以及一些手動編輯),但 Git 提供了你可以用來嘗試瞭解衝突的性質,以及如何最好地解決它們的工具。

現實世界中的合併衝突往往是非平凡的。在這裡,我們將嘗試建立一個非常簡單,儘管是人工的,例子,來讓你對其中涉及的內容有所瞭解。

讓我們從一個包含單個 Python 原始碼檔案的倉庫開始,名為test.py. 它的初始內容如下:

#!/usr/bin/python3
#+
# This code doesn't really do anything at all.
#-

def func_common()
    pass
#end func_common

def child1()
    func_common()
#end child1

def child2()
    func_common()
#end child2

def some_other_func()
    pass
#end some_other_func

將該檔案提交到倉庫,提交資訊寫成類似於“第一個版本”的內容。

現在使用以下命令建立一個新分支並切換到它:

git checkout -b side-branch

(這個第二個分支是為了模擬另一個程式設計師在同一個專案上進行的工作。)編輯檔案test.py,並簡單地交換函式child1child2的定義,相當於應用以下補丁:

diff --git a/test.py b/test.py
index 863611b..c9375b3 100644
--- a/test.py
+++ b/test.py
@@ -7,14 +7,14 @@ def func_common()
     pass
 #end func_common
 
-def child1()
-    func_common()
-#end child1
-
 def child2()
     func_common()
 #end child2
 
+def child1()
+    func_common()
+#end child1
+
 def some_other_func()
     pass
 #end some_other_func

將更新提交到分支side-branch,提交資訊寫成類似於“交換一對函式”的內容。

現在切換回master分支

git checkout master

這也會讓你回到test.py的先前版本,因為那是提交到該分支的最後一個(實際上也是唯一一個)版本。

在這個分支上,我們現在將函式func_common重新命名為common,相當於以下補丁:

diff --git a/test.py b/test.py
index 863611b..088c125 100644
--- a/test.py
+++ b/test.py
@@ -3,16 +3,16 @@
 # This code doesn't really do anything at all.
 #-
 
-def func_common()
+def common()
     pass
-#end func_common
+#end common
 
 def child1()
-    func_common()
+    common()
 #end child1
 
 def child2()
-    func_common()
+    common()
 #end child2
 
 def some_other_func()

將此更改提交到master分支,提交資訊寫成類似於“將 func_common 重新命名為 common”的內容。

現在,嘗試合併你對side-branch:

git merge side-branch

所做的更改。這應該會立即失敗,並顯示類似於以下的資訊:

Auto-merging test.py
CONFLICT (content): Merge conflict in test.py
Automatic merge failed; fix conflicts and then commit the result.

只需檢查 git-status(1) 報告的內容:

On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

        both modified:      test.py

no changes added to commit (use "git add" and/or "git commit -a")

如果我們檢視test.py,它應該看起來像這樣:

#!/usr/bin/python3
#+
# This code doesn't really do anything at all.
#-

def common()
    pass
#end common

<<<<<<< HEAD
def child1()
    common()
#end child1

=======
>>>>>>> side-branch
def child2()
    common()
#end child2

def child1()
    func_common()
#end child1

def some_other_func()
    pass
#end some_other_func

注意那些標記為“<<<<<< HEAD” ... “=======” ... “>>>>>>> src-branch”的部分:第一組標記之間的部分來自 HEAD 分支,即我們正在合併到的分支(master,在本例中),而最後一組標記之間的部分來自名為src-branch的分支,即我們正在合併的分支(side-branch,在本例中)。

假設我們完全瞭解程式碼的功能,我們可以仔細修復所有衝突/重複的部分,刪除標記並繼續合併。但也許這是一個大型專案,即使是專案負責人,也沒有人完全理解程式碼的每個角落。在這種情況下,至少縮小直接導致衝突的提交集非常有用,以便了解正在發生的事情。有一個您可以使用的命令,git log --merge,它專門設計用於在合併衝突期間使用,僅用於此目的。在此示例中,我得到的輸出類似於此

$ git log --merge
commit 9df4b11586b45a30bd1e090706e3ff09692fcfa7
Author: Lawrence D'Oliveiro <ldo@geek-central.gen.nz>
Date:   Thu Apr 17 10:44:15 2014 +0000

    rename func_common to common

commit 4e98aa4dbd74543d7035ea781313c1cfa5517804
Author: Lawrence D'Oliveiro <ldo@geek-central.gen.nz>
Date:   Thu Apr 17 10:43:48 2014 +0000

    swap a pair of functions around
$

現在,作為專案負責人,我可以進一步檢視這兩個提交,並發現衝突的本質非常簡單:一個分支交換了兩個函式的順序,而另一個分支更改了另一個函式的名稱,該函式被引用在重新排列的程式碼中。

另一個有用的命令是 git diff --merge,它顯示暫存區中原始檔狀態與父分支版本之間的 3 路差異

$ git diff --merge
diff --cc test.py
index c9375b3,863611b..088c125
--- a/test.py
+++ b/test.py
@@@ -3,18 -3,18 +3,18 @@@
  # This code doesn't really do anything at all.
  #-
  
--def func_common()
++def common()
      pass
--#end func_common
- 
- def child2()
-     func_common()
- #end child2
++#end common
  
  def child1()
--    func_common()
++    common()
  #end child1
 
+ def child2()
 -    func_common()
++    common()
+ #end child2
+ 
  def some_other_func()
      pass
  #end some_other_func
$

在這裡,您可以在每行的前兩列中看到“+”和“ - ”字元,分別表示相對於兩個分支新增/刪除的行,或者空格表示沒有更改。

有了這些資訊,我可以更有信心地解決修復衝突檔案的難題,建立以下合併版本test.py:

#!/usr/bin/python3
#+
# This code doesn't really do anything at all.
#-

def common()
    pass
#end common

def child2()
    common()
#end child2

def child1()
    common()
#end child1

def some_other_func()
    pass
#end some_other_func

只是為了重新檢查,在對上述修復版本執行 git add test.py 後,但在提交之前,執行另一個 git diff --merge,這應該產生類似的輸出

diff --cc test.py
index c9375b3,863611b..088c125
--- a/test.py
+++ b/test.py
@@@ -3,18 -3,18 +3,18 @@@
  # This code doesn't really do anything at all.
  #-
  
--def func_common()
++def common()
      pass
--#end func_common
- 
- def child2()
-     func_common()
- #end child2
++#end common
  
  def child1()
--    func_common()
++    common()
  #end child1
  
+ def child2()
 -    func_common()
++    common()
+ #end child2
+ 
  def some_other_func()
      pass
  #end some_other_func

git status 說些什麼呢?

On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

        modified:   test.py

現在,當您按照指示輸入 git commit 時,Git 會自動完成合並。

“愚蠢的內容跟蹤器”

[編輯 | 編輯原始碼]

git(1) 手冊頁中,Git 被概括為“愚蠢的內容跟蹤器”。瞭解在這種情況下“愚蠢”的含義很重要:這意味著 Git 不使用複雜的演算法來嘗試自動處理合併衝突,而是專注於僅顯示相關資訊以幫助人類智力解決衝突。Linus Torvalds 曾經說過,他不會相信他的程式碼可以由如此複雜的合併衝突解決系統處理,這就是為什麼他故意將 Git 設計成“愚蠢”的,因此,可靠的。

華夏公益教科書