第 3 章 Mercurial 教程: 合并工作

目录

3.1. 合并的流程
3.1.1. 顶点修改集
3.1.2. 执行合并
3.1.3. 提交合并结果
3.2. 合并有冲突的变更
3.2.1. 使用图形合并工具
3.2.2. 合并实例
3.3. 简化拉-合并-提交程序
3.4. 重命名,复制与合并

我们已经介绍了如何克隆版本库,如何进行修改,从一个版本库向另外一个版本库拉或者推送变更。下面我们要介绍的是如何从不同的版本库合并变更。

3.1. 合并的流程

在使用分布式版本控制工具中,合并是非常重要的。以下是几种需要进行合并的情形。

  • 在一个合作的项目中,Alice和Bob都有一个项目版本库的私有拷贝。Alice修复了在她的库中的一个bug;Bob则在他的版本库中增加了一个新功能。他们希望分享版本库,每个人都得到新的功能,同时又修复了bug。

  • Cynthia同时在一个项目上进行几个不同的任务,每个任务都是孤立的。以这种方式工作意味着她经常需要将不同部分的工作进行合并。

因为我们经常需要合并,Mercurial使得这个过程很简单。下面我们演示一下合并的过程。我们仍然从克隆另外一个版本库开始(看看它出现的多么频繁),并在它上面作更改。

$ cd ..
$ hg clone hello my-new-hello
updating to branch default
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd my-new-hello
# Make some simple edits to hello.c.
$ my-text-editor hello.c
$ hg commit -m 'A new hello for a new day.'

现在hello.c有了内容不尽相同的两份拷贝。 两个版本库的历史也出现差异,就像在图 3.1 “my-hellomy-new-hello 最新的历史分叉”中显示的那样。下面是文件在一个版本库中的拷贝。

$ cat hello.c
/*
 * Placed in the public domain by Bryan O'Sullivan.  This program is
 * not covered by patents in the United States or other countries.
 */

#include <stdio.h>

