章节 ▾ 第二版

3.1 Git 分支 - 分支简述

几乎每个版本控制系统(VCS)都某种形式的分支支持。分支意味着你脱离主开发线,继续工作而不会干扰主线。在许多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”分支不是一个特殊分支。它与任何其他分支完全一样。几乎每个仓库都有一个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等其他VCS中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 <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