如果您以前用过 Subversion, Mercurial 会让你感觉很困惑. 这篇教程将介绍 Mercurial 工作方式最大的区别. 如果你从没接触过 Subversion, 可以直接跳过本章.

重新审视 Subversion

我们公司的程序员们决定用 Mercurial 替换掉 Subversion, 老兄, 你把我搞糊涂了.

最初, 我提出了各种愚蠢的观点来反对迁移. “我们应当把版本库放在一个中央服务器上, 这样会更安全些,”. 你知道吗, 我的观点是错误的. 对于 Mercurial, 每个开发人员的硬盘都会有一份完整的版本库副本. 实际上 Mercurial 更安全. 而且几乎每一个 Mercurial 项目组也都会有一个中央版本库. 你可能会情不自禁的对中央库进行备份, 或者任何其它应 IT 部门要求的操作.

“分布式版本控制的问题是太容易创建分支了, 而分支是最容易出问题的”, 这个观点同样是错误的. Subversion 的分支容易出问题, 主要是因为 Subversion 没有提供足够多的信息来保证分支合并的正常工作. 对于 Mercurial, 分支合并是一件很轻松惬意的事, 所以创建分支是很稀松平常的, 而且无毒副作用.

最后我说 “好吧, 我会看看, 但别指望我能搞定.” 我叫 Jacob 做了张小抄, 列出了常用的 Subversion 操作, 以及在 Mercurial 中的相对应操作.

但我不会给你看这个列表, 这张列表让我迷糊了好几个月.

这件事说明, 如果你以前用过 Subversion, 那你的脑袋会有些转不过弯. 你需要一些反思来更新自己的观念. 在长达六个月的时间内, 我一直认为 Mercurial 要比 Subversion 复杂得多. 但事实上是我没有真正搞明白 Mercurial 到底是如何工作的, 最终我弄清楚了, 发现原来一切是如此的简单!

在这份教程里我将尽量不用 Subversion 来做参照, 因为这除了使你觉得头痛外没有任何意义. 这个世界上让人头痛的事已经够多了. 相反, 对于 Subversion 的用户, 我会在本章节让他们尽量忘记 Subversion, 把大脑清空以便重头学习 Mercurial.

如果你从来没有接触过 Subversion, 你可以直接跳到下一章节, 你不会遗漏任何内容的.

准备好了没? 让我们来个简单的问答吧.

问题 1. 你可以在第一遍的时候就写出完美的代码?

如果你的回答是 “Yes”, 那你绝对是个骗子.

新写好的代码总是充满了 bug. 你需要花些时间调试后才能保证它正常工作. 在此期间, 它可能会影响团队内其他开发人员的工作热情.

这是 Subversion 的传统做法:

  • 在你 checkin 新的代码, 每个开发人员都获取这份新代码.

由于你新写的代码中存在 bug, 你会有如下选择.

  • 你可以 checkin 有 bug 的代码, 然后把所有人搞疯, 或者
  • 在做好充分调试前不要进行 checkin 操作

Subversion 总是给你出这样可恶的难题. 要么库里的代码满是 bug, 要么新写的代码都放在本地.

作为 Subversion 的用户, 我们早已对此习以为常, 以至于很难想象另外一番景象.

使用 Subversion 时, 组员们经常数天甚至数星期都不 checkin 任何东西. 新手们总会担心在代码 checkin 后会导致程序无法编译, 或者让 Mike (高级工程师) 窝火. Mike 有一次因为无法编译而出离愤怒, 他冲进实习生的小隔间, 把掀翻他办公桌上的所有东东, 然后吼道 “这是你最后一天!” (虽然没被炒, 但可怜的实习生差点没湿了裤子.)

因为害怕数周都不 checkin 代码, 使版本控制无法发挥其应有的作用. 最终他们还是需要有经验的开发人员来协助完成代码的 checkin. 如果你根本就不去用, 那版本控制还有什么意义?

下面的图展示了 Subversion 是如何被使用的:

../_images/00-svn.png

对于 Mercurial, 每个开发人员在自己 PC 上有自己的版本库:

../_images/00-hg.png

