章节 ▾ 第二版

3.1 Git 分支 - 分支简述

几乎所有的 VCS 都支持某种形式的 branching。分支意味着你偏离开发主线并继续工作,而不会干扰主线。在许多 VCS 工具中,这是一个稍微昂贵的过程,通常需要你创建一个新的源代码目录副本,对于大型项目来说,这可能需要很长时间。

有些人将 Git 的分支模型称为其“杀手级特性”,它无疑使 Git 在 VCS 社区中脱颖而出。 为什么它如此特别? Git 分支的方式非常轻量级,使分支操作几乎是瞬间完成,并且在分支之间来回切换通常也同样快。 与许多其他 VCS 不同,Git 鼓励经常进行分支和合并的工作流程,甚至一天多次。 了解并掌握此功能可以为您提供强大而独特的工具,并且可以完全改变您的开发方式。

分支简述

要真正了解 Git 如何进行分支,我们需要退一步并检查 Git 如何存储其数据。

你可能还记得 什么是 Git?,Git 不会将数据存储为一系列变更集或差异,而是存储为一系列快照

当你进行提交时,Git 会存储一个提交对象,其中包含指向您暂存的内容快照的指针。 该对象还包含作者的姓名和电子邮件地址,您键入的消息以及指向直接位于此提交之前的提交的指针(其父提交):初始提交的零个父提交,普通提交的一个父提交,以及由合并两个或多个分支产生的提交的多个父提交。

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

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

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

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

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

如果你做了一些更改并再次提交,那么下一次提交会存储一个指向紧接前一次提交的指针。

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

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

注意

Git 中的 “master” 分支并不是一个特殊的分支。它和任何其他分支完全一样。几乎每个仓库都有一个 “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 的特殊指针。 请注意,这与你可能习惯使用的其他 VCS(例如 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 个字符和一个换行符)一样快速而简单。

这与大多数较旧的 VCS 工具的分支方式形成鲜明对比,后者涉及将项目的所有文件复制到第二个目录中。 这可能需要几秒甚至几分钟,具体取决于项目的大小,而在 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 -

scroll-to-top