章节 ▾ 第二版

3.1 Git 分支 - 分支概览

几乎所有的版本控制系统都支持某种形式的分支。分支意味着你可以脱离主开发线,继续进行开发而不影响主线。在许多版本控制工具中,这是一个相对昂贵的过程,通常需要你创建一个源代码目录的全新副本,这对于大型项目来说可能非常耗时。

有些人将 Git 的分支模型称为其“杀手级特性”,它无疑使 Git 在版本控制社区中脱颖而出。为什么它如此特别?Git 分支的实现方式非常轻量级,使得分支操作几乎瞬时完成,而且在分支之间切换也通常一样快。与其他许多版本控制系统不同,Git 鼓励经常进行分支和合并的工作流程,甚至一天内进行多次。理解并掌握这个特性会给你一个强大而独特的工具,并可能彻底改变你的开发方式。

分支概览

为了真正理解 Git 的分支工作方式,我们需要退一步,审视 Git 是如何存储其数据的。

正如你可能还记得 Git 是什么? 中提到的,Git 存储数据的方式不是一系列变更集或差异,而是一系列快照

当你提交时,Git 会存储一个提交对象,其中包含指向你暂存内容快照的指针。该对象还包含作者姓名和电子邮件地址、你输入的提交信息,以及指向该提交之前一个或多个提交的指针(其父提交):对于初始提交为零个父提交,对于普通提交为一个父提交,而对于合并两个或多个分支产生的提交则有多个父提交。

为了可视化这一点,我们假设你有一个包含三个文件的目录,并将它们全部暂存然后提交。暂存文件会计算每个文件的校验和(我们在 Git 是什么? 中提到的 SHA-1 哈希),将该文件的版本存储在 Git 仓库中(Git 将它们称为blob),并将该校验和添加到暂存区。

$ git add README test.rb LICENSE
$ git commit -m 'Initial commit'

当你通过运行 git commit 来创建提交时,Git 会校验每个子目录(在本例中,只有根项目目录),并将它们作为树对象存储在 Git 仓库中。然后,Git 会创建一个提交对象,该对象包含元数据以及指向根项目树的指针,以便在需要时重新创建该快照。

你的 Git 仓库现在包含五个对象:三个blob(每个代表三个文件之一的内容)、一个tree(列出目录内容并指定哪些文件名对应哪些 blob),以及一个commit(指向该根树并包含所有提交元数据)。

A commit and its tree
图 9. 一个提交及其树

如果你进行一些更改并再次提交,下一个提交会存储一个指向其紧前面提交的指针。

Commits and their parents
图 10. 提交及其父提交

Git 中的分支只是一个指向这些提交之一的、可移动的轻量级指针。Git 中的默认分支名称是 master。当你开始进行提交时,会有一个指向你最新提交的 master 分支。每次提交时,master 分支指针会自动向前移动。

注意

Git 中的“master”分支并非特殊分支。它与其他任何分支完全相同。几乎每个仓库都有一个的原因是 git init 命令默认创建它,而且大多数人懒得去更改它。

A branch and its commit history
图 11. 一个分支及其提交历史

创建新分支

当你创建新分支时会发生什么?嗯,这样做会创建一个新的指针供你移动。假设你想创建一个名为 testing 的新分支。你可以通过 git branch 命令来实现

$ git branch testing

这会创建一个指向你当前所在提交的新指针。

Two branches pointing into the same series of commits
图 12. 两个分支指向同一系列提交

Git 如何知道你当前在哪个分支上?它会维护一个特殊的指针,称为 HEAD。请注意,这与你在其他版本控制系统(如 Subversion 或 CVS)中可能习惯的 HEAD 概念大不相同。在 Git 中,它是指向你当前所在本地分支的指针。在这种情况下,你仍然在 master 分支上。git branch 命令仅仅创建了一个新分支,并没有切换到该分支。

HEAD pointing to a branch
图 13. HEAD 指向一个分支

你可以通过运行简单的 git log 命令来轻松查看这一点,该命令会显示分支指针指向何处。此选项称为 --decorate

$ git log --oneline --decorate
f30ab (HEAD -> master, testing) Add feature #32 - ability to add new formats to the central interface
34ac2 Fix bug #1328 - stack overflow under certain conditions
98ca9 Initial commit