只要你愿意, 你可以随时提交提交代码到你的私人版本库, 从版本控制中获益. 每当你的代码到达一定的逻辑点, 你都可以将代码提交.

等到代码比较稳定后, 你想让其他人使用你的新代码, 你可以将你所做的变更 push 到中央版本库. 其他人从中央版本库 pull 代码后, 就可以看到你的代码了.

提交新代码影响他人工作的操作, Mercurial 将这两者分割成两个步骤.

这意味着你提交后 (hg com) 不会影响到其他人. 等到你完成一系列修改, 代码相对稳定后你可以将变更 push (hg push) 到中央版本控制库.

另一个理念上的重大分歧

你确定每条街道都有名字?

好吧, 其实在日本, 没几条有名字的街. 他们通常只给街道两边的 街区 进行编号, 只有非常非常重要的街道才会有自己的名字.

../_images/00-tokyo.png

这就如 Subversion 和 Mercurial 之间的区别.

Subversion 采用 revisions (修订号) 的方式. 修订号看起来就像是某个时间点整个文件系统的一个快照.

Mercurial 则采用 changesets (变更集) 的方式. 变更集是版本与版本之间变更情况的简明列表.

这不是半斤和八两吗, 有啥区别?

区别在这里. 试想一下, 我和你在修改同一份代码, 我们分别建立的不同的分支并独立工作. 期间我们都分别各自做了大量的改动, 两份代码基本上分道扬镳了.

等到我们需要合并的时候, Subversion 尝试分别查看我们俩的修订版本, 然后尝试将两个版本合并到一起. 文件需要如何合并, 多半是靠猜. 所以失败也是很正常的. Subversion 会给出大量的合并冲突 (merge conflicts), 而这些冲突并不是真正存在的, 真正的问题是 Subversion 并没有搞清楚我们具体做了什么操作.

与之相对应, 我们在各自使用 Mercurial 时, Mercurial 频繁的 维护一系列变更集. 等到我们需要合并代码的时候 Mercurial 有足够多的信息用来合并: 它知道 我们分别做了哪些修改, 而且可以 重新应用这些变更, 而不是仅仅看看最终版本, 然后瞎蒙如何把它们撮合起来.

举个例子吧, 如果我小小修改了一个函数, 然后将这个函数挪了个位置. Subversion 不会记录你的操作步骤, 所以等到合并的时候, 它会认为这是一个新的函数. Mercurial 则会分别记录函数修改和函数移动的操作, 这意味着. 如果你也同时对该函数做了修改, Mercurial 更有可能成功的将我们俩的改动合并起来.

由于 Mercurial 把所有事情都当作变更集 (changesets) 处理, 你可以借此来做一些有趣的事情. 你可以将它们 push 给你团队中的朋友试用, 而不是 push 到中央库摧残所有组员.

或许你现在会感觉有些迷惑, 不过不要担心 – 等你看完本教程后自然会有清楚的认识. 至于现在, 你只要清楚 Mercurial 是以 “变更集” (changesets) 而不是 “修订号” (revisions) 的方式处理, 这使得代码合并方面比 Subversion 更要更加容易.

这意味着你可以随心所欲的拉出新分支, 因为合并将不再是噩梦一场.

想知道一些更有趣的事情? 几乎每个我接触使用 Subversion 的项目组都同我谈起过类似的问题. 这个问题实在是太普遍了, 我把她命名为 “Subversion 案例 #1”. 案例如下: 在某个阶段, 他们需要为客户创建一个独立于主干的开发分支. 这通常是一个很自然而然的做法, 但等到需要进行分支合并的时候, 这一切演变成了一场噩梦. 本是应是几分钟内完成的事情, 最终让六个程序员花了两周来修复稳定分支合并到开发分支所产生的 bug.

几乎每个 Subversion 团队都告诉我, 他们再也不想经历一次类似的事情了, 同时他们发誓将远离分支. 现在的他们这么干: 所有的新需求都写在一个巨大的 #ifdef 条件句块中. 因此他们可以只用一个 主干``trunk`` 就搞定. 在完成调试前, 用户看不到任何新代码引入的功能. 坦率的说, 这实在是一个非常可笑的做法.

