章节 ▾ 第二版

3.2 Git 分支 - 分支与合并基础

分支与合并基础

让我们通过一个在现实开发中可能遇到的工作流程,来了解一下分支与合并的简单示例。你将按照以下步骤进行:

  1. 进行网站的开发工作。

  2. 为你正在处理的用户故事(user story)创建一个分支。

  3. 在该分支上进行开发。

此时,你接到电话,得知另一个紧急问题需要修复(hotfix)。你将执行以下操作:

  1. 切换到你的生产环境分支。

  2. 创建一个分支来添加紧急补丁。

  3. 测试通过后,合并补丁分支,并推送到生产环境。

  4. 切回原来的用户故事分支并继续工作。

分支基础

首先,假设你正在进行项目开发,并且在 master 分支上已经有了几次提交。

A simple commit history
图 18. 简单的提交历史

你决定着手处理公司问题跟踪系统中的 #53 号问题。若要创建一个新分支并同时切换到该分支,可以运行带有 -b 参数的 git checkout 命令:

$ git checkout -b iss53
Switched to a new branch "iss53"

这实际上是以下命令的简写:

$ git branch iss53
$ git checkout iss53
Creating a new branch pointer
图 19. 创建新的分支指针

你在网站上进行开发并完成了一些提交。这样做会使 iss53 分支向前推进,因为你当前签出(checkout)了该分支(也就是说,你的 HEAD 指向它)。

$ vim index.html
$ git commit -a -m 'Create new footer [issue 53]'
The `iss53` branch has moved forward with your work
图 20. iss53 分支随着你的工作向前推进

现在你接到电话,得知网站有一个问题,需要立即修复。使用 Git,你不必将你的补丁与已完成的 iss53 变更一起部署,也不必在应用修复到生产环境之前大费周折地回退这些变更。你所要做的只是切回 master 分支。

不过,在切换之前请注意,如果你的工作目录或暂存区有尚未提交的更改且与要签出的分支存在冲突,Git 将不允许你切换分支。切换分支时最好保持干净的工作状态。我们稍后会在 储藏与清理(Stashing and Cleaning) 中讨论绕过此问题的方法(即储藏和修改提交)。现在,假设你已经提交了所有更改,因此可以切回 master 分支:

$ git checkout master
Switched to branch 'master'

此时,你的项目工作目录与你在开始处理 #53 号问题之前完全一样,你可以专注于紧急补丁的修复。记住这一点很重要:当你切换分支时,Git 会重置你的工作目录,使其看起来像该分支上最后一次提交时的状态。它会自动添加、删除和修改文件,以确保你的工作副本与该分支上最近一次提交的内容一致。

接下来,你需要进行紧急修复。让我们创建一个 hotfix 分支,并在其上完成修复工作:

$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'Fix broken email address'
[hotfix 1fb7853] Fix broken email address
 1 file changed, 2 insertions(+)
Hotfix branch based on `master`
图 21. 基于 master 的紧急补丁分支

你可以运行测试,确保补丁符合要求,最后将 hotfix 分支合并回 master 分支以部署到生产环境。你可以通过 git merge 命令来完成:

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

你会注意到合并过程中出现了“快进(fast-forward)”字样。由于 hotfix 分支指向的 C4 提交直接位于你当前所在的 C2 提交之后,Git 只是简单地将指针向前推进。换句话说,当你尝试合并一个提交,且该提交可以通过遵循当前提交的历史记录直接到达时,Git 会为了简化操作而直接向前移动指针,因为没有分叉的开发工作需要合并——这被称为“快进”。

你的变更现在已包含在 master 分支所指向的提交快照中,你可以部署该补丁了。

`master` is fast-forwarded to `hotfix`
图 22. master 快进到 hotfix

在最重要的补丁部署完成后,你准备好切回被中断之前的工作了。不过,首先你要删除 hotfix 分支,因为你不再需要它了——master 分支已经指向了相同的位置。可以使用 git branch-d 选项来删除它:

$ git branch -d hotfix
Deleted branch hotfix (3a0874c).

现在你可以切回 iss53 分支并继续工作了。

$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'Finish the new footer [issue 53]'
[iss53 ad82d7a] Finish the new footer [issue 53]
1 file changed, 1 insertion(+)
Work continues on `iss53`
图 23. 继续在 iss53 上工作

