分支
几乎每个版本控制系统都有某种形式的分支支持。分支意味着偏离了主开发线,并继续进行工作,而不会影响到主开发线。在许多版本控制系统工具中,这是一个比较昂贵的过程,通常需要创建源代码目录的一个新副本,对于大型项目来说可能需要很长时间。
一些人将git的分支模型称为其“杀手级特性”,它确实使git在版本控制系统社区中脱颖而出。为什么它如此特别呢?git分支的方式非常轻量级,使分支操作几乎瞬间完成,并且在分支之间来回切换通常也同样快速。与许多其他版本控制系统不同,git鼓励频繁地进行分支和合并工作流程。
分支简介
为了真正理解git如何进行分支,我们需要退一步看git如何存储其数据。
git不是将数据存储为一系列的变更集或差异,而是存储为一系列的快照。
当提交(commit)时,git会存储一个包含指向暂存内容快照的指针的提交对象。该对象还包含作者的姓名和电子邮件地址、键入的消息,以及指向直接在此提交之前出现的提交或提交的指针(其父提交或父提交):初始提交没有父提交,普通提交有一个父提交,合并两个或多个分支结果的提交有多个父提交。
为了形象化地描述这一点,我们直接应用git book上面的描述示例:
假设有一个包含三个文件的目录,将它们全部暂存并提交。暂存文件会为每个文件计算一个校验和(SHA-1哈希),将该文件的版本存储在git存储库中(git将它们称为blob),并将该校验和添加到暂存区:
$ git add README test.rb LICENSE
$ git commit -m 'Initial commit'
当运行 `git commit` 创建提交时,git 对每个子目录(在这种情况下,只是根项目目录)进行校验和,并将它们存储为树对象(tree object)在git存储库中。然后,git 创建一个提交对象(commit object),其中包含元数据和指向根项目树的指针,以便在需要时重新创建该快照。
git存储库现在包含五个对象:三个blob(每个表示三个文件中的一个的内容),一个树对象(tree),列出目录的内容并指定哪些文件名存储为哪些blob,并且一个提交(commit)包含指向根树的指针和所有提交元数据。
如果进行了一些更改并再次提交,下一个提交会存储一个指向其之前的提交的指针。
在git中,分支(branch)只是一个轻量级的可移动指针,指向这些提交中的一个。git中的默认分支名称是 master。当开始进行提交时,会得到一个指向最后一次提交的 master 分支。每次提交时,master 分支指针会自动向前移动。
创建新的分支
当创建一个新的分支时会发生什么呢?这样做会创建一个新的指针以便移动它。假设想创建一个名为 testing 的新分支。可以使用 git branch 命令来实现这一点。
$ git branch testing
这会创建一个指向当前所在的相同提交的新指针。
git是如何知道当前在哪个分支呢?它会保持一个称为 HEAD 的特殊指针。请注意,这与其他版本控制系统(如Subversion或CVS)中 HEAD 的概念大不相同。在git中,这是指向当前所在的本地分支的指针。在这种情况下,仍然处于 master 分支上。git branch 命令只是创建了一个新分支 - 它没有切换到该分支。
可以通过运行一个简单的 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
可以看到master与testing都指向提交f30ab。
切换分支
应用git checkout切换到目标分支
$ git checkout testing
这有什么重要意义呢?让我们基于testing分支做一个提交来看:
$ vim test.rb
$ git commit -a -m 'Make a change'
这很有趣,因为现在testing 分支已经前进了,但是master 分支仍然指向运行 git checkout 切换分支时所在的提交,再让我们切换回 master 分支。
$ git checkout master
git log 并不总是显示所有的分支。
如果现在运行 git log,可能会想知道刚刚创建的 "testing" 分支去哪了,因为它不会出现在输出中。
NOTE:该分支并没有消失;git 只是不知道我们当前对该分支感兴趣,它试图向我们展示它认为我们感兴趣的内容。换句话说,默认情况下,git log 只会显示已经检出的分支下面的提交历史。
要显示所需分支的提交历史,必须明确指定它:git log testing。要显示所有分支,请在 git log 命令中添加 --all。
这个命令做了两件事情。它将 HEAD 指针移回指向 master 分支,并将工作目录中的文件恢复到 master 分支指向的快照。这也意味着从这一点开始所做的更改将与项目的旧版本分离。它实际上是将在 testing 分支中所做的工作回退,以便可以选择不同的方向。
需要注意的是,当在Git中切换分支时,工作目录中的文件会发生变化。如果切换到一个较旧的分支,工作目录将被恢复为看起来像在该分支上最后一次提交时的样子。如果Git无法干净地执行此操作,它将根本不允许进行切换。
现在让我们做一个新的更改并提交
$ vim test.rb
$ git commit -a -m 'Make other changes'
现在,项目历史已经分叉(请参阅分叉历史)。创建并切换到一个分支,在其中进行了一些工作,然后切换回主分支并进行了其他工作。这两个更改都在不同的分支中进行了隔离:当准备好时,可以在分支之间来回切换并将它们合并在一起。而所做的一切都是通过简单的分支、检出和提交命令完成的。
也可以通过 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 中,这个过程总是瞬间完成。另外,因为我们在提交时记录了父提交,所以为了合并找到一个合适的合并基础是自动完成的,通常非常容易完成。这些功能有助于鼓励开发人员经常创建和使用分支。
让我们看看为什么应该这样做。
NOTE1:
创建一个新分支并立即切换到它是很常见的操作 - 这可以通过一个操作完成,即 git checkout -b <新分支名称>。
NOTE2:
从git版本2.23开始,可以使用git switch而不是git checkout来:
切换到一个已存在的分支:git switch testing-branch。
创建一个新的分支并切换到它:git switch -c new-branch。-c 标志代表创建,你也可以使用完整的标志:--create。
返回到之前检出的分支:git switch -