第 6 章 用 Mercurial 管理源码
Mercurial 是我们公司标配的版本控制系统,公司的日常业务大多离不开它。
除前面几章提到的提交、push、pull、创建分支等基本操作之外,Mercurial 还具备许多功能。灵活运用这些功能能帮助我们在各种情况下顺利完成开发。
本章将重点讲解 Mercurial 的使用窍门,包括一些好用的功能、版本库的管理方法、各种工具等,帮助各位进一步熟练使用 Mercurial。
6.1 Mercurial 版本库的管理与设置
Mercurial 是分布式版本管理系统,所以原则上是不需要 Subversion 那种中央版本库的。但是,开发团队成员之间共享成果(变更集)时,如果仅凭对等(Peer To Peer)方式进行交流,效果是十分有限的。为解决这一问题,需要在所有开发成员都能随时访问的服务器上设置一个中央版本库,用它来共享成果。接下来将要介绍的,就是在服务器上设置中央版本库的步骤。
6.1.1 服务器上的 Uinx 用户群设置
要想对文件系统上的 Mercurial 版本库进行操作,必须能在对象版本库内的“.hg”目录下进行读写,这就需要相应的访问权限。因此,用版本库进行开发的 Unix 用户需要设置为同一用户群来集中管理。
我们创建名为 dev 的群作为本例的开发者群(LIST 6.1)。这需要以管理员权限执行 groupadd
命令。
LIST 6.1 创建 dev 群
$ sudo groupadd dev
然后用 useradd
命令给 dev 群新建用户,用户名指定方法如 LIST 6.2 所示。
LIST 6.2 新建 dev 群的用户
$ sudo useradd -g dev <username>
即通过
指定用户名。
NOTE
指定 -m 选项可以创建主目录。
指定 -s 选项可以设置登录时使用的shell。
另外,向群中添加已有用户时要用 usermod
命令(LIST 6.3)。
LIST 6.3 向 dev 群添加已有用户
$ sudo usermod -a -G dev <username>
NOTE
用户群的修改内容不会对已登录服务器的用户立刻生效。直到下次登录时才会应用新的群。
6.1.2 创建版本库
由于服务器上要放好几个版本库,所以我们新建一个统一存放版本库的目录。本例的路径为 varhg/(LIST 6.4)。各版本库要在这个目录下创建。
LIST 6.4 创建 varhg/ 目录
$ sudo mkdir varhg/
接下来我们在这个目录下新建一个版本库。执行 hg init
命令,新建名为 testrepo 的版本库(LIST 6.5)。
LIST 6.5 创建 testrepo 版本库
$ sudo hg init varhg/testrepo
然后把版本库内“.hg”目录的所有权交给 dev 群,让 dev 群的用户能够对该版本库进行写入操作(LIST 6.6)。
LIST 6.6 将版本库的群改为 dev
$ sudo chgrp -R dev varhg/testrepo/.hg
另外还需要设置 SGID(Set Group ID),保证向版本库写入时,文件的所有者是 dev 群(LIST 6.7)。
LIST 6.7 设置 SGID
$ sudo chmod g+sw -R varhg/testrepo/.hg
这样我们就有了一个可多用户共同使用的版本库。
6.1.3 hgrc 的设置
要想在服务器端执行钩子(Hook)脚本,必须使用可信群或可信用户(关于钩子脚本的相关知识将在 6.2 节学习)。我们在 hgrc 的 trusted 节的 groups 中添加 dev 群。如果该文件不存在,则直接新建。至于 hgrc 的路径,假如版本库路径为 varhg/testrepo/,则 hgrc 位于 varhg/testrepo/.hg/hgrc。clone 来的版本库会自动生成这个文件,而用 init 创建的版本库并不会自动生成,需要手动创建(LIST 6.8)。
LIST 6.8 在 hgrc 的 trusted 节的 groups 中添加 dev 群
[trusted]
groups = dev
6.1.4 使用设置好的版本库
如果想通过 ssh 来 clone 我们刚创建的这个版本库,需要以“ssh:// 主机名 / 目录”的形式指定 clone 位置的路径。举个例子,假设服务器可通过 example.com 域名进行访问,现在我们想 clone 路径 varhg/testrepo 下的版本库,则命令如 LIST 6.9 所示。
LIST 6.9 对 example.com 上的 varhg/testrepo 版本库执行 clone 操作
$ hg clone ssh://example.com/varhg/testrepo
指定路径时请注意斜杠数。协议部分 ssh://
、域名 example.com
与目录 varhg/testrepo
的结合部分都是双斜杠。
NOTE
通过 ssh 连接时如果需要密钥,就需要在 ssh 客户端的设置文件 $HOME/.ssh/config 中预先设置好对连接对象使用的密钥文件。
6.1.5 使用 hgweb 建立简易中央版本库
Mercurial 具有 hgweb 功能,我们只需一个 hg serve
命令就能公开版本库浏览器的 Web 站点。
$ hg serve
启用 hgweb 功能的版本库默认允许 clone/pull,在 hgrc 中添加设置之后还可以进行 push 操作,所以对于一些仅需要在 LAN 内共享的版本库来说,启用 hgweb 功能后就可以当作中央版本库来用,能省去不少功夫。
我们在“.hg/hgrc”中添加以下设置。allow_push 是允许经由 hgweb 进行 push 操作的 user 列表,加上 * 表示不需认证即可 push。push_ssl 用来设置 push 时是否需要 SSL。
[web]
allow_push = *
push_ssl = False
这里不妨加上 -d 选项,让 hgweb 的 Web 服务器常驻。启动之后会显示 hgweb 的 URL。
$ hg serve -d
listening at http://127.0.0.1:8000/ (bound to *:8000)
如果直接执行 hg clone 加 URL,本地版本库名会变成 domain:port,所以我们在 clone 时添加版本库名作为第二传值参数。
$ hg clone http://127.0.0.1:8000/ monjudoh
如下所示,我们在 clone 来的版本库中进行修改,然后提交并 push。
$ cd monjudoh
$ touch monjudoh.txt
$ hg ci -A -m "add monjudoh.txt"
adding monjudoh.txt
$ hg push
pushing to http://127.0.0.1:8000/
searching for changes
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 1 changesets with 1 changes to 1 files
6.2 灵活使用“钩子”
Mercurial 有钩子功能。所谓钩子功能,就是指 Mercurial 在执行特定处理时还能够额外执行其他处理的功能,而且这个额外处理是任意的。比如借助这个功能,我们可以在 push 时发送邮件通知,或者在提交之前自动检测提交是否有疏漏等。
本节将通过几个实际的例子,教各位灵活运用 Mercurial 的钩子功能。
6.2.1 钩子功能的设置方法
钩子功能的设置可通过在 hgrc 的 [hooks] 节下指定 shell 命令或 Python 函数来进行。想让哪个版本库执行钩子功能就在哪个版本库的 hgrc 中进行指定。比如在 6.2.2 节,我们想利用钩子功能在提交时检查编码风格,那么就在本地版本库的 hgrc 中进行指定;又比如在 6.2.5 节,我们想通过钩子功能禁止中央版本库出现多头现象,那么就要在中央版本库的 hgrc 中进行指定。
[hooks]
commit = echo commit;
update = python:myhook.sample
如果在一个钩子事件中指定了多个钩子脚本,那么将执行最后一个。
[hooks]
update = python:myhook.sample1
update = python:myhook.sample2 # 这个钩子事件会被执行
指定为空时不会执行任何操作。我们可以利用空指定覆盖已有操作,从而避免该操作被执行。
[hooks]
update = python:myhook.sample1
update = # update 钩子事件不会被执行
在钩子事件名后面加上后缀,就能让同一个钩子事件执行多个钩子脚本了。
[hooks]
update.sample1 = python:myhook.sample1
update.sample2 = python:myhook.sample2
6.2.2 尝试钩子脚本
如今网络上有许多已公开的钩子脚本,这里我们以“提交时检验 PEP8 编码风格”的脚本为例进行导入。该脚本包含在名为 hghooks1 的钩子脚本集之中。
1https://pypi.python.org/pypi/hghooks/
这个 hghooks 可通过 pip 安装。
$ sudo pip install hghooks
安装完成后,只要在 hooks 中添加以下设置即可开始使用。
[hooks]
pretxncommit.pep8 = python:hghooks.code.pep8hook
6.2.3 钩子事件
钩子事件种类繁多,下面我们对应命令的执行类型,来了解一下其中的一部分。给版本库设置钩子功能时,要看对应命令是否会给版本库带来变更,这两种情况下所用的钩子事件是不一样的。
不会给当前操作版本库带来变更的命令有 update、从当前版本库 push、被其他版本库 pull 等。
会给当前操作版本库带来变更的命令有 commit、tag、从当前版本库 pull、被其他版本库 push 等。
接下来,我们对应着命令来了解一些已定义的钩子事件。其中,changegroup 表示 changeset 群被拿到版本库时的事件。另外,对 changeset 群中各个 changeset 执行的是 incoming。
◉ update 时
当前操作版本库的变更集不会被变更
① preupdate
② update
◉ 提交时
当前操作版本库的变更集会被变更
① precommit
② pretxncommit
③ commit
◉ 从当前版本库 push/ 被其他版本库 pull 时
向其他版本库发送变更集群的操作。当前操作版本库的变更集不会被变更
① preoutgoing
② outgoing
◉ 向当前版本库 pull/ 被其他版本库 push 时
接受其他版本库的变更集群的操作。当前操作版本库的变更集会被变更
① prechangegroup
② pretxnchangegroup
③ changegroup
④ incoming
我们将对版本库 A 执行各操作后的动作汇总成了下表。
执行的命令 | 版本库 A 发生的事件 | 版本库 B 发生的事件 |
---|---|---|
hg update | preupdate, update | 无 |
hg commit | precommit, pretxncommit, commit | 无 |
hg push B | preoutgoing, outgoing | prechangegroup, pretxnchangegroup, incoming |
hg pull B | prechangegroup, pretxnchangegroup, incoming | preoutgoing, outgoing |
可以看到,对于部分会给当前操作版本库变更集带来变更的命令,其执行时发生的事件名带有 pretxn 前缀。这是因为这些变更是事务性的,钩子事件必须发生在变更处理已结束且事务尚未确定的时间点。
6.2.4 钩子功能的执行时机
接下来详细了解一下钩子功能。
我们看到,版本库发生变更时,有些钩子事件带有 pre 前缀,有些却没有。有 pre 前缀的钩子事件位于执行命令指定的处理之前,没有前缀的则位于执行命令指定的处理之后。pre 可以通过 exit 1 在命令指定的处理被执行之前直接中止命令。
比如我们把 usrbin/false 加入 preupdate。
[hooks]
preupdate = echo preupdate;hg parents;usrbin/false;
update = echo update;hg parents;
如下所示,如果工作目录的 parent 处于 tip(最新提交)的前一个状态,那么 hg update tip
命令在执行时将被 preupdate 打断。结果就是 update 并没有执行,工作目录的 parent 仍保持原样。
$ hg log -l2
changeset: 12:45648f583d32
tag: tip
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 14:03:11 2011 +0900
changeset: 11:2b1a3a2e6e29
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 14:02:55 2011 +0900
$ hg parents
changeset: 11:2b1a3a2e6e29
user: onjudoh <monjudoh@gmail.com>
date: ri Dec 02 14:02:55 2011 +0900
$ hg update tip
preupdate
changeset: 11:2b1a3a2e6e29
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 14:02:55 2011 +0900
abort: preupdate hook exited with status 1
$ hg parents
changeset: 11:2b1a3a2e6e29
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 14:02:55 2011 +0900
版本库发生变更时,钩子事件有 3 种:有 pre 前缀的、有 pretxn 前缀的、没有前缀的。pre 位于命令指定的处理被执行之前,pretxn 位于处理实际执行之后且事务完成之前,无后缀的位于事务完成之后。
比如,我们这里设置 precommit、pretxncommit、commit 这 3 个钩子事件来执行 hg tip
。然后执行 hg commit
会发现,从 pretxncommit 起 tip 就被更改了。
[hooks]
precommit = echo precommit;hg tip;
pretxncommit = echo pretxncommit $HG_NODE;hg tip;
commit = echo commit $HG_NODE;hg tip;
$ hg ci -A -m "commit successful"
precommit
changeset: 28:8a68c4364175
tag: tip
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 15:26:47 2011 +0900
pretxncommit d3000a3cea812e687f2f6bac7aaa2716be003cab
changeset: 29:d3000a3cea81
tag: tip
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 15:26:58 2011 +0900
summary: commit successful
commit d3000a3cea812e687f2f6bac7aaa2716be003cab
changeset: 29:d3000a3cea81
tag: tip
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 15:26:58 2011 +0900
summary: commit successful
不过,由于 pretxn 位于事务完成之前,所以在 pretxn 中用 exit 1 可以实现回滚。现在我们把 usrbin/false 加入 pretxncommit 中试试看。
[hooks]
precommit = echo precommit;hg tip;
pretxncommit = echo pretxncommit $HG_NODE;hg tip;usrbin/false;
commit = echo commit $HG_NODE;hg tip;
$ hg parents
changeset: 29:d3000a3cea81
tag: tip
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 15:26:58 2011 +0900
summary: commit successful
$ hg ci -m "commit failed"
precommit
changeset: 29:d3000a3cea81
tag: tip
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 15:26:58 2011 +0900
summary: commit successful
pretxncommit c42df85f2e7632b6e7ec124011b11ad8a9a3d61f
changeset: 30:c42df85f2e76
tag: tip
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 15:36:52 2011 +0900
summary: commit failed
transaction abort!
rollback completed
abort: pretxncommit hook exited with status 1
$ hg parents
changeset: 29:d3000a3cea81
tag: tip
user: monjudoh <monjudoh@gmail.com>
date: Fri Dec 02 15:26:58 2011 +0900
summary: commit successful
可以看到,pretxncommit 时反映出的提交已经被撤销。灵活使用 pretxncommit、pretxnchangegroup 能帮助我们验证提交的结果或 push 的结果是否有问题,并且在有问题时实现回滚。
6.2.5 编写钩子脚本
◉ 用shell 脚本实现钩子脚本
我们以禁止中央版本库出现多头现象的钩子脚本为例,来了解一下用 shell 脚本实现的钩子脚本。首先如下所示,将脚本放到任意位置(“.hg”之下就不错)。
#!/bin/bash
# forceonehead
# add the following to <repository>/.hg/hgrc :
# [hooks]
# pretxnchangegroup.forceonehead = pathto/forceonehead
if [ $(hg heads --template "{branch}\n"|sort|uniq|wc -l) != $(hg heads --template "{branch}\n"|sort|wc -l) ]; then
echo "There are multiple heads."
echo "Please 'hg pull' and get your repository up to date first."
echo "Also, don't 'hg push --force' because that won't work either."
exit 1
fi
接下来,在 hgrc 中将其指定为 pretxnchangegroup 的钩子脚本。具体方法如下。
[hooks]
pretxnchangegroup.forceonehead = .hg/forceonehead
这个脚本被指定给了 pretxnchangegroup,所以它的执行时机位于 push 处理执行之后且事务完成之前。因此我们可以在 push 完毕的状态下执行各种 Mercurial 命令。这里我们利用 heads 命令检查是否出现多头现象,如果出现则通过 exit 1 回滚。这就是带 pretxn 前缀的钩子事件的用法之一。
在 Mercurial 上禁止中央版本库出现多头现象
◉ 用 Python 脚本实现钩子脚本
这里我们讲一个用 Python 脚本实现钩子脚本的例子,即在每次提交时都对第 2 章中完成的留言板发送一次 diff(LIST 6.10)。
LIST 6.10 myhook.py
# coding: utf-8
import logging
import urllib
# 接收方URL
POST_URL = 'http://127.0.0.1:8000/post'
# 发送信息的格式
MESSAGE_FORMAT = """%(description)s
%(diff)s"""
def http_post(url, data):
""" 用POST 方法向url 发送data
"""
fp = urllib.urlopen(url, urllib.urlencode(data))
return fp.read()
def postdiff(ui, repo, hooktype, node=None, source=None, **kwargs):
""" 将差别用HTTP 进行POST 的钩子函数
"""
# 获取提交的上下文对象
context = repo['tip']
# 从上下文中获取差别列表(由于是迭代器对象,所以要列表化)
diff_list = list(context.diff())
# 将差别结合成文本
text_diff = ''.join(diff_list)
# 获取用户
user = context.user()
# 获取概要
description = context.description()
# 生成要发送的信息
message = MESSAGE_FORMAT % {'description': description, 'diff': text_diff}
# 生成发送数据的字典
data = {
'name': user,
'comment': message,
}
# 发送
http_post(POST_URL, data)
将 myhook.py 放到 PYTHONPATH 的影响范围之下,然后在 hgrc 中作如下描述。
[hooks]
commit = python:myhook.postdiff
提交后,会显示如图 6.1 所示的结果。
图 6.1 向留言板发送的提交信息
用 Python 写出的钩子脚本能直接使用 context 对象。当输出结果需要用版本库内数据进行条件判断等加工时,shell 脚本不但要处理输出格式,还必须对字符串进行操作。相对地, Python 脚本只需要从管理各修订版元数据的 context 对象中获取适当信息即可。
另外从管理方面上讲,Python 脚本可以利用 Python 的包管理,用 pip 就可以安装,十分方便。
6.3 分支的操作
下面我们来看看 Mercurial 对分支的操作。所谓分支,是指版本库中独立存在的开发线。分布式版本控制系统的一大优势就在于各个本地环境中的版本库互相独立。相对于 Subversion 等集中式版本控制系统,分布式版本控制系统能更放心地处理分支。
LIST 6.11 hg branch
$ hg branch
default
如 LIST 6.11 所示,默认情况下,版本库中只存在 default 分支。现在我们来创建一个新分支(LIST 6.12)。default 分支是版本库中原本就存在的分支。
LIST 6.12 hg branch(创建分支)
$ hg branch test-branch
$ hg branch
test-branch
分支创建出来之后,我们可以看到当前分支变成了 test-branch。不过对 Mercurial 而言,创建分支的流程到这里还没有结束。只有我们向新分支做过提交之后,这个分支才会具有实体。所以我们先向分支中添加文件,然后再看看效果(LIST 6.13)。
NOTE
Mercurial 允许用户在创建分支后直接进行提交,不必添加或删除文件。
LIST 6.13 向 test-branch 添加文件以及提交
$ touch test2.txt
$ hg add test2.txt
$ hg commit
至此分支创建完毕,我们来查看所有分支(LIST 6.14)。
LIST 6.14 hg branches(查看所有分支)
$ hg branches
test-branch 1:bcbc567db3dd
default 0:74471564b074 (inactive)
不出意外应该能看到 test-branch 和 default 两个分支。接下来我们回到 default 分支。
LIST 6.15 hg update(分支间的切换)
$ hg update default
如 LIST 6.15 所示,用 hg update 能在分支间进行切换。现在我们已经回到了 default 分支。下面来看看分支间合并的相关内容。
6.4 关于合并
利用 Mercurial 等版本控制系统进行多项工作时,必然离不开成果的合并。Mercurial 的 hg merge
命令能自动通过三路合并为我们完成合并工作,但并不是说所有情况下都能正常合并。本节,我们将学习基本的合并流程以及发生冲突时的应对方法。熟练掌握合并的相关技巧,能让各位对版本控制系统更加得心应手。
6.4.1 未发生冲突的合并
多名成员并行开发时会发生多头现象。下面例子中就存在 rev1 和 rev2 两个头。要解决这一问题就必须进行合并。
$ hg log -G --style compact
o 2[tip]:0 c0d244a079ce 2011-11-18 11:40 +0900 tokibito
| tokibito
|
| @ 1 ead8ad7a4d9f 2011-11-16 11:07 +0900 monjudoh
|/ monjudoh
|
o 0 fe75405e6383 2011-11-16 11:05 +0900 monjudoh
first commit
假设现在我们位于 rev1,进行合并要先运行 merge
命令再进行提交。在同一分支内合并多头时,merge
命令不需要加任何传值参数。
$ hg merge
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
$ hg ci -m "merge"
这样一来多头就被合并成了一个头。
$ hg log -G --style compact
@ 3[tip]:1,2 b5f62f57038b 2011-11-18 14:07 +0900 monjudoh
|\ merge
| |
| o 2:0 c0d244a079ce 2011-11-18 11:40 +0900 tokibito
| | tokibito
| |
o | 1 ead8ad7a4d9f 2011-11-16 11:07 +0900 monjudoh
|/ monjudoh
|
o 0 fe75405e6383 2011-11-16 11:05 +0900 monjudoh
first commit
工作中我们也经常会遇到合并两个分支的情况。下面的例子是包含 default 和 tokibito2 两个分支的状态。
2okibito 为本章撰写者冈野真也先生的 Twitter 用户名。——编者注
$ hg log -G
o changeset: 2:a663e1bf8f71
| branch: tokibito
| tag: tip
| parent: 0:fe75405e6383
| user: tokibito
| date: Fri Nov 18 11:40:13 2011 +0900
| summary: tokibito
|
| @ changeset: 1:ead8ad7a4d9f
|/ user: monjudoh <monjudoh@gmail.com>
| date: Wed Nov 16 11:07:59 2011 +0900
| summary: monjudoh
|
o changeset: 0:fe75405e6383
user: monjudoh <monjudoh@gmail.com>
date: Wed Nov 16 11:05:34 2011 +0900
summary: first commit
此时就不能用无传值参数的 merge
命令了。因为分支内不存在多头,无法自动选择合并对象。
$ hg merge
abort: branch 'default' has one head - please merge with an explicit rev
(run 'hg heads' to see all heads)
假设我们位于 default 分支的 rev1,现在只要将合并对象的分支名交给 merge
命令就能完成合并。
$ hg merge tokibito
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
$ hg ci -m "merge"
执行后的合并情况如下。我们是在 default 分支下指定 tokibito 分支执行了 merge
命令,所以被合并的那个修订版现在属于 default 分支。这里千万注意别搞错合并方向。
$ hg log -G
@ changeset: 3:7dffb01df37b
|\ tag: tip
| | parent: 1:ead8ad7a4d9f
| | parent: 2:a663e1bf8f71
| | user: monjudoh <monjudoh@gmail.com>
| | date: Fri Nov 18 14:17:13 2011 +0900
| | summary: merge
| |
| o changeset: 2:a663e1bf8f71
| | branch: tokibito
| | parent: 0:fe75405e6383
| | user: tokibito
| | date: Fri Nov 18 11:40:13 2011 +0900
| | summary: tokibito
| |
o | changeset: 1:ead8ad7a4d9f
|/ user: monjudoh <monjudoh@gmail.com>
| date: Wed Nov 16 11:07:59 2011 +0900
| ummary: monjudoh
|
o changeset: 0:fe75405e6383
user: monjudoh <monjudoh@gmail.com>
date: Wed Nov 16 11:05:34 2011 +0900
summary: first commit
6.4.2 合并时发生冲突以及用文本编辑器解决冲突的方法
虽然大部分情况下合并都能自动完成,然而一旦发生冲突,就需要我们来手动解决问题了。
假设有如下多头情况。
$ hg log -G --style compact
o 2[tip]:0 0dffbb8f7780 2011-11-18 15:09 +0900 monjudoh
| other
|
| @ 1 a239fe812ab0 2011-11-18 15:08 +0900 monjudoh
|/ this
|
o 0 0648c3b5afbd 2011-11-18 15:07 +0900 monjudoh
base
rev0、1、2(base、this、other)中 conflict.txt 的文件内容分别如下所示。
- rev0(base)
A
A
A
A
A
- rev1(this)第 3 行变更为 B
A
A
B
A
A
- rev2(other)第 3 行变更为 C
A
A
C
A
A
然后我们在 rev1(this)执行 merge
命令。
LIST 6.16 执行合并查看冲突
$ hg merge
merging conflict.txt
merge: warning: conflicts during merge
merging conflict.txt failed!
0 files updated, 0 files merged, 0 files removed, 1 files unresolved
use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
不出意外应该会看到如 LIST 6.16 所示的消息。这就是发生了冲突的状态,我们来解决它。
首先执行一个查看冲突状态的命令。
我们用 -l 选项执行 hg resolve
命令,发现 conflict.txt 仍处于未解决状态(LIST 6.17)。
LIST 6.17 hg resolve -l(查看冲突)
$ hg resolve -l
U conflict.txt
U 表示“未解决”(Unresolved)。打开文件可以查看冲突的位置,具体请参考“查看冲突位置”部分的内容。这里与其他版本控制系统一样,可以手动进行修正。 **
◉ 查看冲突位置
我们打开合并时发生冲突的 conflict.txt 文件,会发现内容变成了下面的样子。其实简单说来,所谓发生冲突,就是两个头对同一个文件的同一行进行了变更,计算机无法从机器逻辑层面上决定采用哪一方。要解决这个问题,就必须由人类明确表示出采用哪个变更。
A
A
<<<<<<< local
B
=======
C
>>>>>>> other
A
A
于是我们用文本编辑器打开 conflict.txt,手动编辑出合并后的 conflict.txt 文件。这里我们采用 rev1(this)的变更。
A
A
B
A
A
Mercurial 当然不知道人类是否解决了冲突,所以现在执行 hg resolve -l
仍然会出现之前的结果。此时我们要用 hg resolve -m
将当前状态从“冲突未解决”设置为“冲突解决完毕”(LIST 6.18)。然后只要提交即可。
LIST 6.18 hg resolve -m(解决冲突)
$ hg resolve -m conflict.txt
$ hg resolve -l
R conflict.txt
$ hg ci -m "merge"
$ hg log -G --style compact
@ 3[tip]:1,2 c72367726805 2011-11-18 16:08 +0900 monjudoh
|\ merge
| |
| o 2:0 0dffbb8f7780 2011-11-18 15:09 +0900 monjudoh
| | other
| |
o | 1 a239fe812ab0 2011-11-18 15:08 +0900 monjudoh
|/ this
|
o 0 0648c3b5afbd 2011-11-18 15:07 +0900 monjudoh
base
6.4.3 合并的类型与冲突
上面我们提到冲突是“计算机无法从机器逻辑层面上决定采用哪一方”的状态,这里将对此作进一步的说明。
这里依然和前面一样,假设出现了如下多头现象。
$ hg log -G --style compact
o 2[tip]:0 0dffbb8f7780 2011-11-18 15:09 +0900 monjudoh
| other
|
| @ 1 a239fe812ab0 2011-11-18 15:08 +0900 monjudoh
|/ this
|
o 0 0648c3b5afbd 2011-11-18 15:07 +0900 monjudoh
base
rev0(base)的 conflict.txt 文件的内容如下。
A
A
A
A
A
我们将 rev1、2(this、other)对第 3 行的修改情况以及合并后的结果总结成了下表(合并类型)。
合并类型
类型 | this | other | 结果 |
---|---|---|---|
① | A | A | A(无变更) |
② | B | A | B(采用 this 的变更) |
③ | A | C | C(采用 other 的变更) |
④ | B | B | B(碰巧合并成功) |
⑤ | B | C | 冲突 |
①~④都是合并成功。①的双方都没有作变更,所以合并结果中也没有出现变更。
②和③都只有一方作了变更,所以只采用变更的一方。
④比较特殊,虽然双方都对同一行作了变更,但变更内容是相同的,所以可以直接采用。具体④的情况下能不能成功合并还要看版本控制系统的种类,虽然 Mercurial 认为是成功,但有一部分版本控制系统会判定为冲突。
⑤是双方对同一行作了不同变更,于是出现了冲突。
6.4.4 用 GUI 的合并工具进行合并
除命令行之外,Mercurial 还支持用 GUI 的合并工具进行合并。
◉ KDiff3
这里以 KDiff33 为例介绍 GUI 的合并工具。KDiff3 支持 OS X、Windows、Linux,可以进行三路合并,是 GPLv2 的 OSS。
3http://kdiff3.sourceforge.net/
◉ 安装 KDiff3
任何环境下都能轻松安装 KDiff3。
○ 在 OS X 上安装
下载 dmg 并解压,将解压出来的 kdiff3.app 放到应用程序文件夹中。OS X10.9 无法直接使用放在文件夹中的 kdiff3.app。初次双击启动 kdiff3.app 时,系统会弹出对话框通知无法启动(图 6.2)。
图 6.2 通知 kdiff3.app 无法启动的对话框
于是我们需要以下流程。
初次启动时不要双击,要点右键通过上下文菜单打开(图 6.3)。
图 6.3 通过上下文菜单打开
此时会弹出确认启动的对话框,点击“打开”便能启动 kdiff3.app(图 6.4)。只要第一次成功启动,以后我们就可以通过双击打开它了。
图 6.4 确认启动的对话框
○ 在 Windows 上安装
下载安装包并运行。
○ 在 Linux(基于 Debian)上安装
$ sudo apt-get install kdiff3
○ 在 hgrc 中设置 merge-tools
要想关联 KDiff3,在合并发生冲突时通过它来解决问题,需要在 hgrc 或 Mercurial.ini(Windows 的情况下)中作以下设置。
[merge-patterns]
**.* = kdiff3
[merge-tools]
# Override stock tool location
# MacOSX
kdiff3.executable = Applicationskdiff3.app/Contents/MacOS/kdiff3
# Windows
# kdiff3.regkey=Software\KDiff3
# kdiff3.regappend=\kdiff3.exe
# kdiff3.fixeol=True
# kdiff3.gui=True
# Linux
# kdiff3.executable = ~/bin/kdiff3
# Specify command line
kdiff3.args = $base $local $other -o $output
# Windows
# kdiff3.args=--auto --L1 base --L2 local --L3 other $base $local $other -o $output
# Give higher priority
kdiff3.priority = 1
在 merge-tools 节的 kdiff3 中设置 KDiff3 的命令以及命令行对象,然后在 merge-patterns 节中设置所有文件冲突都使用 kdiff3 解决。
○ 用 KDiff3 解决冲突
我们还以 6.4.2 节描述的状态为例,给版本库执行 hg merge
命令。随后 conflict.txt 发生冲突,KDiff3 自动启动(图 6.5)。
图 6.5 因发生冲突而启动 KDiff3 之后的状态
左、中、右的 A、B、C 中分别是多头共同的祖先版本、执行 merge
命令时的 parent 修订版、执行 merge
命令时的另一个最新修订版中的 conflict.txt 内容,下方的视图显示了合并完成后的结果。我们可以看到发生冲突的地方显示为红色。前面也说了,解决冲突就是要在发生冲突的位置明确选择采用(或者不采用)某一方的变更。
在 KDiff3 中,右键点击发生冲突的位置,就会显示菜单供我们选择变更(图 6.6)。
图 6.6 KDiff3 解决冲突时选择变更的菜单
这里我们选 B(图 6.7)。
图 6.7 KDiff3 选择采用 B 时的状态
现在的状态是冲突已解决,界面中也明确显示出了合并对象的行采用了哪一个结果。随后保存文件并退出即可。
$ hg merge
merging conflict.txt
0 files updated, 1 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
关闭 KDiff3 的同时 merge
命令也就执行完了。与未设置合并工具的合并不同,这里没有出现 Unresolved 的文件。最后只要提交一下,这次合并工作就完工了。
6.5 GUI 客户端
在前面的说明中,我们一直在使用 Mercurial 的命令工具(CUI 客户端),其实 Mercurial 也有 GUI 客户端工具的。本节我们将了解一下 GUI 客户端的优缺点,以及一些工具。
6.5.1 GUI 客户端的介绍
这里先介绍一些主要的 GUI 客户端及其导入方法。
◉ TortoiseHg
TortoiseHg4 最早是 Windows 专用的 GUI 客户端,如今已经可以在 Windows/OS X/Linux 上跨平台使用了(图 6.8)。
4http://tortoisehg.bitbucket.org/
图 6.8 TortoiseHg 的示例界面
○ 安装
在 OS X 环境下,要通过 http://tortoisehg.bitbucket.org/download/index.html 的相应链接下载 zip 格式的 app 压缩包,解压后将 TortoiseHg.app 放到应用程序文件夹中并安装。之后的流程与安装 KDiff3 时一样。在 Windows 环境下,只要从 http://tortoisehg.bitbucket.org/ 下载安装包直接运行安装即可。如果是 Linux,http://tortoisehg.bitbucket.org/download/index.html 也写了哪个发布版有程序包可用。如果各位的环境有相应程序包,可以通过 apt 等包管理系统进行安装。
◉ SourceTree
SourceTree5 是 Atlassian 提供的商用 GUI 客户端,同时支持 Mercurial 和 Git。最早是 OS X 专用的 GUI 客户端,现在则可以同时支持 OS X 和 Windows(图 6.9)。
5http://www.sourcetreeapp.com/
图 6.9 SourceTree 的示例界面
○ 安装
如果是 OS X 环境,需要从 http://www.sourcetreeapp.com/ 下载 dmg 文件,然后将解压出来的 SourceTree.app 放入应用程序文件夹并安装。Mac App Store 上只提供了旧版本,所以请不要用那个。如果是 Windows 环境,则要从 http://www.sourcetreeapp.com/ 下载安装包直接安装。
至于合并工具的设置,在“环境设置”→“Diff”的“外部代码差异对比 / 合并”部分进行(图 6.10)。
图 6.10 设置 SourceTree 的合并工具
6.5.2 GUI 客户端的优点
下面我们以 TortoiseHg 为例来看看 GUI 客户端的优势。
◉ 显示历史图
GUI 客户端最大的特点就是时常显示历史图,并且能以它为起点进行多种操作。
如图 6.11 所示,打开工作台,上部窗格显示历史图,左下窗格显示历史图中已选定的修订版与其 parent 之间的 status,右下的窗格则显示提交日志以及 status 窗格中已选定文件的 diff。在历史图上右键点击各修订版可以直接执行 update 等操作,十分方便。
图 6.11 TortoiseHg 的工作台
◉ 工作目录的状态与差别的显示
在历史图中,工作目录与修订版享受同等待遇。选择工作目录后,左下窗格显示 status,右下窗格显示提交日志的草稿以及 diff(图 6.12)。
图 6.12 TortoiseHg 的工作台(选择了工作目录的状态)
提交也在这个界面中进行。由于提交时能看到文件复制的状态,所以能有效地减少提交方面的失误,比如出现了不该有的变更,或者在没有进行 add/remove 的情况下就进行提交等。
◉ 合并
○ TortoiseHg
TortoiseHg 会在合并发生冲突时显示 Unresolved 文件的列表。
图 6.13 TortoiseHg 解决冲突的界面
如图 6.13 所示,我们可以选择 Unresolved 的文件并点击右键,并从下述解决方案中进行选择。
通过 Mercurial 消除冲突
通常我们在合并时都选择了“尽量自动消除合并冲突”,所以不选用这个
启动 GUI 客户端消除冲突
采用当前所处位置的最新版本,并将状态改为 Resolved
采用合并目标一方的最新版本,并将状态改为 Resolved
编辑 Unresolved 文件后将状态改为 Resolved
6.5.3 GUI 客户端的缺点
GUI 客户端的缺点如下。
操作对象必须是 PC 本地版本库。通过 ssh 在服务器上工作时无法使用它们
hg 命令只要导入 extension 就能轻松地添加功能,但 GUI 客户端只能使用其固定支持的 extension
虽说有着这些缺点,但几乎所有 GUI 客户端都提供了打开 terminal 的功能,在用户觉得用 CUI 更好的时候能随时切换到 CUI。至于在服务器上工作的问题,我们平时的提交可以通过 CUI 进行,遇到合并等用 GUI 更方便的操作时,可以先把版本库 pull 到本地再用 GUI 客户端处理。
6.6 考虑实际运用的 BePROUD Mercurial Workflow
多人合作进行开发时,难免会遇到几个本地环境之间的提交出现矛盾,以及为解决矛盾需要进行合并的情况。另外,在多种功能同时进行开发的过程中,很可能后实现的功能需要先发布。为防止这类情况给团队开发带来混乱,保证开发和发布的顺利进行,我们必须事先确定好版本库创建分支与合并分支的时机。
另外,我们的理想情况是所有工作人员都可以通过命令行对版本库进行操作,但有些时候受条件所限并不能满足这一要求。为在这种情况下也能保证工作进度,我们需要用到一些工具,这些工具也会在本部分进行介绍。
6.6.1 概述
这里将介绍的是我们自己使用 Mercurial 进行源码管理的工作流程和管理结构。我们将在讲述项目相关人员以及开发方法等背景的基础上,为各位说明工作流程的相关内容。
NOTE
本节内容以 bpmercurial-workflow 文档为基础。文档与源码的许可证为 CC6 BY 2.1。本节讲述的流程与 Web 版相同,只是为了方便阅读对结构和文章作了少量修改。
bpmercurial-workflow 文档
http://beproud.bitbucket.org/bpmercurial-workflow/ja/
CC BY 2.1
6Creative Commons,简称 CC,中国大陆正式名称为知识共享,是一个非营利组织,也是一种创作的授权方式。此组织的主要宗旨在于增加创意作品的流通可及性,作为其他人据以创作及共享的基础,并寻找适当的法律以确保上述理念。——编者注
6.6.2 背景
既然要说明工作流程,那自然少不了项目的背景。所以我们先来了解一下背景。
◉ 目标项目
在这个工作流程中我们的目标项目是开发应用于 B to C 的 Web 站点(包括系统在内)。
多个小开发任务(包括设计变更等)并行进行,支持发布顺序不定的情况。
源码等成品的交付工作,均通过 Mercurial 向客户管理的版本库(后述的发布版本库)进行 push。
◉ 这个工作流程的相关人员
○ 程序员
提交系统的源码,主要负责管理版本库的人员。分支的合并与切换也由程序员负责。
服务器上已经为成员准备了各自的用户账户。为方便说明,我们这里假设程序员的用户名为 programmer。
○ 设计师
提交 HTML 模板、各种媒体文件的人员。也可能是网页制作工程师。
设计师需要向服务器上传文件,所以也备有用户账户。
○ 客户
接受开发完成的源码等成品的人员。通过后述的发布版本库取走源码。
◉ 工作环境
程序员和设计师对源码进行修改、测试等工作的环境,即服务器或本地机器上的环境。
本书中,我们将在给开发专设的服务器(Linux)上对版本库进行操作。至于设计师工作的服务器环境,我们也已经保证该环境能启动应用程序服务器来查看模板文件等是否正常工作。以 Python/Django 为例来说,就是用 runserver 等来负责启动。
6.6.3 版本库的结构
我们使用的是分布式版本控制系统,所以必须掌握版本库的结构以及各版本库的作用(图 6.14)。
图 6.14 版本库的结构
◉ 主版本库(master)
包含所有成果的版本库。用来积累以及共享整个团队每天开发出来的成果(比如开发中的分支等)。
版本库路径示例:varhg/example-prj
◉ 发布版本库(release)
装有已完成开发且等待发布到生产环境中的源码。放入这个版本库中的代码就是我们交给客户的成品。客户会使用这个版本库中的代码进行部署等工作。一般只有 default 分支处于活动状态。
进行发布(交付)工作时,我们要把 default 分支从主版本库 push 到发布版本库。
版本库路径示例:varhg/example-prj-release
◉ 工作版本库(working)
进行源码添加及修改等工作的版本库。开发成果要先提交到这个版本库,然后再 push 到主版本库。获取其他人的开发成果时,要从主版本库 pull 到这个版本库。
工作版本库既可以在服务器上也可以在本地环境上。
由于本书所介绍的开发将在服务器上进行,所以我们将主版本库 clone 至工作人员在服务器上的主目录下。
版本库路径示例:homeprogrammer/example-prj。
6.6.4 提交源码
源码写好后要提交到工作版本库。
default 分支要时常用于发布,所以在提交成果时,除了为发布而进行合并以外都要使用其他分支。
◉ 问题与分支
我们在第 5 章中也提到了,修改源码时要先发起问题,然后根据问题编号创建相应分支。
○ 示例
为了修改源码,我们在问题跟踪系统中创建了编号为 #5 的问题(图 6.15)。
图 6.15 在问题跟踪系统中创建的问题
这种情况下,修改后的源码要提交到 t5 分支。于是我们新建 t5 分支。
$ cd ~
$ cd example-prj
$ hg branch # 查看当前分支
default
$ # 问题编号为5,所以从default 分支创建名为t5 的分支
$ hg branch t5
接下来,将源码添加成为管理对象,然后提交。
$ hg add
$ hg commit
这样就可以将我们的修改提交上去了。
在这个状态下,default 分支并没有被修改,所以就算其中的代码传到其他版本库(比如发布版本库等),我们所作的修改也不会被发布(图 6.16)。
图 6.16 向分支提交之后的历史图
一般说来,修改过的源码不会很快被发布。
如果我们将暂不发布的源码提交到了 default 分支,那么一旦遇到需要插入临时发布的情况,就必需面临很多容易出问题的操作,比如多头现象、删除不合适的变更等。
只有到了发布那一刻才能将源码提交至 default 分支,也就是在开发过程中保证源码只出现在开发分支里,这样才能保证安全。
我们将工作版本库的更改传到主版本库,具体代码如下。
$ hg push --new-branch # 指定new-branch 远程添加分支
专栏 hg push —new-branch
不加任何选项的 hg push 命令不能在远程版本库创建新的头。不但已有的分支受此限制,而且在远程版本库新建分支的操作也包含在内。指定 —new-branch 选项可以在远程版本库添加新分支。另外,即便指定了 —new-branch 选项,我们依然无法给已有分支创建新的头,所以可以放心大胆地创建新分支并 push 变更。还有一点要注意,就算我们已经将本地创建的工作分支与 default 等合并,在 push 时仍然需要指定 —new-branch 选项。
6.6.5 提交设计
如何对待设计模板以及媒体文件是个非常难的点,而且根据设计师的技术以及工作人数的不同,这项工作的难度会有极大变化。一般说来,我们会使用 GUI 客户端通过 FTP、SCP 等将文件上传至服务器,确认其正常工作后再直接在服务器上提交文件。
◉ 在分支中进行设计
设计模板和媒体文件也要和源码一样提交到分支中。这是为了保证在与系统合并失败时能立刻还原到正常工作的状态。至于分支名,我们建议也和源码一样,起一个与问题编号相对应的名字。
如果设计需要频繁更改但系统没有变更,那么上面的方法会显得很繁琐,工作量很大。这种情况可以创建一个设计专用的分支,把所有和设计相关的变更都提交到这里(图 6.17)。
$ cd example-prj
$ hg branch
default
$ hg branch design # 从default 创建设计专用的分支
$ hg commit # 提交
图 6.17 向设计专用分支提交后的历史图
◉ 伴随系统变更而产生的设计提交
在添加、修改系统源码之后,如果需要对设计进行添加或修正,那么设计师必须先将查看设计的分支切换至已提交系统源码变更的分支,然后再进行自己的提交。
6.6.6 分支的合并
◉ 发布时的合并
为发布(交付)在工作分支中开发的功能或设计,我们需要先将工作分支合并到 default 分支(图 6.18)。
$ hg update default
$ hg branch
default
$ # 为发布t5 分支中的变更,要将该分支合并到default 分支
$ hg merge t5
$ hg commit
图 6.18 t5 分支合并到 default 分支后的历史图
◉ 用来追踪最新变更的合并
发布之后,未发布的分支也必须吸收这些变更,也就是要将 default 分支合并到对象分支(图 6.19)。把 default 分支的成果吸收到所有对象分支中能带来很多好处,比如对象分支向 default 分支合并时不会出现冲突。另外,对象分支的头与合并后的修订版,即 default 分支的新头之间不会有任何差别。这意味着只要合并前的分支通过了测试,合并后也必然能通过测试。
$ hg update t3
$ # 将default 分支合并到t3 分支,使t3 分支吸收最新的代码
$ hg merge default
$ hg commit
图 6.19 default 分支合并到 t3 分支后的历史图
专栏 如何处理已经无用的分支
当我们完成某部分开发,将与问题相对应的分支合并到 default 之后,这个分支就没有用了。但是,它仍然会以(inactive)的形式残留在 Mercurial 的分支一览中。
$ hg branches
default 7:a587bc0eb68e
design 6:e3f875bb6623
t3 3:0440d428d18a
t2 2:c4b2cd02f0c5
t1 1:c65aded0fe16
t5 4:9647e05b7580 (inactive) # 已经合并到default的分支仍会留在一览表里
inactive(非活动)状态表示该分支已经合并到其他分支(这里是 default)了。因此,有些尚未被发布的分支也可能进入 inactive 状态。如果不分清已经发布完毕的无用分支和仍要使用的分支,就很可能酿成遗漏发布等事故。Mercurial 有一个专门表示无用分支的状态closed(已关闭),我们可以将没有用的分支设置成这个状态。关闭某个分支(转入 closed 状态)时,需要先切换到该分支下,然后在提交时指定 —close-branch。
$ hg update t5
$ hg branch
t5
$ hg commit —close-branch # 将t5分支转为closed状态
$ hg branches # 已被close的t5不再出现在一览表中
default 7:a587bc0eb68e
design 6:e3f875bb6623
t3 3:0440d428d18a
t2 2:c4b2cd02f0c5
t1 1:c65aded0fe16
closed 状态的分支虽然不会显示在 hg branches 命令的一览表中,但这并不代表分支被删除了。虽然不关闭分支不会对开发造成影响,但考虑到事故风险,还是建议各位将无用的分支关闭。
6.6.7 集成分支
在统合多个分支的变更内容时,我们要创建一个集成分支来合并它们。
◉ 创建问题
先创建问题来确定分支编号(图 6.20)。
图 6.20 集成分支的问题
◉ 创建集成分支
创建与问题编号相对应的集成分支 t6。
$ hg update default
$ hg branch t6
$ hg commit # 提交分支
将要集成的对象分支合并到 t6(图 6.21)。
$ hg update t6 # 更新集成分支
$ hg merge t1 # 合并t1 分支
$ hg commit
$ hg merge t3 # 合并t3 分支
$ hg commit
图 6.21 将对象分支合并到用于查看的分支后的历史图
发布集成分支中的全部内容时,要将集成分支 t6 合并到 default 分支。
$ hg update default
$ hg merge t6 # 集成分支
$ hg commit
专栏 集成分支的用武之地
在变更内容出现冲突,或者需要在特定时间整合分支并统一发布等情况下,集成分支会显得非常好用。
另外,我们还可以将开发完成的各功能分支先合并到集成分支,经过综合测试之后再向 default 合并。
6.7 小结
本章就 Mercurial 的使用方法进行了说明,内容如下。
钩子功能的概述与活用方法
分支、合并以及消除冲突的方法
GUI 客户端的介绍
另外,还对多人开发时的工作流程进行了说明,它对于防范冲突及运用上的不统一有着重要的意义。
如今,无论人多人少,Mercurial 等版本控制系统都是开发中必备的工具,能否用好版本控制系统已成为左右开发效率的一个重要因素。
专栏 Git 与 Mercurial 的区别
Git 是一种广受欢迎的分布式版本控制系统,与 Mercurial 齐名。因此,我们能看到很多 Git 与 Mercurial 的命令对照表,这就是为 Git 用户准备的 Mercurial 入门,或者是为 Mercurial 用户准备的 Git 入门。不过,如果让 Git 用户单纯根据命令对照表来使用 Mercurial,或者反过来让 Mercurial 用户根据对照表使用 Git,恐怕会是一种很危险的行为。要知道,虽然 Git 和 Mercurial 同为分布式版本控制系统,但二者的模型并不相同。乍看上去,人们只要记住 Git 中的命令和 Mercurial 中的命令的差别,就能随便在二者间换着用了,然而即使有对照表,也无法改变它们不同的本质,所以对很多操作都是都会产生误解,这早晚会让我们栽跟头。举个例子更能说明 Git 和 Mercurial 模型间的差异,Mercurial 的版本库由提交对象的图表构成,而 Git 的版本库由提交对象的图表和引用构成。
这个差别最明显地体现在对分支的看法以及同步(push/pull)上。
Mercurial 的分支本质上是给提交对象的分支的根部命了名。严格说来,分支只不过是被命了名的各个提交对象的参数。但是,在我们给子分支新命名之前,子分支会继承父分支的名字,所以说它在给分支的根部命名。
相对地,Git 的分支是给提交对象的图表的最新端命了名。这是一个每次提交都会跟着最新端切换的引用。Git 的 fast-foward 合并其实就是这个表示分支的引用的切换,分支的删除其实就是引用的删除。因此,Mercurial 中既没有 fast-foward 合并也没有分支的删除(fast-foward曾被导入过,但与 Git 的 fast-foward 合并仍是貌相似而质不同)。
如果把 push/pull 看作版本库的同步,那么版本库模型不同的理由将更加显而易见。Mercurial 的版本库是提交对象的图表,因此只需将提交对象图表的差别同步即可。push 操作中只需发送对方没有的那部分图表,pull 则只需获取自己没有的那部分图表。比如“删除历史”就无法同步。就算我们加上 -f 选项,最多也就是让历史受到污染(共享了多余的子图表) 。
Git 的版本库是提交对象的图表和引用,因此双方必须保持同步。切换引用的必要性致使 pull 需要伴随合并操作。如果不进行合并,该分支就将出现 remote 的和 local 的两个最新端。但是 Git 的分支是给最新端命的名,这就要求一个分支只能有一个最新端。Git 的 pull 之所以必须以分支为单位,恐怕就是因为存在这样一个需主观意识判断的合并过程。push 无法进行主观意识判断,所以只有满足 fast-foward 合并(不需主观意识判断的合并)的情况下才允许 push。
在更改历史上也有显著差异。Git 在日常使用中充满了更改历史,所以习惯 Git 的人在用 Mercurial 时总会去找类似的操作。但是,此时直接按对照表操作会出现很大隐患。虽然 Git 和 Mercurial 中都有“更改历史”,但二者实际要做的事却完全不同。Git 是新建图表并将引用切换到最新端,所以在 GC 启动之前仍然保留着更改前的图表。这个图表我们只是看不到罢了,但仍可以通过 reflog 引用它。相对地,Mercurial 是实际更改原有图表,即向图表添加新的子图表并删除旧的子图表。一个由图表的引用构成,一个由图表直接构成,这里,二者的差别非常显 著。正因为如此,Git 在更改失败时只需引用 reflog 寻找更改前的修订版(分支的最新端),将这个修订版设置为该分支的头即可完成恢复,但 Mercurial 就必须通过 bundle backup 恢复旧的子图表,然后用 strip 命令删除新的子图表。可见,由于 Git 与 Mercurial 在模型方面存在差异,导致更改历史有着实质上的不同,更改历史失败后的恢复难度及安全性也都大相径庭。所以各位在用M ercurial 更改历史时,建议先让保存原历史的选项有效,等确认更改无误后再通过 strip 命令删除旧的子图表。