第4章 分支
几乎每一个VCS(Version Control System 版本控制软件)都以这样或那样的形式支持“分支”。这意味着你可以从开发的主线克隆出一个开发进度与主线完全一样的分支,进而在这个分支上继续做一些开发,而不会影响到原来的主线。
在众多的VCS中,分支这样的操作会有较大的代价,它们通常是通过复制代码来完成分支的创建的,这对于大型项目而言必然是十分耗时的操作。
有人指出Git中的分支是一个“杀手级特性”(killer feature)。的确,Git中的“分支”特性让Git从众多的VCS中脱颖而出。Git中的分支操作是十分轻量的,几乎是瞬间完成的(nearly instantaneous),在Git中来回切换分支是很快速的。不像很多其他的VCS,Git鼓励我们大量地使用分支!理解并且掌握分支操作是学习Git不可避免的一个技术关卡。
4.1 铺垫
为了充分理解Git的分支,我们需要先学习Git是如何存储数据的。当你提交的时候,Git会存储一个“提交对象”,该提交对象包含一个指针,指向一个快照(树),该快照就是提交那一刻的所有文件的快照(副本)。这个提交对象同时也包含作者的名字和邮箱、提交信息、指向前一次提交的指针。
为了更加形象地说明,让我们假设你有一个包含3个文件的目录,3个文件的名字分别是:README、LICENSE、test.rb。当你把它们加入到暂存区,再提交,git就会为每一个文件生成一个id,同时git还会生成一颗树,树的根节点列出了哪个文件名对应哪个id,git还会生成一个提交对象,该对象包含一个指向该树根节点的指针,如下图所示:
98ca9就是提交对象,92ec2就是树的根节点,通过该根节点可以找到提交那一刻的所有文件的内容,也就是所谓的快照。
在Git中的分支本质上就是一个指向某个提交对象的指针。也就是说,分支就是指针!分支就是指针!分支就是指针!Git中默认的分支名叫做master,记住分支就是指针,当你做提交操作时,git实际上是让这个master指针指向你最后一次的提交,每当你提交时,master指针都会自动向前移动以指向最新一次的提交
4.2 创建分支
当你创建一个新的分支时,仅仅是创建了一个新的指针,该指针指向当前的提交。比如,你想创建一个名为testing的分支,你可以键入以下命令:
git branch testing
这个命令将会创建一个新的指针,指向你当前所处的提交,此时两个分支都指向同一个提交对象:
那么git如何确定你究竟处在哪个分支上呢?这就要借助于另外一个叫做HEAD的指针了,该指针是一个二级指针(指向指针的指针)。如下图所示:
HEAD指针指向哪个分支,就表示你处在哪个分支上。你现在仍然处在master分支上,“git branch”命令仅仅是创建一个新的分支,该命令并不会让你切换到刚刚创建的分支上。
你可以利用 git log --decorate 命令来查看你目前处于哪个分支
4.3 切换分支
切换分支的命令如下:
git checkout 分支名字
让我们切换到testing分支:
这个命令本质上是让HEAD指向了testing:
git这样设计分支的意义何在?好吧,让我们此时做一个提交:
此时的分支模型看起来是这个样子:
有趣的是,testing分支向前移动了,而master分支并没有向前移动。让我们再次切回master分支
git checkout master
这个命令做了两件事情:它让HEAD指向了master,同时将你工作区中的文件恢复到了master所指向的文件快照的状态。
注意,git log 命令不会显示所有的分支提交,git log默认只会显示当前分支的提交。如果想要查看所有分支的提交信息,则需要为 git log 命令添加 --all 参数。
此时如果我们再次提交会发生什么?如下:
现在你项目的提交历史出现了分叉。一开始,你只有一个叫做master的分支,然后你创建了一个testing分支,并在testing上做了一些提交,然后你又切换回master分支,并又在master上做了一些提交。此时两个分支中的提交是被隔离开的:
你也可以通过以下命令来查看此时的提交历史:
git log --oneline --decorate --graph --all
该命令有3个作用
1. 会打印出你的提交历史
2. 显示出各个分支的指向哪个提交
3. 显示出你的提交历史是如何分叉的
可以看出,在Git中,创建分支和切换分支是十分轻量的,这与其他VCS形成了鲜明的对比,其他的VCS中的分支会复制整个项目文件到第二个文件夹中,当项目变得越来越大时,代价就会越来越高。而在Git中,分支操作几乎是瞬时完成的,所以Git鼓励程序员大量使用分支!
小技巧:你可以利用以下命令在创建新分支的同时,切换到该新分支:
git checkout -b 新分支名
小技巧:以下命令可以在最近两个分支之间来回切换
git switch -
注意:如果当前分支中,工作区中一个内容不同的文件工作没有完成,比如修改了一个被git管理(已经提交过)的文件,还没有提交,此时则无法切换分支。(如果是新建的文件,还没有被git管理,是可以切换分支的)
当前例子只是为了演示分支的创建和提交的分叉,并不设计分支的合并。为了学习以下的合并分支,可以删除当前仓库,从头开始提交。
4.4 合并分支
分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN
如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了Git又学会了SVN!
快速合并
我们已经知道,每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长,这条线就是一个分支,即master分支。Git用master指向最新的提交,再用HEAD指向master
注意:
1. head用于确定目前处于哪个分支
2. master用于指向当前工作区对应master分支的哪个提交
当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前在dev分支上:
不过从现在开始,对工作区的修改和提交就是针对dev分支了,每提交一次后,dev指针都会往前移动一步,而master指针不变:
假如我们完成了在dev分支上的工作并且通过了测试,就可以把dev合并到master上。Git怎么合并呢?为了让master把dev分支合并进来,需要先切换到master分支,再让master去合并dev:
git checkout master
git merge dev
目前这种情况,当mater合并dev时,就是直接让master指向dev的当前提交,就完成了合并。这种情况我们称之为快速合并(Fast-forward),如下图所示:
合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支:
git branch -d dev
没有冲突的分叉合并
当你在master从做了一个feature1分支,并做了一次提交,然后又切换回master分支,并做了一次提交,此时的git分支模型开起来就是这个样子:
此时让master分支合并feature1分支的话(没有冲突),就会产生一个新的提交,这个新的提交融合了master分支的最新提交和feature1分支的最新提交,如下:
根据以下git log的提示,完成所有步骤
有冲突的分叉合并
查看3.html的内容:
=====是一个分隔符,上部分内容是当前master分支对3.html做的修改,下部分内容是feature1对3.html做的修改。
你可以根据实际情况来解决这个冲突,这里把两部分修改都保留下来:
再次提交,即可完成合并,查看git提交日志
冲突解决的本质是,在冲突发生后,只要再次执行“git add . ”,"git commit"命令,冲突就算解决了。
4.5 综合例子
让我们通过一个简单的例子再来串一遍合并分支。在实际的开发中,你按以下方式使用git
1. 在一个项目中做一些开发工作
2. 要为项目添加新的功能时,创建一个新的分支
3. 在该新分支上进行新功能的开发
此时你接到一个电话,生产分支(master分支)需要立即修复一个bug,你按以下步骤来处理:
1. 切换到生产分支
2. 从生产分支创建创建一个hotfix分支来修复bug
3. 修复完成后,成功通过测试,将hotfix分支合并到master分支
4. 切换回你原来的分支继续工作
Basic Branching
首先,在master分支上已经有一些提交了:
然后,你决定做一个 issue#53 分支来做一些开发工作:
git checkout -b iss53
然后,你在iss53这个分支上做了一些修改,并进行了一次提交:
vi index.html
git commit -a -m 'Create new footer [issue 53]'
此时,你接到一个电话,说在产品分支中(master)发现了一个问题,你必须停止手头的任何工作,要立即解决这个bug。你要做的是,切换到产品分支。注意在切换分支时,工作目录必须是干净的,而此时手头的工作并没有完成,提交也不是,撤销工作区中的所有修改也不是,怎么办?此时可以先把工作区中的修改先存起来,再切换到产品分支
git stash
git checkout master
此时,你工作区中的文件内容,就是你开始issue#53分支之前的状态,这是非常重要的一点:当你切换到某个分支的时候,git会重置你工作区中的文件内容到该分支最后一次提交时的状态。接下来你创建了一个hotfix分支以修复产品分支的bug,并且在hotfix分支上做了一次提交:
在经过测试后,确定产品分支中的bug在hotfix分支已经解决了,你就可以让master分支把hotfix分支合并了:
git checkout master
git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)
注意在结果中出现了 Fast-forward,因为maseter指向的C4提交和hotfix指向的C2提交之间没有分叉,所以git仅仅是移动master指针到hotfix所指向的提交,也就是前面说过的“快速合并”:
在这个超级重要的bug被修复并部署到服务器之后,你准备切换回原来的iss53分支继续原先的工作了。不过你应该先删除 hotfix 这个分支,因为它没有必要再存在了,master分支已经包含了 hotfix分支的所有提交了,你可以执行以下的命令来删除一个分支:
git branch -d hotfix
现在,你可以切原来的iss53分支继续原先的工作了,但是原先工作区中的修改已经被存起来了,如何恢复呢?如下
git checkout iss53
git stash pop
然后你又在iss53上进行了一次提交,以完成 iss53 分支上的开发任务:
注意在C5提交中,并没有包含当初在 hotfix 分支中做的任何修改。所以你应该让master分支把iss53分支合并起来,或者在未来任何一个你觉得合适的时刻再做这个合并也不迟。
Basic Merging
假设你决定让master分支去合并iss53分支中的修改了,你会按如下命令操作:
git checkout master
git merge iss53
此时的 master 合并 iss53 与上面的 master 合并 hotfix 不同。在当前这种情况下,你的提交历史中出现了分叉。因为 master 中的 C4 提交不是 iss53 中 C5 提交的“直系祖先”,所以git会用3个提交来完成这次合并:C4、C5,以及它们共同的祖先C2:
git会自动创建一个新的提交来完成这次合并,这样的提交叫做“合并提交”,该提交的特殊之处在于它有多个父提交:
现在你可以删除 iss53 分支了
git branch -d iss53
注意,有时候这个合并过程并不顺利。比如在
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
此时git就不会自动创建一个新的提交了。你可以通过 git status 命令来查看哪个文件再合并时冲突了:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
冲突文件的内容大概是这个样子:
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html
最终,你决定这样修改冲突文件:
<div id="footer">
please contact us at email.support@github.com
</div>
然后就可以提交本次的修改,以完成合并了,提交信息默认看起来像这个样子:
Merge branch 'iss53'
Conflicts:
index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
# .git/MERGE_HEAD
# and try again.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
# modified: index.html
#
4.6 分支管理
创建分支:
git branch 分支名
切换分支
git checkout 分支名
创建分支的同时切换分支
git checkout -b 分支名
在最近的两个分支之间切换
git switch -
查看分支:
git branch
查看分支的同时,也查看每个分支的最新提交
git branch -v
删除分支
git branch -d 分支名
强制删除还没有被合并的分支
git branch -D 分支名
合并分支
git merge 分支名
查看已合并到当前分支的其它分支
git branch --merged
查看还没有合并到当前分支的其他分支
git branch --no-merged