Git指南(3)——分支与合并

Git

几乎每一种版本控制系统都以某种形式支持分支。使用分支意味着你可以从开发主线上分离开来,然后在不影响主线的同时继续工作。相比同类软件,Git很显著的一个优点,就是版本的分支(branch)和合并(merge)十分方便。有人把Git的分支模型称为“必杀技特性”,因为它的分支可谓是难以置信的轻量级。理解分支的概念并熟练运用后,你才会意识到为什么Git是一个如此强大而独特的工具,并从此真正改变你的开发方式。本文将简单介绍Git的分支与合并的基本知识。

1. Git分支管理策略

Git的新建分支操作几乎可以在瞬间完成,并且在不同分支间切换起来也差不多一样快。和许多其他版本控制系统不同,Git鼓励在工作流程中频繁使用分支与合并,哪怕一天之内进行许多次都没有关系。

但是太方便了也会产生副作用。如果你不加注意,很可能会留下一个枝节蔓生、四处开放的版本库,到处都是分支,完全看不出主干发展的脉络。所以在学习Git的分支之前,有必要先了解一下常用的分支管理策略。

(本节参考自阮一峰的《Git分支管理策略》

  1. 主分支master:代码库应该有且只有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。
  2. 开发分支develop:主分支只用来分布重大版本,日常开发应该在开发分支Develop上完成。如果想正式对外发布,就在Master分支上,对Develop分支进行“合并”。
  3. 功能分支feature:从develop上衍生出来,一般是为了开发某种特定功能,开发完成后,要再合并入develop。分支名字可以采用feature-*的形式命名。
  4. 修补bug分支fixbug:软件正式发布以后,难免会出现bug。这时就需要从master分支上面分出来,进行bug修补,修补结束以后,再合并进master和develop分支。分支名字可以采用fixbug-*的形式命名。
  5. 预发布分支release:发布正式版本之前(即合并到Master分支之前),可能需要从develop分支上分出来一个预发布的版本进行测试,预发布结束以后,必须合并进develop和master分支。分支名字可以采用release-*的形式命名。(此种分支一般比较少用)

以上的功能分支、修补bug分支和预发布分支都属于临时性分支,一般使用完以后要删除掉这种分支,使得代码库的常设分支只有master和develop。最后以一张图总结一下常用的分支形式图。

Git分支管理示意图

2. 分支与管理命令

以下命令用来创建一个新的分支。

git checkout -b develop
# 与git branch develop等价

创建完后执行git status会告诉你现在处于新的develop的分支,执行git branch可以看到仓库中存在的所有分支。在新的分支下你可以正常地编辑代码和提交,而不会影响到其他分支上的代码。如果你想切换回主分支,直接git checkout master即可。

上一节介绍了一些临时性分支,往往使用完毕后要删除,命令如下:

git branch -d fixbug-somebug
# -d参数只能删除那些已经被当前分支合并的分支,如果要强制删除未被合并的分支,使用-D参数

有分支就会有合并,否则分支就失去了它的意义。下面的例子先切换到master,然后把develop分支的改动合并到主分支上:

git checkout master
git merge develop

Git的合并功能非常强大,很多情况下它都会非常智能地帮你自动合并分支的代码,但是有时候也会出现冲突(conflict)而无法自动合并的情况,这时就需要我们手动来解决冲突。现在假设你在合并代码的时候,hello.py这个文件出现了冲突,该文件冲突的地方大概会是下面这个样子:

<<<<<<< HEAD
print 'This is the HEAD of master branch'

=======
print 'The conflict code of develop branch'
>>>>>>> master

Git会自动给冲突的代码填上一些看上去和你奇怪的标记表明两个分支上代码的差异,手动解决冲突只要保留你想要的代码(也可以修改它们),然后删除多余的“<<<<”“====”“>>>>”行即可。最后记得要commit一下提交你的修改,如果出现“fixed conflict”字样就表明冲突已解决。

如果是少量的代码冲突用手动解决还好,当碰上大量的代码冲突时,直接在源文件中看有点力不从心了。如果你安装了Git Tortoise这个软件,就可以使用里面的图形化冲突解决工具,左右两个窗口对比着来解决冲突。当然也可以使用其他的第三方工具来合并。

3. 变基简介

Git还提供了一种合并功能,称为变基(rebase),它和上面提到的合并(merge)非常相似。下面以一个例子来讲解它们的区别:假设现在本地repo处于master分支,远程repo处于origin/master分支,它们的分支路线图如下:

      D---E master
     /
A---B---C---F origin/master

使用merge合并后,路线分支图如下:

      D--------E  
     /           \
A---B---C-----F---G   master, origin/master

而使用进行变基合并后,路线分支图如下:

A---B---C---F---D'---E'   master, origin/master

也就是说,使用rebase来合并分支可以让你的分支路线图看起来只有一条主线。此外,本地的分支之间也可以使用变基合并,比如把fixbug分支变基到master分支,只需要:

git checkout master
git rebase fixbug

拉取pull预设的行为是将远程的repo与本地的repo合并,如果本地的branch和远端的branch会同步得非常频繁(几乎是完全同步),这时候就会发现pull下来经常会冲突,然后用merge就会造成路线图变得很复杂,难以看到开发的主线,就像下图所示。这时候就会推荐使用git pull --rebase来进行变基拉取。

复杂的分支路线图

在rebase的过程中,也许会出现冲突,Git会停止rebase并会让你去解决冲突。解决完冲突后,用git-add命令去更新这些内容的,只要执行以下语句(不需要提交commit)。

git rebase --continue

这样git会继续应用余下的补丁。在任何时候,你可以用git rebase --abort来终止rebase的行动,并且分支会回退到rebase开始前的状态。

虽然rebase可以让路线图看起来非常干净,但使用它还是具有一定风险的!要记住:一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行rebase操作。在进行rebase的时候,实际上抛弃了一些现存的提交对象而创造了一些类似但不同的新的提交对象。如果你把原来分支中的提交对象发布出去,并且其他人更新下载后在其基础上开展工作,而稍后你又用git rebase抛弃这些提交对象,把新的重演后的提交对象发布出去的话,你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容时,提交历史就会变得一团糟。

总而言之,如果把衍合当成一种在推送之前清理提交历史的手段,而且仅仅rebase那些尚未公开的提交对象,就没问题。如果rebase那些已经公开的提交对象,并且已经有人基于这些提交对象开展了后续开发工作的话,就会出现叫人沮丧的麻烦。(参考此处麻烦的例子

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器