章节 ▾ 第二版

3.1 Git 分支 - 分支入门

几乎每个版本控制系统都提供某种形式的分支支持。分支意味着你从开发主线中分离出来,继续进行工作,而不会干扰主线。在许多 VCS 工具中,这是一个相对耗费资源的过程,通常需要你创建一个源代码目录的新副本,这对于大型项目来说可能需要很长时间。

有些人将 Git 的分支模型称为其“杀手级功能”,它确实使 Git 在 VCS 社区中脱颖而出。它为何如此特别?Git 分支的方式非常轻量,使得分支操作几乎是瞬时的,并且在分支之间来回切换也通常同样快。与许多其他 VCS 不同,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(每个代表三个文件中的一个的内容)、一个(列出目录的内容并指定哪些文件名存储为哪些 blob),以及一个提交(带有指向该根树的所有提交元数据和指针)。

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的特殊指针。请注意,这与你可能习惯的其他 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。要显示所有分支,请在你的git log命令中添加--all

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 <新分支名称>在一个操作中完成。

注意

从 Git 2.23 版本开始,你可以使用git switch而不是git checkout

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

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

  • 返回到你上次检出的分支:git switch -