int main(int argc, char **argv)
{
	printf("once more, hello.\n");
	printf("hello, world!\");
	printf("hello again!\n");
	return 0;
}

下面是该文件在另外一个版本库中稍微不同的版本。

$ cat ../my-hello/hello.c
/*
 * Placed in the public domain by Bryan O'Sullivan.  This program is
 * not covered by patents in the United States or other countries.
 */

#include <stdio.h>

int main(int argc, char **argv)
{
	printf("hello, world!\");
	printf("hello again!\n");
	return 0;
}

图 3.1. my-hellomy-new-hello 最新的历史分叉

XXX add text

我们已经知道,从我们的my-hello版本库推送变更对工作目录没有任何影响。

$ hg pull ../my-hello
pulling from ../my-hello
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)

但是,hg pull命令还是输出了一些关于 heads的信息。

3.1.1. 顶点修改集

记住Mercurial记录了每个变更的父版本。如果一个变更有父版本,我们称之为其父版本的孩子或者后裔。领头版本是一个没有孩子的版本。顶点版就是这样一个领头版本,因为版本库中的最新版没有任何孩子。有时候一个版本库中会有多个领头版本

图 3.2. 从 my-hello 拉到 my-new-hello 之后版本库的内容

XXX add text

图 3.2 “从 my-hello 拉到 my-new-hello 之后版本库的内容”中,你可以看到将变更从my-hello拖到my-new-hello之后的效果。my-new-hello中已有的版本历史没有发生任何变化,但是增加了一个新的版本。从图 3.1 “my-hellomy-new-hello 最新的历史分叉”中,我们可以发现变更集标识符在新的版本库中保持不变,但是版本号变了。 (顺便说一句,这个例子很好的解释了为什么在讨论变更集的时候版本号是不安全的。我们可以使用hg heads命令查看版本库中的领头版本。

$ hg heads
changeset:   6:2e443ac19c36
tag:         tip
parent:      4:2278160e78d4
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Mar 17 03:00:29 2011 +0000
summary:     Added an extra line of output

changeset:   5:51869dafe02e
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Mar 17 03:00:38 2011 +0000
summary:     A new hello for a new day.

3.1.2. 执行合并

如果我们使用常用的hg update命令来更新到新的顶点,会发生什么事情呢?

$ hg update
abort: crosses branches (use 'hg merge' or use 'hg update -c')

Mercurial告诉我们hg update不能进行合并;当它认为我们可能需要进行合并的时候,它不会更工作目录。除非我们强制它这样做。(附带说一下,使用hg update -C命令强制更新会丢掉当前目录没有提交的变更。)

我们使用 hg merge 命令来合并两个领头版本。

$ hg merge
merging hello.c
0 files updated, 1 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)

我们解析了hello.c的内容。这个操作更新了工作目录,使其包含了两个领头版本的变更,这反映在hg parents的输出和hello.c的内容上。

$ hg parents
changeset:   5:51869dafe02e
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Mar 17 03:00:38 2011 +0000
summary:     A new hello for a new day.

changeset:   6:2e443ac19c36
tag:         tip
parent:      4:2278160e78d4
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Mar 17 03:00:29 2011 +0000
summary:     Added an extra line of output

$ cat hello.c
/*
 * Placed in the public domain by Bryan O'Sullivan.  This program is
 * not covered by patents in the United States or other countries.
 */

#include <stdio.h>

int main(int argc, char **argv)
{
	printf("once more, hello.\n");
	printf("hello, world!\");
	printf("hello again!\n");
	return 0;
}

3.1.3. 提交合并结果

当我们完成合并,并通过hg commit提交合并的结果之前,hg parents命令都会显示当前版本有两个父版本。

$ hg commit -m 'Merged changes'

现在我们有了一个新的顶点版本;注意先前的领头版本是它的父版本。这和刚才我们用 hg parents显示的版本一致。

$ hg tip
changeset:   7:c70193a5739d
tag:         tip
parent:      5:51869dafe02e
parent:      6:2e443ac19c36
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Mar 17 03:00:39 2011 +0000
summary:     Merged changes

图 3.3 “在合并期间,以及提交之后的工作目录与版本库”显示了在合并过程中,工作目录中发生了什么,还有当提交发生的时候,它是如何影响版本库的。在合并过程中,当前目录有两个父变更集,他们都成为新的变更集的父版本。

图 3.3. 在合并期间,以及提交之后的工作目录与版本库

XXX add text

我们有时候说合并有两边:左边是hg parents命令输出的第一个父版本,右边是第二个。如上例所示,如果在合并前版本号是5,那么它就会成为合并的左边。

3.2. 合并有冲突的变更

大多数合并都很简单,但有时候你会发现待合并的不同变更修改了同一文件的相同部分。除非修改完全一致,否则合并会出现冲突,这时候你就要决定如何在不同的变更中取舍,最后形成一致的结果。

图 3.4. 冲突的修改

XXX add text

图 3.4 “冲突的修改”是一个文档变更相互冲突的的实例。我们从文件的一个版本开始,做了一些修改;但别人也在相同的地方作了不同的修改。我们解决冲突的目的就是要决定文件到底应该是什么样子。

Mercurial没有内建的工具处理冲突。相反,它会运行外部程序,通常是一个能够以图形化显示冲突的软件。缺省情况下,Mercurial会试着从有可能在你系统上安装的几个合并工具中挑选一个。它首先会尝试几个全自动的合并工具;如果不成功(因为解决冲突需要人工干预)或者找不到,它会尝试其他不同的图形化合并工具。

如果将环境变量HGMERGE传给你的程序,还可以让Mercurial运行特定的程序或者脚本。

3.2.1. 使用图形合并工具

我比较喜欢的图形化的合并工具是kdiff3。我会用它来描述图形化合并工具的一般特征。你可以在图 3.5 “使用 kdiff3 合并文件的不同版本”看到正在使用中的kdiff3的截图. 它正在进行的这种合并叫三路合并。因为我们感兴趣的文件有三种不同的版本。所以它把窗口的上半部分为3格。

  • 左边是文件的版本,也就是我们合并的两个版本的最新的父版本。

  • 中间是我们的版本,其内容已经被我们修改。

  • 右边是他们的版本,就是从我们试图进行合并的变更集。

它们下面的方格是合并的当前结果。我们任务就是替换所有的红色文字,它们标志着未解决的冲突,我们必须在我们的他们的版本中进行合理的合并。

这四格都是关联的,如果我们将其中任意一个垂直或水平滚动,其他的格也会更新对应文件的相关部分的显示。

图 3.5. 使用 kdiff3 合并文件的不同版本

XXX add text

对于文件的每个冲突,我们可以用基版本,我们的版本或者他们的版本的一些文字的组合来解决冲突。也可以在任何时候手工编辑合并后的文件。这种情况下我们需要进一步的修改。

许多可以选择的合并工具,篇幅有限,这里就不多介绍了。他们适应于不同的平台,各有其优点和缺点。大多数软件都是针对合并纯文本文件优化的,另外一些则是针对特殊的格式(通常是XML)进行了优化。

3.2.2. 合并实例

在本例中,我们将重建图 3.4 “冲突的修改”的修改历史。我们从创建文档基版本的版本库开始。

$ cat > letter.txt <<EOF
> Greetings!
> I am Mariam Abacha, the wife of former
> Nigerian dictator Sani Abacha.
> EOF
$ hg add letter.txt
$ hg commit -m '419 scam, first draft'

我们克隆版本库并且修改文件。

$ cd ..
$ hg clone scam scam-cousin
updating to branch default
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd scam-cousin
$ cat > letter.txt <<EOF
> Greetings!
> I am Shehu Musa Abacha, cousin to the former
> Nigerian dictator Sani Abacha.
> EOF
$ hg commit -m '419 scam, with cousin'

让另外一个克隆去模拟别人修改文件(这意味着当你利用不同的版本库处理不同任务的时候,常常需要和自己合并,实际上也就是找到并解决冲突。)

$ cd ..
$ hg clone scam scam-son
updating to branch default
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd scam-son
$ cat > letter.txt <<EOF
> Greetings!
> I am Alhaji Abba Abacha, son of the former
> Nigerian dictator Sani Abacha.
> EOF
$ hg commit -m '419 scam, with son'

这个文件已经有了两个不同的版本,我们来设置适合进行合并的环境。

$ cd ..
$ hg clone scam-cousin scam-merge
updating to branch default
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd scam-merge
$ hg pull -u ../scam-son
pulling from ../scam-son
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
not updating, since new heads added
(run 'hg heads' to see heads, 'hg merge' to merge)

在本例中,我们设置HGMERGE告诉MERCURIAL用非交互式的合并命令。许多类似Unix系统都有这个功能。(如果你在电脑上,不必设置HGMERGE,直接使用GUI会更好)

$ export HGMERGE=merge
$ hg merge
merging letter.txt
merge: warning: conflicts during merge
merging letter.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
$ cat letter.txt
Greetings!
<<<<<<< /tmp/tour-merge-conflictbvrMkW/scam-merge/letter.txt
I am Shehu Musa Abacha, cousin to the former
=======
I am Alhaji Abba Abacha, son of the former
>>>>>>> /tmp/letter.txt~other.bzAOZu
Nigerian dictator Sani Abacha.

由于合并解决不了冲突的变更,所以冲突文件会有标记,表明哪些行存在冲突,他们来自我们的文件还是他们的文件。

Mercurial能根据merge命令的退出状态判断出合并失败,所以它会告诉我们如果我们想重新执行合并操作的时候需要运行什么命令。这可能很有用,例如,我们运行了一个图形化合并工具然后发现不知所措或者意识到犯了错误然后退出。

如果自动化或者手动合并失败,那么我们只好自己修复受影响文件,然后提交合并的结果。

$ cat > letter.txt <<EOF
> Greetings!
> I am Bryan O'Sullivan, no relation of the former
> Nigerian dictator Sani Abacha.
> EOF
$ hg resolve -m letter.txt
$ hg commit -m 'Send me your money'
$ hg tip
changeset:   3:45b59e06bbbe
tag:         tip
parent:      1:40d894591daf
parent:      2:28f13c93b6ed
user:        Bryan O'Sullivan <bos@serpentine.com>
date:        Thu Mar 17 03:00:41 2011 +0000
summary:     Send me your money

[注意] 在哪里能找到 hg resolve 命令?

Mercurial 1.1版于2008年发布,从这个版本开始引入了hg resolve命令。如果你还在使用更老的版本(可以运行hg version命令查看),这个命令还不存在。如果你的Mercurial的版本比1.1还旧,那么我强烈建议你在尝试解决复杂的合并之前升级到新的版本。

3.3. 简化拉-合并-提交程序

上面讨论的合并变更的过程非常简单,但是需要按照顺序允许三个命令

hg pull -u
hg merge
hg commit -m 'Merged remote changes'

在最后提交的时候,你需要输入一条提交信息,这通常并不怎么有趣。

如果有可能的话,最好能够减少需要的步骤。实际上,Mercurial发布的时候有一个扩展叫做fetch可以完成这个工作。

Mercurial提供了灵活的扩展机制,人们可以利用它扩展它的功能,同时保持Mercurial的核心很小并且容易管理。有些扩展增加了新的命令,你可以通过命令行使用它们,而其它的都是幕后工作,比如增加了Mercurial内建服务器模式的能力。

fetch扩展增加了一个新的命令,毫无疑问,它叫做hg fetch。这个扩展的功能像hg pull -u, hg mergehg commit的组合。 它首先从其他版本库将变更拖入当前版本库。如果它发现变更向版本库添加了新的领头版本,它会更新到新的领头版本,开始合并然后(如果合并成功)提交合并的结果和一条自动产生的提交信息。如果没有新的领头版本,它仅仅会将当前目录更新到新的顶点。

使用fetch扩展非常容易。编辑你的家目录的.hgrc文件,找到扩展段或者创建一个扩展段。然后增加一行fetch=

[extensions]
fetch =

(一般情况下,=的右边应该表示如何找到扩展,但是因为fetch扩展是在标准的发布版中, Mercurial知道哪里能找到它。)

3.4. 重命名,复制与合并

在一个项目的生命里,我们经常需要更改它的文件和目录的布局。这可能很简单,仅仅是对文件改名,或者非常复杂,像对整个项目的文件层次重新规划。

Mercurial对这种复杂的变更支持的很好,只要你告诉它你想干什么就可以了。如果你想要给一个文件改名,只要用hg rename[2]给它改名就可以了,在以后需要合并的时候,Mercurial知道该怎么做。

我们将在第 5.3 节 “拷贝文件”中详细介绍这些命令的使用。



[2] 如果你是一个Unix用户,你会很高兴的发现hg rename可以简写为hg mv