谈谈分支与合并

branch
对于开发人员来说,经常会碰到这样的难题:某一个特性A正在开发之中,突然有了新的需求特性B,或者有紧急的bug C需要修改。如果B或C的重要性排在A之后,那么没有问题,等A开发完上线之后再来开发B或者C就可以了。但是通常情况是B和C的重要性要高于A。这种情况下,已经开发了一半的A特性的代码该何去何从?

branch_simple
代码目录拷贝

最简单的办法是把当前A特性的代码拷贝一份出来,放到别的目录下面,然后再修改原来的代码以支持B特性或者解决C bug,等这个版本改完后再把之前拷贝出来的A特性代码合并回去。但是拷贝出来的这份代码是不受版本控制的,中间的任何改动都没有记录,与使用版本控制的思想背道而驰。另外合并代码是个非常关键的操作,如果只有一个分支,手工合并问题也不大。但如果分支一多,就容易犯错了。

另一种办法就是使用版本控制软件自身的分支功能。

SVN branch

SVN的分支功能比较鸡肋,与上面的第一种办法相比好不到哪里去。使用svn的代码库下面会有trunk和branch这两个目录,trunk下的代码是主干版本,branch下则是分支版本。创建一个新分支就是在branch下面拷贝一份代码而已。与上面的方法相比,唯一的一点进步就是每次改动可以提交到代码库了。但是这种方式仍然非常的笨重,每次都要把代码拷贝一份到branch目录下。
branch_svn
在分支的处理上,SVN确实比较差。可能在设计之初就没有考虑到多分支并行的情况。所以SVN正确的使用方式就是线性的提交。更多的讨论可以参考stackoverflow上的这个回答:So why did Subversion merges suck?

Git branch

接下来再看看Git的分支是如何工作的。Git中的分支与svn的分支从概念上有着天壤之别。svn中的分支只是代码的拷贝,git中的分支本质上是指向某次commit的指针。如下图中一共有master, feature_b, bug_c三个分支,分别指向不同的提交。
branch_git

这样设计的好处是分支的操作灵活了很多,创建新分支只要增加一个指针就完成了。比起复制整个代码库快了很多。另外git切换分支的操作是在当前工作目录进行的,不需要像SVN那样单独建一个branch目录。切换分支非常方便。

Git与SVN一个关键的区别在于SVN提交历史的数据结构是树形结构,一个节点只能有一个父节点。而Git是有向无环图(DAG),一个节点可以有多个父节点。

数据结构差异
数据结构差异

三方合并

DAG这样的数据结构让Git可以追踪每一个分支的来源,所以Git在合并分支时可以实现自动的三方合并,而SVN只能是两方合并(或者需要人工介入的三方合并)。

假设有一个函数,分支1在函数开始加了一句log,分支2在函数末尾加了一句log。如果使用两方合并,是无法自动完成合并的。因为它并不知道要合并成什么样子,是应该保留第一行的log,还是保留最后一行?这种情况就会需要人工介入来解决冲突。如果类似的改动很多,那么合并的效率是非常低下的,而且容易出错。

branch_2ways

Git在做分支合并操作时,会根据提交历史的DAG计算出两个分支最近的公共祖先,然后根据公共祖先和两个分支做一次三方合并。这种合并方式大大减少了人工介入,减轻了合并的工作量,保证了合并分支的准确性。
branch_3ways

Git与SVN协作

Git在分支处理上有诸多优点,但很多时候整个团队采用的版本控制是SVN,这种情况下有没有办法享受到Git的各种优点呢?办法总是有的,只不过会有所牺牲,无法做到跟原生使用Git一样。

一种办法是使用git svn命令。官方文档里面是这么描述的:

Git 最为重要的特性之一是名为 git svn 的 Subversion 双向桥接工具。该工具把 Git 变成了 Subversion 服务的客户端,从而让你在本地享受到 Git 所有的功能,而后直接向 Subversion 服务器推送内容,仿佛在本地使用了 Subversion 客户端。也就是说,在其他人忍受古董的同时,你可以在本地享受分支合并,使暂存区域,衍合以及 单项挑拣等等。这是个让 Git 偷偷潜入合作开发环境的好东西,在帮助你的开发同伴们提高效率的同时,它还能帮你劝说团队让整个项目框架转向对 Git 的支持。这个 Subversion 之桥是通向分布式版本控制系统(DVCS, Distributed VCS )世界的神奇隧道。

非常让人激动是不是?好像是一个非常完美的解决方案。不过文档后面又写道:

习惯了 Git 的工作流程以后,你可能会创建一些特性分支,完成相关的开发工作,然后合并他们。如果要用 git svn 向 Subversion 推送内容,那么最好是每次用衍合来并入一个单一分支,而不是直接合并。使用衍合的原因是 Subversion 只有一个线性的历史而不像 Git 那样处理合并,所以 git svn 在把快照转换为 Subversion 的 commit 时只能包含第一个祖先。

git svn推荐的工作流程是使用衍合来进行分支的合并。我不太喜欢这种方法,最重要的原因就是通过衍合这种方式会修改提交历史,将本来是有向无环图的结构变成了线性的,那就跟svn没什么两样了。

除了git svn命令,还有一种方式。建立一个git repo,然后将svn当前最新版本的代码拷贝到git repo中。之后的开发工作都在git中进行,想拉分支就拉分支,想推送到远程仓库就推送。一个特性开发完成之后,把特性分支合并进master分支。然后把git中的master分支看作是SVN主干分支的镜像,每次git的master分支有进展时,把代码同步提交到SVN即可。

branch_co

这种方式的缺点是需要手工保持两个仓库的主分支同步,但换来了极大的便利性。你可以在这个Git repo里面随意施展你的技能,只要不把master分支玩坏就可以了。比如可以推送到远程仓库,建立多人协作开发的流程,这在之前的方法里面是无法做到的。

我喜欢用SourceTree作为Git客户端,图形界面优雅,支持多平台。最迷人的特性就是图形化的提交历史,每个分支是怎么来的,最后合并到哪里,这些信息都一目了然。
git-graph

Referrence

  1. Three-Way Merging: A Look Under the Hood
  2. Git 分支 – 何谓分支
  3. Git 与其他系统 – Git 与 Subversion

《谈谈分支与合并》有3个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注