11. 使用分支
这份教程是 Git 分支的综合介绍。首先,我们简单讲解如何创建分支,就像请求一份新的项目历史一样。然后,我们会看到 git checkout 是如何切换分支的。最后,学习一下 git merge 是如何整合独立分支的历史。
我们已经知道,Git 分支和 SVN 分支不同。SVN 分支只被用来记录偶尔大规模的开发效果,而 Git 分支是你日常工作流中不可缺失的一部分。
git branch
分支代表了一条独立的开发流水线。分支是我们在第二篇中讨论过的「编辑/缓存/提交」流程的抽象。你可以把它看作请求全新「工作目录、缓存区、项目历史」的一种方式。新的提交被存放在当前分支的历史中,导致了项目历史被 fork 了一份。
git branch
命令允许你创建、列出、重命名和删除分支。它不允许你切换分支或是将被 fork 的历史放回去。因此,git branch
和 git checkout
、git merge
这两个命令通常紧密地结合在一起使用。
用法
git branch
列出仓库中所有分支。
git branch <branch>
创建一个名为 <branch>
的分支。不会 自动切换到那个分支去。
git branch -d <branch>
删除指定分支。这是一个安全的操作,Git 会阻止你删除包含未合并更改的分支。
git branch -D <branch>
强制删除指定分支,即使包含未合并更改。如果你希望永远删除某条开发线的所有提交,你应该用这个命令。
git branch -m <branch>
将当前分支命名为 <branch>
。
讨论
在 Git 中,分支是你日常开发流程中的一部分。当你想要添加一个新的功能或是修复一个 bug 时——不管 bug 是大是小——你都应该新建一个分支来封装你的修改。这确保了不稳定的代码永远不会被提交到主代码库中,它同时给了你机会,在并入主分支前清理你 feature 分支的历史。
比如,上图将一个拥有两条独立开发线的仓库可视化,其中一条是一个不起眼的功能,另一条是长期运行的功能。使用分支开发时,不仅可以同时在两条线上工作,还可以保持主要的 master branch
不混入奇怪的代码。
分支的顶端
Git 分支背后的实现远比 SVN 的模型要轻量。与其在目录之间复制文件,Git 将分支存为指向提交的引用。换句话说,分支代表了一系列提交的 顶端 ——而不是提交的 容器 。分支历史通过提交之间的关系来推断。
这使得 Git 的合并模型变成了动态的。SVN 中的合并是基于文件的,而Git 让你在更抽象的提交层面操作。事实上,你可以看到项目历史中的合并其实是将两个独立的提交历史连接起来。
栗子
创建分支
分支只是指向提交的 指针 ,理解这一点很重要。当你创建一个分支是,Git 只需要创建一个新的指针——仓库不会受到任何影响。因此,如果你最开始有这样一个仓库:
接下来你用下面的命令创建了一个分支:
git branch crazy-experiment
仓库历史保持不变。你得到的是一个指向当前提交的新的指针:
注意,这只会 创建 一个新的分支。要开始在上面添加提交,你需要用 git checkout
来选中这个分支,然后使用标准的 git add
和 git commit
命令。
删除分支
一旦你完成了分支上的工作,准备将它并入主代码库,你可以自由地删除这个分支,而不丢失项目历史:
git branch -d crazy-experiment
然后,如果分支还没有合并,下面的命令会产生一个错误信息:
error: The branch 'crazy-experiment' is not fully merged.
If you are sure you want to delete it, run 'git branch -D crazy-experiment'.
Git 保护你不会丢失这些提交的引用,或者说丢失访问整条开发线的入口。如果你 真的 想要删除这个分支(比如说这是一个失败的实验),你可以用大写的 -D
标记:
git branch -D crazy-experiment
它会删除这个分支,无视它的状态和警告,因此需谨慎使用。
git checkout
git checkout
命令允许你切换用 git branch
创建的分支。查看一个分支会更新工作目录中的文件,以符合分支中的版本,它还告诉 Git 记录那个分支上的新提交。将它看作一个选中你正在进行的开发的一种方式。
在上一篇中,我们看到了如何用 git checkout
来查看旧的提交。「查看分支」和「将工作目录更新到选中的版本/修改」很类似;但是,新的更改 会 保存在项目历史中——这不是一个只读的操作。
用法
git checkout <existing-branch>
查看特定分支,分支应该已经通过 git branch
创建。这使得 <existing-branch>
成为当前的分支,并更新工作目录的版本。
git checkout -b <new-branch>
创建并查看 <new-branch>
,-b
选项是一个方便的标记,告诉Git在运行 git checkout <new-branch>
之前运行 git branch <new-branch>
。
git checkout -b <new-branch> <existing-branch>
和上一条相同,但将 <existing-branch>
作为新分支的基,而不是当前分支。
讨论
git checkout
和 git branch
是一对好基友。当你想要创建一个新功能时,你用 git branch
创建分支,然后用 git checkout
查看。你可以在一个仓库中用 git checkout
切换分支,同时开发几个功能。
每个功能专门一个分支对于传统 SVN 工作流来说是一个巨大的转变。这使得尝试新的实验超乎想象的简单,不用担心毁坏已有的功能,并且可以同时开发几个不相关的功能。另外,分支可以促进了不同的协作工作流。
分离的 HEAD
现在我们已经看到了 git checkout
最主要的三种用法,我们可以讨论上一篇中提到的「分离 HEAD
」状态了。
记住,HEAD
是 Git 指向当前快照的引用。git checkout
命令内部只是更新 HEAD
,指向特定分支或提交。当它指向分支时,Git 不会报错,但当你 check out 提交时,它会进入「分离 HEAD
」状态。
有个警告会告诉你所做的更改和项目的其余历史处于「分离」的状态。如果你在分离 HEAD
状态开始开发新功能,没有分支可以让你回到之前的状态。当你不可避免地 checkout 到了另一个分支(比如你的更改并入了这个分支),你将不再能够引用你的 feature 分支:
重点是,你应该永远在分支上开发——而绝不在分离的 HEAD
上。这样确保你一直可以引用到你的新提交。不过,如果你只是想查看旧的提交,那么是否处于分离 HEAD
状态并不重要。
例子
下面的例子演示了基本的 Git 分支流程。当你想要开发新功能时,你创建一个专门的分支,切换过去:
git branch new-feature
git checkout new-feature
接下来,你可以和以往一样提交新的快照:
# 编辑文件
git add <file>
git commit -m "Started work on a new feature"
# 周而复始…
这些操作都被记录在 new-feature
上,和 master
完全独立。你想添加多少提交就可以添加多少,不用关心你其它分支的修改。当你想要回到「主」代码库时,只要 check out 到 master
分支即可:
git checkout master
这个命令在你开始新的分支之前,告诉你仓库的状态。在这里,你可以选择并入完成的新功能,或者在你项目稳定的版本上继续工作。
git merge
合并是 Git 将被 fork 的历史放回到一起的方式。git merge
命令允许你将 git branch
创建的多条分支合并成一个。
注意,下面所有命令将更改 并入 当前分支。当前分支会被更新,以响应合并操作,但目标分支完全不受影响。也就是说 git merge
经常和 git checkout
一起使用,选择当前分支,然后用 git branch -d
删除废弃的目标分支。
用法
git merge <branch>
将指定分支并入当前分支。Git 会决定使用哪种合并算法(下文讨论)。
git merge --no-ff <branch>
将指定分支并入当前分支,但 总是 生成一个合并提交(即使是快速向前合并)。这可以用来记录仓库中发生的所有合并。
讨论
一旦你在单独的分支上完成了功能的开发,重要的是将它放回主代码库。取决于你的仓库结构,Git 有几种不同的算法来完成合并:快速向前合并或者三路合并。
当当前分支顶端到目标分支路径是线性之时,我们可以采取 快速向前合并 。Git 只需要将当前分支顶端(快速向前地)移动到目标分支顶端,即可整合两个分支的历史,而不需要“真正”合并分支。它在效果上合并了历史,因为目标分支上的提交现在在当前分支可以访问到。比如,some-feature
到 master
分支的快速向前合并会是这样的:
但是,如果分支已经分叉了,那么就无法进行快速向前合并。当和目标分支之间的路径不是线性之时,Git 只能执行 三路合并 。三路合并使用一个专门的提交来合并两个分支的历史。这个术语取自这样一个事实,Git 使用 三个 提交来生成合并提交:两个分支顶端和它们共同的祖先。
但你可以选择使用哪一种合并策略时,很多开发者喜欢使用快速向前合并(搭配 rebase 使用)来合并微小的功能或者修复 bug,使用三路合并来整合长期运行的功能。后者导致的合并提交作为两个分支的连接标志。
解决冲突
如果你尝试合并的两个分支同一个文件的同一个部分,Git 将无法决定使用哪个版本。当这种情况发生时,它会停在合并提交,让你手动解决这些冲突。
Git 的合并流程令人称赞的一点是,它使用我们熟悉的「编辑/缓存/提交」工作流来解决冲突。当你遇到合并冲突时,运行 git status
命令来查看哪些文件存在需要解决的冲突。比如,如果两个分支都修改了 hello.py
的同一处,你会看到下面的信息:
# On branch master
# Unmerged paths:
# (use "git add/rm ..." as appropriate to mark resolution)
#
# both modified: hello.py
#
接下来,你可以自己修复这个合并。当你准备结束合并时,你只需对冲突的文件运行 git add
告诉 Git 冲突已解决。然后,运行 git commit
生成一个合并提交。这和提交一个普通的快照有着完全相同的流程,也就是说,开发者能够轻而易举地管理他们的合并。
注意,提交冲突只会出现在三路合并中。在快速向前合并中,我们不可能出现冲突的更改。
例子
快速向前合并
我们第一个例子演示了快速向前合并。下面的代码创建了一个分支,在后面添加了两个提交,然后使用快速向前合并将它并入主分支。
# 开始新功能
git checkout -b new-feature master
# 编辑文件
git add <file>
git commit -m "开始新功能"
# 编辑文件
git add <file>
git commit -m "完成功能"
# 合并new-feature分支
git checkout master
git merge new-feature
git branch -d new-feature
对于临时存在、用作独立开发环境而不是组织长期运行功能的工具的分支来说,这是一种常见的工作流。
同时注意,运行 git branch -d
时 Git 不应该产生错误提示,因为 new-feature
现在可以在主分支上访问了。
三路合并
下一个例子很相似,但需要进行三路合并,因为 master
在这个功能开发时取得了新进展。这是复杂功能和多个开发者同时工作时常见的情形。
# 开始新功能
git checkout -b new-feature master
# 编辑文件
git add <file>
git commit -m "开始新功能"
# 编辑文件
git add <file>
git commit -m "完成功能"
# 在master分支上开发
git checkout master
# 编辑文件
git add <file>
git commit -m "在master上添加了一些极其稳定的功能"
# 合并new-feature分支
git merge new-feature
git branch -d new-feature
注意,Git 现在无法进行快速向前合并,因为无法将 master
直接移动到 new-feature
。
对大多数工作流来说,new-feature
会是一个需要一段时间来开发的复杂功能,这也是为什么同时 master
会有新的提交出现。如果你的分支上的功能像上面的一样简单,你会更想将它 rebase 到 master
,使用快速向前合并。它会通过整理项目历史来避免多余的合并提交。