跳转到内容

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 <[email protected]>
Date:   Thu Apr 17 10:44:15 2014 +0000

    rename func_common to common

commit 4e98aa4dbd74543d7035ea781313c1cfa5517804
Author: Lawrence D'Oliveiro <[email protected]>
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 设计成“愚蠢”的,因此,可靠的。

华夏公益教科书