目录
Mercurial does not work with files in your repository unless you tell it to
manage them. The hg status command will
tell you which files Mercurial doesn't know about; it uses a
“?
” to display such files.
使用hg add命令来让Mercurial跟踪一个文件。
一旦添加了一个文件,该文件的hg
status的输出就从“?
”变成了“A
”.
$
hg init add-example
$
cd add-example
$
echo a > myfile.txt
$
hg status
? myfile.txt$
hg add myfile.txt
$
hg status
A myfile.txt$
hg commit -m 'Added one file'
$
hg status
在运行hg commit之后,你在提交之前添加的文件将不会再出现在hg status命令的输出中。原因在于,在缺省情况下,hg status仅仅告诉你那些你可能“感兴趣”的文件—那些你已经(比如)修改,删除,改名的文件。如果你的版本库中包含几千个文件,你基本上不会想知道Mercurial跟踪了哪些文件,如果这些文件并没有被更改的话。(你还是可以得到这一信息的,以后我们还会讨论它。)
添加一个文件之后,Mercurial并不会马上对它做任何操作。相反,在下次你提交的时候 ,它会给这个文件作一个快照。并且在你以后每次提交的时候继续跟踪这个文件,直到你删除它。
Mercurial一个有用的特征是如果你将一个目录名传递给一个命令,任何一个Mercurial命令都会作如下处理“我要在这个目录和它的子目录中的所有文件上执行操作”。
$
mkdir b
$
echo b > b/somefile.txt
$
echo c > b/source.cpp
$
mkdir b/d
$
echo d > b/d/test.h
$
hg add b
adding b/d/test.h adding b/somefile.txt adding b/source.cpp$
hg commit -m 'Added all files in subdirectory'
注意在这个上面这个例子中,Mercurial输出了它添加的文件的名字,然而在前面的例子中当我们添加文件myfile.txt
的时候,它没有输出任何东西。
在先前的那个例子中,我们在命令中明确的给出了要添加的文件。在这种情况下,Mercurial假设我们知道我们在做什么,所以它不输出任何东西。
然而,当我们通过目录隐含地指定文件的时候,Mercurial会将其操作的每个文件的文件名都输出。这样会更清晰,同时减少可能意外情况。大多数Mercurial命令都有这样的行为。
Mercurial并不跟踪目录信息。相反,它会跟踪文件的路径。在创建一个文件之前,它首先会创建该文件路径中缺少的目录。在删除文件之后,它会删除在被删除文件路径上的任何空目录。这看起来区别不大,但是却导致这样的结果:Mercurial不可能管理一个完全为空的目录。
空目录基本上没有什么用,而且你可以用其他的方法达到相同的效果。因此Mercurial的开发者认为管理空目录带来的复杂性超过它带来的好处。
如果你真的希望在版本中包含空目录,有几种方法。其一是创建一个目录,然后用hg
add命令在目录中添加一个“隐藏”文件。比如在UNIX类似系统上,大多数命令和GUI工具都认为文件名以点(“.
”)
开头的文件是隐藏文件。这种方法如下。
$
hg init hidden-example
$
cd hidden-example
$
mkdir empty
$
touch empty/.hidden
$
hg add empty/.hidden
$
hg commit -m 'Manage an empty-looking directory'
$
ls empty
$
cd ..
$
hg clone hidden-example tmp
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
ls tmp
empty$
ls tmp/empty
一旦你决定要将一个文件从版本库中删除,使用 hg
remove命令。它会删除该文件,同时通知Mercurial停止跟踪它(下次提交的时候生效)。已被删除的文件在hg status的输出中以“R
”标识。
$
hg init remove-example
$
cd remove-example
$
echo a > a
$
mkdir b
$
echo b > b/b
$
hg add a b
adding b/b$
hg commit -m 'Small example for file removal'
$
hg remove a
$
hg status
R a$
hg remove b
removing b/b
在你使用hg remove删除一个文件之后,Mercurial不再跟踪这个文件的变化,即使你在工作目录以同样的名字重新创建了一个文件。如果你以相同的名字重新创建了一个文件,并且希望Mercurial跟踪新的文件,只要用hg add添加它就可以了。Mercurial会知道这个新加的文件虽然和以前的文件名字相同,但是毫无关系。
如果你在某一版本中将一个文件删除,而后将工作目录更新到以前的某个版本,那时这个文件还没有被删除,那么这个文件会重新在工作目录中出现,其内容和你提交那个变更集的内容相同。然后如果你更新到比较新的版本,这时这个文件已经被删除了,那么Mercurial会再次将这个文件从工作目录中删除。
如果一个文件被删除了,但是不是用hg
remove命令删除的,Mercurial认为它丢失了。 丢失的文件在hg
status的输出中以“!
”标识。一般情况下Mercurial命令不会对丢失的文件作任何处理。
$
hg init missing-example
$
cd missing-example
$
echo a > a
$
hg add a
$
hg commit -m 'File about to be missing'
$
rm a
$
hg status
! a
如果hg
status报告版本库中的一个文件丢失了,而且你确实不需要这个文件了,你可以在以后的任何时间运行hg remove --after
命令,告诉Mercurial你希望删除这个文件。
$
hg remove --after a
$
hg status
R a
另一方面,如果你是不小心把那个文件删掉的,可以用hg revert命令加上要恢复的文件名。它会将文件恢复到未修改前的状态。
$
hg revert a
$
cat a
a$
hg status
Mercurial提供了一个hg copy命令,可以用来拷贝文件。当你用这个命令拷贝文件时,Mercurial会记录下这个文件是由原来的文件拷贝而来的。以后在你把工作和其他人的工作合并的时候,它会对这些拷贝的文件进行特殊处理。
在合并过程中,变更会“传递”给拷贝。为了更好的解释它,我们创建一个例子。我们从一个仅仅包含一个文件的小版本库开始。
$
hg init my-copy
$
cd my-copy
$
echo line > file
$
hg add file
$
hg commit -m 'Added a file'
我们需要并行的工作,所以我们要进行合并。所以我们将版本库克隆。
$
cd ..
$
hg clone my-copy your-copy
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
回到初始的版本库,我们用hg copy命令创建我们开始时建立的文件的拷贝。
$
cd my-copy
$
hg copy file new-file
然后如果我们看一下hg status的输出,发现拷贝的文件看起来像普通的新添加的文件。
$
hg status
A new-file
当时如果我们给hg status加上-C
选项,它会输出另外一行:新添加的文件是从那个文件拷贝而来的。
$
hg status -C
A new-file file$
hg commit -m 'Copied file'
现在,回到我们克隆的那个版本库,我们并行的作一点改动,给我们最开始创建的文件添加一行。
$
cd ../your-copy
$
echo 'new contents' >> file
$
hg commit -m 'Changed file'
现在版本库里面有了修改过的文件。我们从第一个版本库拖变更,并且合并两个顶版本,Mercurial会将变更从我们修改的文件file
传递给它的拷贝,new-file
。
$
hg pull ../my-copy
pulling from ../my-copy 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 merge
merging file and new-file to new-file 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)$
cat new-file
line new contents
这一行为—文件的变更会传递到它的拷贝中— 可能看起来难以理解, 但在大多数情况下,它很受欢迎。
首先要记住这种传播仅仅发生在合并的时候。所以如果你用hg copy拷贝了一个文件,此后在工作过程这原文件进行了修改,这时什么也不会发生。
其次要了解的是,只有当你在合并的变更还没有看到这个拷贝的时候,修改才会通过拷贝传播。
Mercurial这样设计的原因如下。假设我们在源代码中修正了一个重要的bug,然后提交了变更。与此同时,你决定用hg copy命令在你的版本库中拷贝这个文件,但你并不知到这个bug也没有发现它已经被修复了,并且你已经开始在你的拷贝上进行修改了。
如果你把我的变更拖进来并且合并,但Mercurial不会将变更传递给拷贝,那么你的新代码文件将仍然包含那个bug,除非你知道并且手动修复那个它,这个bug会一直保留在你的拷贝里面。
Mercurial将修复了bug的变更从原始文件自动的传播到拷贝,从而避免了这样的问题。据我了解,Mercurial是唯一的像这样在拷贝间传播变更的版本控制系统。
一旦你的变更历史中有了拷贝和随后的合并记录,通常再次将变更从原始文件传递到拷贝文件,这就是Mercurial仅仅在第一次合并的时候在拷贝间传递变更的原因,而不是此后。
如果出于某种原因,你觉得这种自动在拷贝间传递变更的方式不适合你,那么你可以使用系统的拷贝命令(在类Unix系统上就是cp命令)来复制文件,然后使用hg add手动添加拷贝的文件。在你这么作之前,请重新阅读第 5.3.2 节 “为什么要传递变更?”,确认这一功能是不是真的不适合你的情况,然后作出正确的决定。
在使用hg copy命令的时候,Mercurial会复制工作目录中当前的文件。也就是说,如果你对文件作了修改,并且没有提交,那么hg copy生成的新的文件将包含这些修改。(我觉得这样有点不合常理,所以在这里提一下。)
hg copy命令和Unix的cp命令功能类似(如果你喜欢,可以用hg cp作为它的别名)。我们必须提供两个或者两个以上参数, 其中最后一个为目标,其他的是源。
如果你传给hg copy一个文件作为源,而目标文件不存在,它创建该文件。
$
mkdir k
$
hg copy a k
$
ls k
a
如果目标是一个目录,Mercurial会将所有的源文件拷贝到目标目录。
$
mkdir d
$
hg copy a b d
$
ls d
a b
$
hg copy z e
copying z/a/c to e/a/c
$
hg copy z d
copying z/a/c to d/z/a/c
和hg
remove命令一样,如果你手动拷贝了一个文件并且希望Mercurial知道你拷贝了这个文件,可以给hg copy命令加上--after
选项。
$
cp a n
$
hg copy --after a n
与拷贝文件比起来,重命名文件使用频率更高。我在讨论重命名文件之前讨论hg copy命令是因为Mercurial对于拷贝和重命名的处理方式相同。因此知道了Mercurial怎么处理拷贝文件也就知道了Mercurial怎么处理重命名文件。
在你运行hg rename命令的时候,Mercurial 首先对每个源文件都做一份拷贝,然后删除它,并将其标识为删除。
$
hg rename a b
hg status命令显示新拷贝的文件的状态是添加,拷贝的那个文件的状态是删除。
$
hg status
A b R a
和hg copy命令的结果一样,我们必须给hg status命令加上-C
选项才能看到Mercurial是将新加的文件是作为原始文件的拷贝进行管理的,同时原始文件已经删除。
$
hg status -C
A b a R a
hg remove和hg
copy命令一样,你在事后可以使用--after
选项告诉Mercurial改名。大多数情况下,hg rename和hg
copy的行为和接受的选项都是相似的。
如果你熟悉Unix命令行,那么告诉你一个好消息,可以用hg mv替代hg rename。
因为Mercurial的重命名是以拷贝删除的方式实现的,如果拷贝了一个文件,然后又将其重命名,变更还是会从原来的文件传播到更名后的文件。
如果我修改了一个文件,同时你又将其更名,然后我们合并相关变更,那么我在原始文件名下作的修改将会传播到你的新文件中去。(这个功能你可能认为“很简单,”但是不是所有的版本控制系统都有这个功能。
对于变更会跟随拷贝这个功能,你可能不以为意地说 “好吧,这个可能会有用” 这里要说明让变更跟随重命名非常重要。 如果没有这个功能,那么在文件重命名之后,变更很容易被丢进黑洞。
分歧更名是这样的情况,假设两个开发者的版本库中有一个文件—文件名为foo
。
$
hg clone orig anne
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
hg clone orig bob
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$
cd anne
$
hg rename foo bar
$
hg ci -m 'Rename foo to bar'
同时, Bob将它改名为quux
。(记住hg
mv是hg rename的别名。)
$
cd ../bob
$
hg mv foo quux
$
hg ci -m 'Rename foo to quux'
我认为这是一个冲突,因为开发者对这个文件应该如何命名有了不同的意见。
你觉得他们合并的时候会发生什么呢?在合并包含分歧更名的变更集的时候,Mercurial实际上会将两个文件都保留。
# See http://www.selenic.com/mercurial/bts/issue455$
cd ../orig
$
hg pull -u ../anne
pulling from ../anne searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files 1 files updated, 0 files merged, 1 files removed, 0 files unresolved$
hg pull ../bob
pulling from ../bob 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 merge
warning: detected divergent renames of foo to: bar quux 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)$
ls
bar quux
Mercurial一直都有一个bug,如果它在进行合并的时候发现一边有一个文件,而另外一边有一个相同名称的目录,那么合并就会失败。这个问题记录在issue 29。
$
hg init issue29
$
cd issue29
$
echo a > a
$
hg ci -Ama
adding a$
echo b > b
$
hg ci -Amb
adding b$
hg up 0
0 files updated, 0 files merged, 1 files removed, 0 files unresolved$
mkdir b
$
echo b > b/b
$
hg ci -Amc
adding b/b created new head$
hg merge
abort: Is a directory: /tmp/issue29thzCc3/issue29/b
Mercurial提供了一些有用的命令,它们可以帮助你从一些常见的错误中恢复。
可以hg revert命令取消对工作目录做的变更。比如,你不小心用hg add命令添加了一个文件,只要运行hg revert加上你添加的文件的文件名就可以了,文件的内容不会有任何改变,仅仅是Mercurial不再跟踪它了。你也可以用hg revert消除对文件作出的错误的更改。
要记住hg revert命令仅仅适用于你的变更还没有提交的时候。一旦你提交了变更,然后发现这是个错误,你仍然有机会修正,虽然能做的很有限。
关于hg revert命令的更多信息,还有如何处理已经提交的变更,请参考第 9 章 查找和修改错误。
在庞大而且复杂的项目中,两个变更集的合并常常让人头痛。假设有关大的源文件,合并的两边都作了很多修改:这不可避免的会导致很多冲突,有些要试几次才能解决。
我们用一个简单的例子看看如何处理这种情况。我们将包含一个文件的版本库克隆两次。
$
hg init conflict
$
cd conflict
$
echo first > myfile.txt
$
hg ci -A -m first
adding myfile.txt$
cd ..
$
hg clone conflict left
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
hg clone conflict right
updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$
cd left
$
echo left >> myfile.txt
$
hg ci -m left
$
cd ../right
$
echo right >> myfile.txt
$
hg ci -m right
$
cd ../conflict
$
hg pull -u ../left
pulling from ../left searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
hg pull -u ../right
pulling from ../right 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)
$
hg heads
changeset: 2:1887833b02ba tag: tip parent: 0:30272197d5ab user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 02:59:50 2011 +0000 summary: right changeset: 1:49bc86613f81 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Mar 17 02:59:50 2011 +0000 summary: left
正常情况下,如果这时我们运行hg
merge。它会运行一个GUI程序,让我们解决myfile.txt
的冲突。但是,为了简化这里的演示,我们希望合并失败。我们可以按照下面的方法做。
$
export HGMERGE=false
我们告诉Mercurial的合并状态机,如果它检测到不能自己解决的冲突的话,就运行命令false(像我们希望的一样,立即失败返回)。
如果现在我们运行hg merge,他会停止运行同时报告一条错误。
$
hg merge
merging myfile.txt merging myfile.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
即使我们没有注意到合并失败,Mercurial也会阻止我们意外地提交失败的合并结果。
$
hg commit -m 'Attempt to commit a failed merge'
abort: unresolved merge conflicts (see hg resolve)
这种情况下,hg commit失败的时候,它建议我们使用陌生的hg resolve命令。和以前一样, hg help resolve会输出帮助的摘要。
合并发生时,大多数文件没有任何变化。Mercurial对于每个需要进行操作的文件,都会跟踪它的状态。
如果Mercurial在合并后发现任何文件处于未解决状态,它会认为这次合并失败。幸运的是,我们不需要再次从头开始进行合并。
hg resolve的--list
或者-l
选项会打印出每个合并过的文件的状态。
$
hg resolve -l
U myfile.txt
在hg
resolve的输出中,已经解决的文件标识为R
,而未解决文件标识为U
。
如果有任何文件被标识为U
,那么我们就不能提交合并的结果。
我们有几种方法将文件从未解决状态变成解决状态。至今为止最常用的命令是重新运行hg
resolve。如果我们将文件名或者目录名传递给它,那么它会重新在给定为止合并任何未解决的文件。如果我们将--all
或者-a
选项传给它,那么它会重新合并所有的未解决文件。
Mercurial also lets us modify the resolution state of a file directly. We
can manually mark a file as resolved using the --mark
option, or as unresolved using the
--unmark
option. This allows us to
clean up a particularly messy merge by hand, and to keep track of our
progress with each file as we go.
缺省情况下,hg diff命令的输出与普通的diff命令兼容,但是这样有缺点。
$
hg rename a b
$
hg diff
diff -r 1c08e0ba14c4 a --- a/a Thu Mar 17 02:59:49 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -a diff -r 1c08e0ba14c4 b --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/b Thu Mar 17 02:59:49 2011 +0000 @@ -0,0 +1,1 @@ +a
我们给一个文件改名,而hg diff的输出却掩盖了事实。hg
diff命令可以接受选项--git
或者-g
,使用新的差异格式以更加可读的方式显示这些信息。
$
hg diff -g
diff --git a/a b/b rename from a rename to b
这个选项在以下情况下十分有用,否则就很令人费解:一个文件如果用hg status查看,显示的状态是被修改了,但是如果用hg diff检查,却什么输出也没有。如果我们修改了文件的执行属性就会出现这种情况。
$
chmod +x a
$
hg st
M a$
hg diff
The normal diff command pays no attention to file
permissions, which is why hg diff prints
nothing by default. If we supply it with the -g
option, it
tells us what really happened.
$
hg diff -g
diff --git a/a b/a old mode 100644 new mode 100755
版本控制系统最擅长管理人们书写的文本文件,例如源代码,不同的版本间文件的改动不会很大。集中式版本控制系统可以很好的出来二进制文件,例如位图文件。
例如,一个典型的游戏开发团队不仅要在版本控制系统中管理源代码,还要管理二进制财产(像地理数据,贴图,地图布局)。
因为一般不可能合并两个冲突的二进制文件,集中式系统通常提供文件锁机制来解决这个问题。它允许一个用户说 “ 我是唯一能修改这个文件的人”。
和集中式系统相比,在分布式版本控制系统中,决定要管理那些文件和怎么管理的指导思路发生了变化。
例如,在本质上,一个分布式版本控制系统不能提供文件锁定机制。没有内建的机制可以防止两个人对一个二进制文件作相互冲突的修改。如果在你的团队中,有些人要频繁的编辑二进制文件,那么这时就不适合使用Mercurial—或者任何其他的分布式版本控制系统—来管理这些文件。
在存储文件的变更的时候,Mercurial通常只会保存当前版本和上一个版本之间的差异。对于大多数文本文件而言,这时非常高效的。但是有些文件(特别是二进制文件)而言,对于文件逻辑内容上的很小的改动,可能导致文件中很多或者绝大多数字节发生变化。例如,压缩文件就会这样。如果一个文件连续的版本之间的差异总是很大,Mercurial就不能有效的存储文件的版本历史。这会影响本地的存储需求和克隆版本库需要的时间。
要了解在这实际的使用中的影响,假设你准备用Mercurial管理一个OpenOffice文档。OpenOffice使用zip压缩的格式存储文件。即使你的文档在OpenOffice中仅仅修改了一个字符,在你存档的时候几乎文件中的每个字节都发生了变化。现在假设文件大小为2MB。因为每次你存档的时候文件中大部分都发生了变化,Mercurial不得不在你每次提交的时候都存储所有2MB的文件,即使是从你的角度来说,每次的变化只有几个单词。一个不符合Mercurial存储假设的频繁编辑的文件,将很快使版本库的膨胀。
更加糟糕的是,如果你和其他人都在编辑一个OpenOffice文档,没有什么办法合并你的工作。实际上,也没有办法告诉你不同变更之间的差异。
因为Mercurial在每个克隆中都含有完整的历史拷贝,所以在一个项目中,每个使用Mercurial进行协作的人都可以在灾难发生的时候成为备份。如果中央版本库发生故障,你可以从一个贡献者那里克隆版本库作为替代,然后可以拖出其他版本库没有的变更。
It is simple to use Mercurial to perform off-site backups and remote mirrors. Set up a periodic job (e.g. via the cron command) on a remote server to pull changes from your master repositories every hour. This will only be tricky in the unlikely case that the number of master repositories you maintain changes frequently, in which case you'll need to do a little scripting to refresh the list of repositories to back up.
If you perform traditional backups of your master repositories to tape or
disk, and you want to back up a repository named
myrepo
, use hg clone -U myrepo
myrepo.bak to create a clone of myrepo
before
you start your backups. The -U
option doesn't check out a
working directory after the clone completes, since that would be superfluous
and make the backup take longer.
If you then back up myrepo.bak
instead of
myrepo
, you will be guaranteed to have a consistent
snapshot of your repository that won't be pushed to by an insomniac
developer in mid-backup.