将开发版本和稳定版本隔离开, 正是源码版本控制的建议做法.

当切换到 Mercurial, 你可能没有意识到, 分支已经变得可行, 你不必再为使用分支而感到恐惧.

这意味着你可以针对新需求组建一个小开发团队协作开发, 等到他们完成后, 再将这个开发库合并到中央开发库, 这是完全可行的!

这意味着你可以创一个 QA 库. 让 QA 团队试用里面的代码. 如果能正常工作, QA 则将代码 push 到中央库, 这样保证了中央库里总是稳定的, 经过测试的代码. 而且这是完全可行的!

这意味着你可以在各个相互独立的库中进行调试, 如果代码正常工作, 你可以将他们合并到主库, 如果出现问题, 你可以直接丢弃它们, 这也是完全可行的!

最后一个理念上的重大分歧

Subversion 与 Mercurial 之间的这个分歧或许并不是什么大不了的事. 但如果你不了解它的话, 很可能会因此栽跟头, 问题是这样的:

Subversion 的版本控制是基于 文件 的. 但是 Mercurial 的版本控制是作用于整个目录 – 包括所有的子目录.

在 Subversion 中你可以通过以下方法注意到这一点. 你在子文件夹进行 commit 操作的时候, 只会对当前目录和当前目录的所有子目录生效, 这样很可能导致你忘记提交其它目录的变更. 相反, Mercurial 的所有命令都是针对整个库的. 如果你的代码放在 c:/code, 不管你是在 c:/code 或是任何 c:/code 的子目录下执行 hg commit 命令, 效果都是一样的.

这个问题没什么大不了的, 但是如果你习惯让整个公司共用一个巨无霸的版本控制库, 然后让每个人在各自的目录下工作, 这对 Mercurial 并不是一个合理的使用方式 – 你最好让每个项目都有自己独立的库.

最后...

你可以相信我下面所说的每一句话.

Mercurial 比 Subversion 更好.

对个人和团队而言, 这都是一个更好的代码管理方式.

Mercurial 确实 更好.

记住我的话, 去理解 Mercurial 的工作方式, 以 Mercurial 的方式去使用 Mercurial, 不要试图去抗拒, 不要试图以 Subversion 的老方法去使用 Mercurial. 总之, 适应 Mercurial 的工作方式, 将会使你的工作变得愉快, 有成就感, 而且舒适.

在初期, 你可能会感受到诱惑 (我知道你会的), 然后非常想抛弃 Mercurial, 重返 Subversion 的温暖怀抱, 它是如此陌生, 就像生活在陌生的国度, 接着你就犯了思乡病, 然后你会找出各种各样的理由支撑这个观点, 比如, Mercurial 工作目录太费硬盘空间, 事实上, 这是瞎扯, 实际上 Mercurial 比 Subversion 还更省空间. (这是真的!)

然后你会继续走到 Subversion 的老路上去, 因为你试图使用 Subversion 的方式去创建分支, 但使你困惑的是, 这似乎不太好用, 因为你真正需要的是 Mercurial 的分支管理方式, 通过 clone 一个新的库, 不要尝试将 Subversion 里的经验套用到 Mercurial, 去学习 Mercurial 的工作方式, 然后一切会变得 如此简便, 相信我.

接着你会找人给做张 “Subversion 和 Mercurial 操作对照表”, 你会花三个月去想 hg fetchsvn update 其实没啥区别, 却从来没有想过 hg fetch 具体是做什么的, 如果有一天出问题了, 你会责怪 Mercurial, 而真正应当责怪的是你自己并没有理解 Mercurial 的工作原理.

我想你会这么干, 因为我当初就这么干来着.

不要再犯相同的错误了. 学习 Mercurial, 信任 Mercurial, 找到适合 Mercurial 的工作方式, 你将走在源码控制整个时代的前列. 当你的竞争对手们为了解决分支合并所带来的冲突忙得焦头烂额的时候, 你输入 hg merge 然后得意的说: “太棒了, 顺利搞定.” 然后 Mike 冷静下来, 和实习生出去抽一根 (译注: 原文是美俚 “大麻烟”), 一切其乐融融, 生活多美好.