值得注意的是,你在 hotfix 分支中所做的更改并未包含在 iss53 分支的文件中。如果你需要合并这些更改,可以通过运行 git merge mastermaster 分支合并到你的 iss53 分支中,或者你也可以等到稍后决定将 iss53 合并回 master 时再整合这些变更。

合并基础

假设你认为 #53 号问题的工作已完成,准备将其合并到 master 分支。为此,你将把 iss53 分支合并到 master 中,就像之前合并 hotfix 分支一样。你只需要签出要合并入的分支,然后运行 git merge 命令即可:

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

这看起来与之前执行的 hotfix 合并略有不同。在这种情况下,开发历史从某个较早的点开始分叉了。因为你所在分支的提交并不是要合并进来的分支的直接祖先,Git 需要做一些额外工作。在这种情况下,Git 会进行一次简单的三路合并(three-way merge),利用两个分支末端的快照以及它们的共同祖先进行处理。

Three snapshots used in a typical merge
图 24. 典型合并中使用的三个快照

Git 不会只是简单地移动分支指针,而是创建一个由此次三路合并产生的新快照,并自动创建一个指向它的新提交。这被称为合并提交(merge commit),它的特殊之处在于它有多个父提交。

A merge commit
图 25. 一个合并提交

现在你的工作已经合并完成,不再需要 iss53 分支了。你可以在问题跟踪系统中关闭该问题,并删除该分支:

$ git branch -d iss53

合并冲突基础

有时,这个过程不会那么顺利。如果你在两个要合并的分支中修改了同一个文件的同一部分,Git 将无法干净地合并它们。如果针对 #53 号问题的修复修改了与 hotfix 分支相同的文件部分,你就会遇到合并冲突,看起来像这样:

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

Git 没有自动创建新的合并提交,而是暂停了进程,等待你解决冲突。如果你想在任何时候查看合并冲突后哪些文件处于未合并状态,可以运行 git status

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

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

    both modified:      index.html

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

任何存在合并冲突且未解决的文件都会被列为未合并(unmerged)。Git 会在冲突文件中添加标准的冲突解决标记,以便你手动打开并解决这些冲突。你的文件包含一个看起来像这样的部分:

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

这意味着 HEAD 中的版本(你的 master 分支,因为你在运行合并命令时签出的就是它)是该块的顶部(======= 上方的一切),而 iss53 分支中的版本看起来像是底部的那部分。为了解决冲突,你必须选择保留某一方,或者自己合并内容。例如,你可能通过将整个块替换为以下内容来解决此冲突:

<div id="footer">
please contact us at email.support@github.com
</div>

这种解决方案各取了一部分内容,并且 <<<<<<<=======>>>>>>> 行已被完全删除。在你解决完每个冲突文件中的每一处冲突后,对每个文件运行 git add 将其标记为已解决。暂存文件即在 Git 中将其标记为已解决。

如果你想使用图形化工具来解决这些问题,可以运行 git mergetool,它会启动一个适当的可视化合并工具并引导你完成冲突解决。

$ git mergetool

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html

Normal merge conflict for 'index.html':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

如果你想使用除默认工具之外的其他合并工具(Git 在本例中选择了 opendiff,因为命令是在 macOS 上运行的),你可以在“one of the following tools”后看到所有支持的工具列表。只需输入你想使用的工具名称即可。

注意

如果你需要更高级的工具来解决复杂的合并冲突,我们将在 高级合并(Advanced Merging) 中进一步讨论。

退出合并工具后,Git 会询问你合并是否成功。如果你告诉脚本合并成功,它会为你暂存该文件以标记为已解决。你可以再次运行 git status 来验证所有冲突是否已解决。

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

Changes to be committed:

    modified:   index.html

如果你对结果满意,并且确认所有有冲突的文件都已暂存,可以输入 git commit 来完成合并提交。默认的提交消息看起来像这样:

Merge branch 'iss53'

Conflicts:
    index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#	.git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#	modified:   index.html
#

如果你认为这对将来查看此合并的人有帮助,你可以修改此提交消息,详细说明你是如何解决冲突的,并在原因不明显的情况下解释你所做更改的原因。