我已经使用Subversion很长一段时间来进行个人项目管理。
越来越多的人在赞美Git、Mercurial和分布式版本控制系统(DVCS)。
我想试试整个DVCS,但我对这两个选项都不太熟悉。
Mercurial和Git之间有哪些区别?
注:我并不是试图找出哪一个是“最好”的,甚至不是应该从哪一个开始。 我主要关注它们相似的和不同的核心领域,因为我对它们在实施和哲学上的差异感兴趣。
我已经使用Subversion很长一段时间来进行个人项目管理。
越来越多的人在赞美Git、Mercurial和分布式版本控制系统(DVCS)。
我想试试整个DVCS,但我对这两个选项都不太熟悉。
Mercurial和Git之间有哪些区别?
注:我并不是试图找出哪一个是“最好”的,甚至不是应该从哪一个开始。 我主要关注它们相似的和不同的核心领域,因为我对它们在实施和哲学上的差异感兴趣。
免责声明:我使用Git,并在git邮件列表上跟踪Git的发展,甚至对Git做出了一些贡献(主要是gitweb)。我从文档和FreeNode上的#revctrl IRC频道的讨论中了解了一些关于Mercurial的知识。
感谢所有在#mercurial IRC频道上为本文提供帮助的人们。
这里最好有一些表格的语法,类似于PHPMarkdown / MultiMarkdown / Markdown的Maruku扩展
.hgtags
文件来管理每个仓库的标签,并且还支持在.hg/localtags
中创建本地标签;Git中的标签是存储在refs/tags/
命名空间下的引用,默认情况下在获取时会自动跟踪,并需要显式推送。git bisect
命令的启发,而git bundle
的想法则来自于hg bundle
。
git gc
进行维护(以减少磁盘空间并提高性能),尽管现在Git会自动执行此操作。(这种方法提供了更好的存储库压缩。)区别:
在Git中,树对象形成一个分层结构; 在Mercurial中,清单文件是一个平面结构。在Git中,Blob对象存储文件内容的一个版本;在Mercurial中,Filelog存储一个单个文件的整个历史记录(如果我们不考虑重命名等任何复杂情况)。这意味着在操作领域上,Git比Mercurial更快,其他所有事项都相等(如合并或显示项目历史记录),而Mercurial比Git更快的领域也存在(如应用补丁或显示单个文件的历史记录)。对于终端用户来说,这个问题可能不重要。
由于Mercurial的变更日志结构采用固定记录结构,因此Mercurial中的提交只能有最多两个父级;而Git中的提交可以有两个以上的父级(也称为“章鱼合并”)。尽管您可以(理论上)通过一系列双亲合并来替换章鱼合并,在Mercurial和Git仓库之间进行转换时可能会导致复杂性。
据我所知,Mercurial没有类似于Git的带注释标签(标签对象)。 带注释标签的一个特殊情况是签名标签(带有PGP / GPG签名); 在Mercurial中可以使用GpgExtension来实现相当的功能,该扩展与Mercurial一起分发。 无法像在Git中那样对非提交对象进行标记,但我认为这并不是非常重要(某些git存储库使用已标记的blob来分发公共PGP密钥以用于验证签名标签)。在Git中,引用(分支,远程跟踪分支和标签)位于提交DAG之外(应该如此)。 refs/heads/
命名空间中的引用(本地分支)指向提交,并通常由“git commit”更新; 它们指向分支的末尾(head),因此是这样的名称。 refs/remotes/<remotename>/
命名空间中的引用(远程跟踪分支)指向提交,在远程存储库<remotename>
中跟随分支,并通过“git fetch”或等效方式进行更新。 refs/tags/
命名空间中的引用(标签)通常指向提交(轻量级标签)或标签对象(带注释和签名的标签),并且不打算更改。
.hgtags
文件中。这有两个后果:首先,Mercurial必须使用此文件的特殊规则来获取所有标签的当前列表并更新此类文件(例如,它读取文件的最近提交修订版本,而不是当前已检出的版本);其次,您必须提交更改以使新标签对其他用户/其他存储库可见(就我所知)。hg/localtags
中的本地标签,这些标签对其他人不可见(当然也不可转移)。refs/tags/
名称空间中。默认情况下,当获取或推送一组修订时,git会自动获取或推送指向正在获取或推送的修订版本的标签。尽管如此,您可以在一定程度上控制获取或推送哪些标签。heads
、remotes
或tags
之外的其他命名空间,例如local-tags
用于本地标签。.hgtags
(树中的文件是可转移的,但普通文件是有版本的),或者有仅本地的标签(.hg/localtags
是非版本化的,但不可转移)。refs/heads/
命名空间中,因此'master'分支的完全限定名称是'refs/heads/master'。.hg/bookmarks
文件复制到远程存储库。您还可以使用hg id -r <bookmark> <url>
来获取当前书签的最新修订版本ID。refs/heads/*:refs/remotes/origin/*
映射意味着可以在'origin/master'远程跟踪分支('refs/remotes/origin/master')中找到远程存储库中'master'分支('refs/heads/master')的状态。
Mercurial还有所谓的命名分支,其中分支名称被嵌入提交中(在更改集中)。这样的名称是全局的(在提取时传输)。这些分支名称永久记录为更改集元数据的一部分。使用现代Mercurial,可以关闭“命名分支”并停止记录分支名称。在此机制中,分支的提示是即时计算的。
我认为Mercurial的“命名分支”应该称为提交标签,因为它们就是这样。在某些情况下,“命名分支”可能会有多个提示(多个无子节点的提交),还可以由几个不相交的修订图部分组成。
在Git中没有类似Mercurial的"内嵌分支"的等价物;此外,Git的哲学是,虽然可以说分支包含某些提交,但这并不意味着提交属于某个分支。
请注意,Mercurial文档仍建议至少为长期存在的分支(每个存储库工作流程中的单个分支)使用单独的克隆(单独的存储库),也称为通过克隆进行分支。
Mercurial默认推送所有头。如果您想要推送单个分支(单个头),则必须指定要推送的分支的末尾修订版本。您可以通过修订号(本地到存储库)、修订标识符、书签名称(本地到存储库,不会传输)或嵌入式分支名称(命名分支)来指定分支末尾。
据我所知,如果您推送包含被标记为某个"命名分支"的提交范围的修订版本,则将在您推送到的存储库中拥有此"命名分支"。这意味着这些嵌入式分支("命名分支")的名称是全局的(对于给定存储库/项目的克隆而言)。
默认情况下(取决于“push.default”配置变量),“git push”或“git push ”将推送匹配的分支,即仅推送本地分支中已经存在于您要推送到的远程存储库中的分支。您可以使用“--all”选项进行git-push(“git push --all”)以推送所有分支,您可以使用“git push ”来推送给定的单个分支,您可以使用“git push HEAD”来推送当前分支。hg pull --rev <rev> <url>
"或"hg pull <url>#<rev>
"指定要获取的分支来获取单个分支。您可以使用修订标识符、"命名分支"名称(嵌入在更改日志中的分支)或书签名称来指定<rev>。然而,书签名称(至少目前)不会被传输。您获取的所有"命名分支"修订版本都属于被传输的范畴。"hg pull"将其获取的分支的提示存储为匿名未命名头。git fetch
"(或"git fetch <remote>
")会从远程仓库获取所有分支(从refs/heads/
名称空间),并将它们存储在refs/remotes/
名称空间中。这意味着例如在远程'origin'中命名为'master'(完整名称:'refs/heads/master')的分支将被存储(保存)为'origin/master' 远程跟踪分支(完整名称:'refs/remotes/origin/master')。git fetch <remote> <branch>
来获取Git中的单个分支- Git将在FETCH_HEAD中存储所请求的分支,这类似于Mercurial未命名的头。在Git中,有许多命名修订版本的方法(例如在git rev-parse手册中描述):
^
表示提交对象的第一个父级,^n
表示合并提交的第n个父级。对于修订参数的后缀~n
表示直接第一个父级线路上提交的第n个祖先。这些后缀可以组合形成从符号参考路径后面的修订说明符,例如'pu~3^2~3'还有涉及reflog的修订说明符,此处未提及。在Git中,每个对象(提交、标签、树或blob)都有其SHA-1标识符;有特殊的语法,例如'next:Documentation'或'next:README'来引用指定修订版本中的树(目录)或blob(文件内容)。
Mercurial还有许多命名变更集的方式(例如在hg手册中描述):
差异
如上列表所示,Mercurial提供了本地于存储库的修订号,而Git则不提供。另一方面,Mercurial仅提供相对于“tip”(当前分支)的相对偏移量,这些相对偏移量是存储库本地的(至少没有ParentrevspecExtension),而Git允许指定从任何tip后面的任何提交。
A..B
语法指线性历史中从A(不包括A)开始到B结束的修订范围,是从下方打开的范围缩写("语法糖"),它等同于^A B
,对于历史遍历命令,它表示所有从B可达但不从A可达的提交。这意味着即使A不是B的祖先,A..B
范围的行为也是完全可预测的(并且非常有用):A..B
表示从A和B的公共祖先(合并基础)到修订B的修订范围。A:B
语法指定范围,与 Git 不同的是,该范围作为一个闭区间。同时,B:A范围是A:B范围的反向顺序,在 Git 中不是这样(但请参见下面关于A...B
语法的说明)。但这种简单性是有代价的:只有在A是B的祖先或反之亦然时,修订范围A:B才有意义,即仅适用于线性历史;否则(我猜)范围是不可预测的,并且结果仅限于存储库(因为修订号局限于存储库)。Git还使用符号A...B
表示修订版本的对称差异;它的意思是A B --not $(git merge-base A B)
,这意味着从A或B中可达的所有提交,但不包括从它们两者都可达的所有提交(从公共祖先可达)。
Mercurial使用重命名跟踪来处理文件重命名。这意味着文件被重命名的信息保存在提交时; 在Mercurial中,此信息以filelog(文件revlog)元数据的“增强diff”形式保存。其结果是你必须使用hg rename
/ hg mv
... 或者你需要记住运行hg addremove
来执行基于相似性的重命名检测。
Git在版本控制系统中是独一无二的,因为它使用重命名检测来处理文件重命名。这意味着在需要时检测到文件被重命名:当进行合并时,或者显示差异时(如果请求/配置)。这样做的好处是可以改进重命名检测算法,并且不会在提交时冻结。
Git和Mercurial都需要使用--follow
选项来跟踪单个文件的历史记录中的重命名。在git blame
/ hg annotate
中显示文件的逐行历史记录时,它们都可以跟踪重命名。
git blame
命令能够跟踪代码移动,甚至可以将代码从一个文件移动(或复制)到另一个文件,即使代码移动不是作为整个文件重命名的一部分。据我所知,这个功能在Git中是独特的(截至2009年10月)。
hg-serve
或Mercurial CGI脚本,并且需要在服务器机器上安装Mercurial。git-daemon
实现)访问,需要在服务器上安装git。这些协议中的交换是客户端和服务器协商它们共有的对象,然后生成并发送packfile。现代Git包括对"智能"HTTP协议的支持。git update-server-info
生成的额外信息(通常从钩子运行)。交换包括客户端遍历提交链,并根据需要下载松散的对象和packfiles。缺点是它会下载比严格要求更多的内容(例如,在只有单个packfile的极端情况下,即使仅获取几个修订版本,它也会被整个下载),并且可能需要许多连接才能完成。Mercurial是用Python实现的,一些核心代码是为了性能而用C编写的。它提供API来编写扩展(插件)作为添加额外功能的一种方式。其中一些功能,如"书签分支"或签署修订版本,由Mercurial分发的扩展提供,并需要启用它。
Git是用C、Perl和shell脚本实现的。Git提供了许多适用于脚本的低级命令(plumbing)。引入新功能的通常方法是将其编写为Perl或shell脚本,并在用户界面稳定后以C进行重写,以提高性能、可移植性,并避免shell脚本的角落情况(此过程称为builtinification)。简而言之
我认为通过观看这两个视频,您可以感受到这些系统在设计上的相似之处和不同之处:
Linus Torvalds 关于 Git 的演讲 (http://www.youtube.com/watch?v=4XpnKHJAok8)
Bryan O'Sullivan 关于 Mercurial 的演讲 (http://www.youtube.com/watch?v=JExtkqzEoHY)
它们在设计上非常相似,但在实现上却非常不同。
我使用 Mercurial。据我所知,Git 不同的一个主要特点是它跟踪文件内容而不是文件本身。Linus说,如果你将一个函数从一个文件移动到另一个文件,Git会告诉你这个单个函数在移动过程中的历史记录。
他们还说,Git 在 HTTP 上运行较慢,但它有自己的网络协议和服务器。
Git 作为 SVN 的厚客户端比 Mercurial 更好用。您可以对 SVN 服务器进行拉取和推送。Mercurial 中的此功能仍在开发中
Mercurial 和 Git 都有非常好的网页托管解决方案(BitBucket 和 GitHub),但 Google Code 仅支持 Mercurial。顺便说一下,他们为了决定支持哪一个 DVCS,进行了非常详细的 Mercurial 和 Git 比较分析 (http://code.google.com/p/support/wiki/DVCSAnalysis)。里面有很多有用的信息。
我经常同时使用这两种版本控制工具,它们之间的主要功能区别在于Git和Mercurial在存储库中命名分支的方式不同。对于Mercurial,分支名称与其变更集一起被克隆和拉取。当你在Mercurial中向一个新分支添加变更并将其推送到另一个存储库时,分支名称也会同时被推送。因此,在Mercurial中,分支名称是全局的,你必须使用书签扩展来拥有本地轻量级分支名称(如果你需要的话)。默认情况下,Mercurial使用匿名轻量级代码行,称为“heads”。而在Git中,分支名称及其到远程分支的唯一映射是本地存储的,你必须显式地管理它们,这意味着要知道如何做。这基本上就是Git比Mercurial更难学习和使用的原因。
正如其他人在这里提到的,还有很多次要的差异。但分支的问题是最大的区别所在。
╔═════════════════════════════╦════════════════════════════════════════════════════════════════════════════════════════════════╗
║ Git ║ Mercurial ║
╠═════════════════════════════╬════════════════════════════════════════════════════════════════════════════════════════════════╣
║ git pull ║ hg pull -u ║
║ git fetch ║ hg pull ║
║ git reset --hard ║ hg up -C ║
║ git revert <commit> ║ hg backout <cset> ║
║ git add <new_file> ║ hg add <new_file> (Only equivalent when <new_file> is not tracked.) ║
║ git add <file> ║ Not necessary in Mercurial. ║
║ git add -i ║ hg record ║
║ git commit -a ║ hg commit ║
║ git commit --amend ║ hg commit --amend ║
║ git blame ║ hg blame or hg annotate ║
║ git blame -C ║ (closest equivalent): hg grep --all ║
║ git bisect ║ hg bisect ║
║ git rebase --interactive ║ hg histedit <base cset> (Requires the HisteditExtension.) ║
║ git stash ║ hg shelve (Requires the ShelveExtension or the AtticExtension.) ║
║ git merge ║ hg merge ║
║ git cherry-pick <commit> ║ hg graft <cset> ║
║ git rebase <upstream> ║ hg rebase -d <cset> (Requires the RebaseExtension.) ║
║ git format-patch <commits> ║ hg email -r <csets> (Requires the PatchbombExtension.) ║
║ and git send-mail ║ ║
║ git am <mbox> ║ hg mimport -m <mbox> (Requires the MboxExtension and the MqExtension. Imports patches to mq.) ║
║ git checkout HEAD ║ hg update ║
║ git log -n ║ hg log --limit n ║
║ git push ║ hg push ║
╚═════════════════════════════╩════════════════════════════════════════════════════════════════════════════════════════════════╝
Mercurial几乎完全由Python编写而成。Git的核心是用C语言编写的(应该比Mercurial更快),工具则是用sh、perl、tcl编写并使用标准GNU utils。因此,它需要将所有这些实用程序和解释器带到不包含它们的系统中(例如Windows)。
两者都支持与SVN一起使用,尽管据我所知,git在Windows上对SVN的支持存在问题(也许只是我不走运/笨拙,谁知道呢)。还有一些扩展可以允许git和Mercurial之间的交互操作。
Mercurial拥有很好的Visual Studio集成。上次我检查时,Git插件正在工作,但速度非常慢。
它们的基本命令集非常相似(init,clone,add,status,commit,push,pull等)。因此,基本工作流程将是相同的。此外,两者都有类似于TortoiseSVN的客户端。
Mercurial的扩展可以使用Python编写(不足为奇!),而对于git,扩展可以以任何可执行形式编写(可执行二进制文件、shell脚本等)。一些扩展非常强大,例如git bisect
。
底线:如果你有一个大团队正在开发单个巨大的应用程序,请使用Git;如果你的单个应用程序很小,并且规模是由这些应用程序的数量而不是大小决定的,请使用Mercurial。
请查看一段时间前Scott Chacon的文章。
我认为git有一个“更复杂”的声誉,但在我的经验中,它并不比必须的更复杂。 在我看来,git模型要容易理解得多(标记包含提交(以及指向零个或多个父提交的指针)包含树包含块和其他树... 完成)。
不仅是我的经验表明git并不比mercurial更令人困惑。 我建议再次阅读Scott Chacon关于此事的博客文章。
.hgtags
中的1.0标签而感到困惑。然而,您不需要查看.hgtags
,您会发现hg tags
仍然列出所有标签。此外,这种行为是将标签存储在版本控制文件中的简单结果 - 再次强调,该模型易于理解并且非常可预测。 - Martin Geisler有一个与DVCS本身无关的显著区别:
Git在C开发者中似乎非常受欢迎。Git是Linux内核的事实上存储库,这可能是它在C开发者中如此受欢迎的原因。对于那些只在Linux / Unix世界中工作的人来说尤其如此。
Java开发人员似乎更喜欢Mercurial而不是Git。可能有两个原因:一是一些非常大的Java项目托管在Mercurial上,包括JDK本身。另一个原因是Mercurial的结构和干净的文档吸引了来自Java领域的人,而这些人认为Git在命令命名方面不一致且缺乏文档。我并不是说这是真的,我是说人们已经习惯了从他们的日常环境中得到的东西,然后他们倾向于从中选择DVCS。
Python开发人员几乎全部支持Mercurial,我认为。实际上没有任何理性的理由支持这一点,除了Mercurial基于Python。 (我也使用Mercurial,我真的不明白为什么人们会对DVCS的实现语言大惊小怪。我不懂Python的话,如果不是因为某个地方列出它是基于Python的,我就不会知道)。
我不认为您可以说一种DVCS适合某种语言而另一种不适合,因此您不应该从中选择。但实际上,人们选择(部分原因)是基于他们所在社区中最多接触到的DVCS。
(不,我没有使用统计数据来支持我以上的观点...这完全是基于我的主观看法)