你可以看到 mastertesting 分支都指向 f30ab 提交。

切换分支

要切换到现有分支,请运行 git checkout 命令。让我们切换到新的 testing 分支

$ git checkout testing

这会将 HEAD 移动到指向 testing 分支。

HEAD points to the current branch
图 14. HEAD 指向当前分支

这有什么意义呢?嗯,让我们再进行一次提交

$ vim test.rb
$ git commit -a -m 'Make a change'
The HEAD branch moves forward when a commit is made
图 15. 提交时 HEAD 分支向前移动

这很有趣,因为现在你的 testing 分支已经向前移动了,而你的 master 分支仍然指向你在运行 git checkout 切换分支时所在的提交。让我们切换回 master 分支

$ git checkout master
注意
git log 不会总是显示所有分支

如果你现在运行 git log,你可能会想知道你刚刚创建的“testing”分支去哪儿了,因为它不会出现在输出中。

分支并没有消失;Git 只是不知道你对该分支感兴趣,它试图显示它认为你感兴趣的内容。换句话说,默认情况下,git log 只会显示你已检出的分支下方的提交历史。

要显示所需分支的提交历史,你必须显式指定它:git log testing。要显示所有分支,请将 --all 添加到你的 git log 命令中。

HEAD moves when you checkout
图 16. 检出时 HEAD 移动

该命令执行了两项操作。它将 HEAD 指针移回指向 master 分支,并将工作目录中的文件恢复到 master 指向的快照。这也意味着你从现在开始所做的更改将与旧版本项目分道扬镳。它基本上是回滚了你在 testing 分支中所做的工作,以便你可以朝不同的方向前进。

注意
切换分支会更改工作目录中的文件

重要的是要注意,当你切换 Git 中的分支时,工作目录中的文件会发生变化。如果你切换到较旧的分支,你的工作目录将恢复到上一次在该分支提交时的样子。如果 Git 无法干净地执行此操作,它将不允许你切换。

让我们进行一些更改并再次提交

$ vim test.rb
$ git commit -a -m 'Make other changes'

现在你的项目历史已经分叉(请参见 分叉历史)。你创建了一个分支并切换到它,在该分支上进行了一些工作,然后切换回你的主分支并进行了其他工作。这些更改都隔离在不同的分支中:你可以在分支之间来回切换,并在准备好时将它们合并。所有这些都通过简单的 branchcheckoutcommit 命令完成。

Divergent history
图 17. 分叉历史

你也可以通过 git log 命令轻松地看到这一点。如果你运行 git log --oneline --decorate --graph --all,它将打印出你的提交历史,显示你的分支指针在哪里以及你的历史如何分叉。

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) Make other changes
| * 87ab2 (testing) Make a change
|/
* f30ab Add feature #32 - ability to add new formats to the central interface
* 34ac2 Fix bug #1328 - stack overflow under certain conditions
* 98ca9 Initial commit of my project

由于 Git 中的分支实际上是一个包含其指向的提交的 40 位 SHA-1 校验和的简单文件,因此创建和销毁分支的成本很低。创建一个新分支就像向一个文件写入 41 字节(40 个字符和一个换行符)一样快速简单。

这与大多数旧版本控制工具的分支方式形成了鲜明对比,它们通常涉及将项目的所有文件复制到第二个目录中。根据项目大小,这可能需要几秒钟甚至几分钟,而在 Git 中,这个过程总是瞬时的。此外,由于我们在提交时记录了父提交,因此查找合适的合并基础进行合并会自动完成,并且通常非常容易。这些特性有助于鼓励开发人员频繁创建和使用分支。

让我们看看为什么你应该这样做。

注意
同时创建新分支并切换到该分支

通常情况下,你会创建一个新分支并立即想切换到该新分支——这可以通过一个操作完成,即使用 git checkout -b <newbranchname>

注意

从 Git 版本 2.23 开始,你可以使用 git switch 代替 git checkout

  • 切换到现有分支:git switch testing-branch

  • 创建新分支并切换到它:git switch -c new-branch-c 标志表示 create(创建),你也可以使用完整标志:--create

  • 返回到之前检出的分支:git switch -