分支是 git 的杀手锏功能之一。就和绝地武士能够运用原力解决问题一样,gitter 必须能够熟练运用分支迅速应对问题。本章节会带领读者一起探寻 git 分支的设计思想,使后续的 git 修行更加的顺利。
Git 如何存储数据
理解 Git 分支机制之前,回顾一下 Git 存储数据的原理是十分有必要的。当我们在命令行中执行以下命令产生一次提交后,Git 内部发生了什么?
# 在 git 的工作空间新建三个文件。
vim gitbranchA.txt
vim gitbranchB.txt
vim gitbranchC.txt
# 暂存操作会为目录下每个文件计算校验和,并生成相应的 tree object 的树对象并存放到仓库中。
git add .
# 生成一个持有指向 tree object 对象的指针的提交对象。
git commit -m 'initial commit of branch'
现在 Git 仓库中包含了5个对象:3个 blob 对象对应着文件 gitbranchA、gitbranchB、gitbranchC、1个树对象对应着目录结构以及 blob 对象和文件名之间关系,以及一个指向树对象的提交对象。如下图所示。
如果你之前有接触过 linux 的 inode 索引式文件系统,你应该对这样的文件结构产生似曾相识的感觉。
假设你现在又做了一些更改并进行了一次提交,那么第二次提交就会保存着指向它上一次提交的指针。
在 git 中,我们用父提交指代上一次提交。
新建一个分支的含义
Git 的分支,只不过是一个指向某次对象的指针。无论指针的名称叫做 master 还是 dev,他们在 git 内部都以指针的形式存在,没有任何区别。
当你使用git branch testing
创建一个分支时,将会创建一个新的指针指向当前你所在分支指向的提交对象上。注意上图中叫做 HEAD 的特殊指针。和其他指针不同,HEAD 是一个指向指针的指针。HEAD 指针中存放的指针内存地址,即代表了你目前所在的分支。
现在我们切换到 testing 分支,并进行一系列的提交。
git checkout testing
vim test.rb
git commit - a -m 'made a change'
情况开始变得有趣了:testing 分支已经向前移动,而 master 分支依然指向移动之前的提交对象。换言之,目 master 分支指向的提交对象,是 testing 分支指向的提交对象的父提交对象。
现在让我们切换回 master 分支,做出一些改动并再次提交一次。
git checkout master
vim test.rb
git commit -a -m 'made other changes'
从现在开始。项目历史产生了分叉。可见,改变分支指针指向的提交对象,就能轻而易举的在各个不同的分支之间进行切换。而其他版本控制系统缺乏类似的轻量级的分支切换开销特性。
什么时候需要将分支合并?
试想目前你正处于以下图示中的工作场景。你正在 iss53 号分支上进行着开发工作。突然,业务人员反馈线上出现了一个紧急问题需要修复。于是你基于 master 分支建立了 hotfix 分支,并将补丁开发完毕。剩下的只要将 hotfix 分支合并回 master 分支,即可大功告成了。
git checkout master
git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)
在 master 分支上使用 git merge hotfix 即可将 hotfix 分支合并回 master。你会注意到合并时出现了 fast-forward 的提示。由于当前所在分支 master 所指向的提交对象,是待合并分支 hotfix 所指向的提交对象父提交对象。所以,对于顺着其中一个提交历史可以直达另一个提交提交历史的合并请求,git 会把分支指针向前移动。这种单线历史合并操作不存在分歧。
关于这个紧急问题的解决方案发布之后,你准备回到被打断之前时的工作中。 然而,你应该先删除 hotfix 分支,因为你已经不再需要它了 —— master 分支已经指向了同一个位置。 你可以使用带 -d 选项的 git branch 命令来删除分支:
git branch -d hotfix
现在你可以切换回你正在工作的分支继续你的工作,也就是针对 #53 问题的那个分支(iss53 分支)。
时光飞逝,iss53 的开发工作正式完成。你打算将其合并回 master 分支。这和之前你合并 hotfix 分支所做的工作差不多。 你只需要检出到你想合并入的分支,然后运行 git merge 命令:
$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html | 1 +
1 file changed, 1 insertion(+)
这和你之前合并 hotfix 分支的时候看起来有一点不一样。 在这种情况下,你的开发历史从一个更早的地方开始分叉开来(diverged)。 因为,master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。 出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4 和 C5)以及这两个分支的工作祖先(C2),做一个简单的三方合并。
和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。
合并请求产生冲突的原因
当合并请求产生三方合并操作时,git 并不会自作聪明的帮助你判断在应该以哪一方的信息为准。只要待合并的2个分支针对同一个文件的同一个位置有改动,git 就没办法帮助你自动进行处理。此时,就产生了合并冲突,需要用户自行解决。
<<<<<<< HEAD:index.html
=======
>>>>>>> iss53:index.html
什么是远程分支
远程分支是一个你无法直接在上面工作的普通分支。你只能合并或者基于远程分支创建副本。刚接触远程分支这个概念时,容易被 remote 这个词汇所误导。实际上,远程分支就是一个存在于本地的,指向某个提交对象的特殊指针而已。
远程指针特殊在哪里?
远程分支的表现形式是(remote)/(branch)。当你将数据从服务器上克隆到本地时,clone 命令会将服务器所有的分支指针拉取到本地,并在本地创建一个指向服务器上 master 分支的分支,并取名为:orgin/master。同时,git 也会帮你创建你自己的本地 master 分支。这个分支和 orgin/master 分支在最开始是指向同样的位置,这样你就可以在上面开始工作了。
如果你在本地的 master 分支做了一些工作,然而在同一时间,其他人推送提交到 git.ourcompany.com 并更新了它的 master 分支,同时,你又使用 git fetch origin
命令同步了远程服务器的数据。那么现在你的分支就会像是下面这样。
这和本地分支产生历史分叉没什么区别。唯一不同的是,你无法直接切换到 orgin/master 分支下进行工作,仅此而已。
如果你想要把 origin/master 远程分支所指向的提交对象的内容,反应到你本地的 master 分支的工作空间中,只需要通过合并或者pull
的方式,将 origin/master 合并即可。
git merge origin/master
同时你也可以基于远程分支,创建自己的本地分支,以便在其上工作。
结语
Git 相对于其他版本控制系统的杀手锏:分支——本质上就是存在于本地的指针文件。切换分支其实就是改写 HEAD 指针文件中的内容。这样的设计思想,使得其他版本控制系统中费时费力的分支切换到了 git 中变得易如反掌。低廉的分支切换开销,使得在不同的分支上进行快速切换,迅速应对问题的工作方式成为了可能。
参考书籍
《精通 Git pro 第二版》