Git和Mercurial - 比较与对比

524

我已经使用Subversion很长一段时间来进行个人项目管理。

越来越多的人在赞美Git、Mercurial和分布式版本控制系统(DVCS)。

我想试试整个DVCS,但我对这两个选项都不太熟悉。

Mercurial和Git之间有哪些区别?

注:我并不是试图找出哪一个是“最好”的,甚至不是应该从哪一个开始。 我主要关注它们相似的和不同的核心领域,因为我对它们在实施和哲学上的差异感兴趣。


5
请参见http://stackoverflow.com/questions/995636/popularity-of-git-mercurial-bazaar-vs-which-to-recommend/995799#995799。 - VonC
2
可能是Mercurial和Git之间的区别是什么?的重复问题。 - nawfal
9个回答

456

免责声明:我使用Git,并在git邮件列表上跟踪Git的发展,甚至对Git做出了一些贡献(主要是gitweb)。我从文档和FreeNode上的#revctrl IRC频道的讨论中了解了一些关于Mercurial的知识。

感谢所有在#mercurial IRC频道上为本文提供帮助的人们。



概述

这里最好有一些表格的语法,类似于PHPMarkdown / MultiMarkdown / Markdown的Maruku扩展

  • 仓库结构:Mercurial不允许使用多个父节点进行合并(八爪鱼合并),也不允许标记非提交对象。
  • 标签:Mercurial使用带有特殊规则的版本化.hgtags文件来管理每个仓库的标签,并且还支持在.hg/localtags中创建本地标签;Git中的标签是存储在refs/tags/命名空间下的引用,默认情况下在获取时会自动跟踪,并需要显式推送。
  • 分支:在Mercurial中,基本工作流程基于匿名头;Git使用轻量级命名分支,并具有特殊类型的分支(远程跟踪分支)来跟踪远程仓库中的分支。
  • 修订版本命名和范围:Mercurial提供了本地仓库的修订版本号,并基于此本地编号计算相对修订版本(从tip开始计数,即当前分支),以及修订版本范围;Git提供了一种相对于分支tip引用修订版本的方式,并且修订版本范围是基于修订版本的图形拓扑结构。
  • Mercurial使用重命名追踪,而Git使用重命名检测来处理文件重命名。
  • 网络:Mercurial支持SSH和HTTP“智能”协议以及静态HTTP协议;现代Git支持SSH、HTTP和GIT“智能”协议以及HTTP(S)“愚蠢”协议。两者都支持离线传输的包文件。
  • Mercurial使用扩展(插件)和已建立的API;Git具有可编程性和已建立的格式。

有一些事情使Mercurial与Git不同,但也有其他事情使它们相似。这两个项目都从彼此那里借鉴了一些想法。例如,在Mercurial中,bisect extension之前的命令受到Git中git bisect命令的启发,而git bundle的想法则来自于hg bundle

代码库结构,存储修订记录

在Git中,其对象数据库中有四种类型的对象:blob对象包含文件内容,分层的tree对象存储目录结构,包括文件名和文件权限的相关部分(文件的可执行权限、符号链接),commit对象包含作者信息,指向代表提交时的存储库状态快照的树对象(通过项目顶级目录的树对象)以及对零个或多个父提交的引用,以及tag对象引用其他对象并可以使用PGP / GPG进行签名。
Git使用两种存储对象的方式:loose格式,其中每个对象都存储在单独的文件中(这些文件只写入一次,从未修改),以及packed格式,其中许多对象在单个文件中进行增量压缩存储。操作的原子性是通过在写入对象后写入新对象的引用(使用create + rename技巧进行原子操作)来提供的。
Git代码库需要定期使用git gc进行维护(以减少磁盘空间并提高性能),尽管现在Git会自动执行此操作。(这种方法提供了更好的存储库压缩。)
据我所知,Mercurial将文件的历史记录存储在一个称为filelog的文件中(与重命名跟踪和一些辅助信息一起),它使用称为manifest的扁平结构来存储目录结构,并且使用称为changelog的结构来存储有关变更集(修订版)的信息,包括提交消息和零、一个或两个父级。
Mercurial使用transaction journal来提供操作的原子性,并依赖于截断文件来在失败或中断操作后进行清理。修订日志是只追加的。
从Git和Mercurial中的代码库结构来看,可以发现Git更像对象数据库(或内容寻址文件系统),而Mercurial更像传统的固定字段关系数据库。

区别:
在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/命名空间中的引用(标签)通常指向提交(轻量级标签)或标签对象(带注释和签名的标签),并且不打算更改。

标签

在Mercurial中,您可以使用tag为修订版本提供持久名称;标签被类似于忽略模式的方式存储。这意味着全局可见的标签存储在您的存储库中受修订版本控制的.hgtags文件中。这有两个后果:首先,Mercurial必须使用此文件的特殊规则来获取所有标签的当前列表并更新此类文件(例如,它读取文件的最近提交修订版本,而不是当前已检出的版本);其次,您必须提交更改以使新标签对其他用户/其他存储库可见(就我所知)。
Mercurial还支持存储在hg/localtags中的本地标签,这些标签对其他人不可见(当然也不可转移)。
在Git中,标签是指向其他对象(通常是标记对象,这些对象反过来指向提交)的固定(常量)命名引用,存储在refs/tags/名称空间中。默认情况下,当获取或推送一组修订时,git会自动获取或推送指向正在获取或推送的修订版本的标签。尽管如此,您可以在一定程度上控制获取或推送哪些标签
Git将轻量级标签(直接指向提交)和注释标签(指向包含标签消息的标记对象,该消息可选包括PGP签名,反过来指向提交)略有不同地处理,例如,默认情况下,它仅在使用“git describe”描述提交时才考虑已注释的标签。
Git中没有严格等同于Mercurial本地标签的东西。然而,Git最佳实践建议设置单独的公共裸仓库,将准备好的更改推送到其中,其他人从其中克隆和提取。这意味着您不推送的标签(和分支)是私有的,属于您的存储库。另一方面,您还可以使用命名空间headsremotestags之外的其他命名空间,例如local-tags用于本地标签。
个人观点:我认为标签应该在修订图之外,因为它们与修订图外部相关(它们是指向修订图的指针)。标签应该是非版本化的,但可转移的。Mercurial选择使用类似于忽略文件的机制,这意味着它必须特殊处理.hgtags(树中的文件是可转移的,但普通文件是有版本的),或者有仅本地的标签(.hg/localtags是非版本化的,但不可转移)。
分支
在Git中,本地分支(分支尖端或分支头)是指向提交的命名引用,可以在其中增加新的提交。分支也可以表示活动开发线,即从分支尖端可达的所有提交。本地分支驻留在refs/heads/命名空间中,因此'master'分支的完全限定名称是'refs/heads/master'。
Git中的当前分支(表示已检出的分支和新提交将进入的分支)是由HEAD引用引用的分支。一个人可以直接将HEAD指向提交,而不是符号引用;处于匿名未命名分支的这种情况称为分离的HEAD(“git branch”显示您在“(无分支)”上)。
在Mercurial中有匿名分支(分支头),可以使用书签(通过bookmark extension)。这样的书签分支纯粹是本地的,这些名称(直到版本1.6)不能使用Mercurial进行传递。您可以使用rsync或scp将.hg/bookmarks文件复制到远程存储库。您还可以使用hg id -r <bookmark> <url>来获取当前书签的最新修订版本ID。
自1.6版本以来,书签可以推送/拉取。BookmarksExtension页面有一个关于Working With Remote Repositories的部分。在Mercurial中,书签名称是全局的,而Git中的'remote'定义还描述了从远程仓库的名称到本地远程跟踪分支名称的映射;例如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”来推送当前分支。
上述所有内容都假定Git未配置哪些分支通过“remote..push”配置变量进行推送。
获取分支
注意:在此处我使用Git术语,“fetch”表示从远程存储库下载更改而不将这些更改与本地工作集成。这就是“git fetch”和“hg pull”的作用。
如果我理解正确的话,默认情况下Mercurial会从远程仓库获取所有头信息,但是你可以通过"hg pull --rev <rev> <url>"或"hg pull <url>#<rev>"指定要获取的分支来获取单个分支。您可以使用修订标识符、"命名分支"名称(嵌入在更改日志中的分支)或书签名称来指定<rev>。然而,书签名称(至少目前)不会被传输。您获取的所有"命名分支"修订版本都属于被传输的范畴。"hg pull"将其获取的分支的提示存储为匿名未命名头。
在Git中,默认情况下(对于由"git clone"创建的'origin'远程和使用"git remote add"创建的远程),"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未命名的头。
这些只是强大的refspec Git语法的默认示例:使用refspecs,您可以指定和/或配置要获取哪些分支以及在何处存储它们。例如,“获取所有分支”默认情况表示为“+refs/heads/*:refs/remotes/origin/*”通配符refspec,“获取单个分支”是“refs/heads/<branch>:”的简写形式。 Refspecs用于将远程存储库中的分支(refs)名称映射到本地refs名称。但是,要能够有效地使用Git,您不需要(太多)了解refspecs(主要感谢“git remote”命令)。
个人观点:我个人认为Mercurial中的“命名分支”(其中分支名称嵌入在变更集元数据中)是误导性设计,尤其是对于分布式版本控制系统。例如,假设Alice和Bob都在他们的存储库中拥有名为'for-joe'的“命名分支”,这些分支没有任何共同之处。但是,在Joe的存储库中,这两个分支将被错误地视为单个分支。因此,您必须想出一种约定以防止分支名称冲突。这不是Git的问题,在其中,Alice的'for-joe'分支将是'alice/for-joe',而Bob的分支将是'bob/for-joe'。请参见Mercurial wiki上提出的Separating branch name from branch identity问题。
Mercurial的“书签分支”目前缺乏核心分发机制。 区别:
这是Mercurial和Git之间的主要区别之一,正如james woodyattSteve Losh在他们的回答中所说。Mercurial默认使用匿名轻量级代码行,其术语称为“heads”。Git使用轻量级命名分支,并使用一个单射映射将远程存储库中分支的名称映射到远程跟踪分支的名称。Git“强制”您命名分支(除了单个未命名分支的情况,称为分离的HEAD),但我认为这对于分支繁重的工作流(例如主题分支工作流)更有效,意味着在单个存储库范例中有多个分支。

命名修订版本

在Git中,有许多命名修订版本的方法(例如在git rev-parse手册中描述):

  • 完整的SHA1对象名称(40字节的十六进制字符串),或者是在存储库中唯一的子字符串
  • 符号引用名称,例如'master'(指向'master'分支),或'v1.5.0'(指向标签),或'origin/next'(指向远程跟踪分支)
  • 对于修订参数的后缀^表示提交对象的第一个父级,^n表示合并提交的第n个父级。对于修订参数的后缀~n表示直接第一个父级线路上提交的第n个祖先。这些后缀可以组合形成从符号参考路径后面的修订说明符,例如'pu~3^2~3'
  • "git describe"的输出,即最近的标签,可选地后跟破折号和一定数量的提交,后跟破折号、'g'和简写对象名称,例如'v1.6.5.1-75-g5bf8097'。

还有涉及reflog的修订说明符,此处未提及。在Git中,每个对象(提交、标签、树或blob)都有其SHA-1标识符;有特殊的语法,例如'next:Documentation'或'next:README'来引用指定修订版本中的树(目录)或blob(文件内容)。

Mercurial还有许多命名变更集的方式(例如在hg手册中描述):

  • 普通整数被视为修订号。需要记住,修订号是“特定存储库”的本地号码;在其他存储库中,它们可能不同。
  • 负整数被视为相对于tip的顺序偏移量,其中-1表示tip,-2表示tip之前的修订版本,依此类推。它们也是“本地”的存储库。
  • 唯一的修订标识符(40位十六进制字符串)或其唯一前缀。
  • 标签名称(与给定修订版本相关联的符号名称),或书签名称(带有扩展名:与给定头部相关联的符号名称,本地存储库),或“命名分支”(提交标签;由“命名分支”给出的修订版本是具有给定提交标签的所有提交的提示(无子提交),如果有多个这样的提示,则具有最大修订号)
  • 保留名称“tip”是一个特殊标签,始终标识最新的修订版本。
  • 保留名称“null”表示空修订版本。
  • 保留名称“.”表示工作目录父级。

差异
如上列表所示,Mercurial提供了本地于存储库的修订号,而Git则不提供。另一方面,Mercurial仅提供相对于“tip”(当前分支)的相对偏移量,这些相对偏移量是存储库本地的(至少没有ParentrevspecExtension),而Git允许指定从任何tip后面的任何提交。

Git中最新的修订版本被称为HEAD,而在Mercurial中被称为“tip”;Git中没有空修订版本。Mercurial和Git都可以有多个根(可以有多个没有父级的提交;这通常是以前分离的项目合并的结果)。
另请参阅:Elijah's Blog上关于许多不同类型的修订说明符文章(newren's)。
个人意见:我认为修订号(至少对于分布式开发和/或非线性/分支历史)被高估了。首先,对于分布式版本控制系统,它们必须要么是本地存储库,要么需要将某些存储库视为特殊方式处理为中央编号机构。其次,具有更长历史记录的较大项目可以在5位数字范围内拥有许多修订版本,因此它们仅比缩短为6-7个字符的修订标识符提供轻微优势,并且暗示着严格的排序,而修订版本只有部分排序(我的意思是修订版本n和n+1不需要是父级和子级)。
修订范围
在 Git 中,修订范围是拓扑结构的。常见的A..B语法指线性历史中从A(不包括A)开始到B结束的修订范围,是从下方打开的范围缩写("语法糖"),它等同于^A B,对于历史遍历命令,它表示所有从B可达但不从A可达的提交。这意味着即使A不是B的祖先,A..B范围的行为也是完全可预测的(并且非常有用):A..B表示从A和B的公共祖先(合并基础)到修订B的修订范围。
在 Mercurial 中,修订范围基于修订号的范围。使用A:B语法指定范围,与 Git 不同的是,该范围作为一个闭区间。同时,B:A范围是A:B范围的反向顺序,在 Git 中不是这样(但请参见下面关于A...B语法的说明)。但这种简单性是有代价的:只有在A是B的祖先或反之亦然时,修订范围A:B才有意义,即仅适用于线性历史;否则(我猜)范围是不可预测的,并且结果仅限于存储库(因为修订号局限于存储库)。
Mercurial 1.6解决了这个问题,引入了新的拓扑修订范围,其中'A..B'(或'A::B')被理解为同时是X的后代和Y的祖先的变更集。我猜这相当于Git中的'--ancestry-path 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中,git blame命令能够跟踪代码移动,甚至可以将代码从一个文件移动(或复制)到另一个文件,即使代码移动不是作为整个文件重命名的一部分。据我所知,这个功能在Git中是独特的(截至2009年10月)。

网络协议

Mercurial和Git都支持从同一文件系统上的存储库获取和推送,其中存储库URL只是指向存储库的文件系统路径。两者还支持从中获取。
Mercurial支持通过SSH和HTTP协议进行获取和推送。对于SSH,需要在目标机器上拥有可访问的shell帐户和安装/可用的hg副本。对于HTTP访问,则需要运行hg-serve或Mercurial CGI脚本,并且需要在服务器机器上安装Mercurial。
Git支持两种用于访问远程存储库的协议:
  • "智能"协议,包括通过SSH和自定义git://协议(由git-daemon实现)访问,需要在服务器上安装git。这些协议中的交换是客户端和服务器协商它们共有的对象,然后生成并发送packfile。现代Git包括对"智能"HTTP协议的支持。
  • "愚蠢"协议,包括HTTP和FTP(仅用于获取),以及HTTPS(通过WebDAV进行推送)不需要在服务器上安装git,但它们需要存储库包含由git update-server-info生成的额外信息(通常从钩子运行)。交换包括客户端遍历提交链,并根据需要下载松散的对象和packfiles。缺点是它会下载比严格要求更多的内容(例如,在只有单个packfile的极端情况下,即使仅获取几个修订版本,它也会被整个下载),并且可能需要许多连接才能完成。

扩展:脚本化与扩展(插件)

Mercurial是用Python实现的,一些核心代码是为了性能而用C编写的。它提供API来编写扩展(插件)作为添加额外功能的一种方式。其中一些功能,如"书签分支"或签署修订版本,由Mercurial分发的扩展提供,并需要启用它。

Git是用CPerlshell脚本实现的。Git提供了许多适用于脚本的低级命令(plumbing)。引入新功能的通常方法是将其编写为Perl或shell脚本,并在用户界面稳定后以C进行重写,以提高性能、可移植性,并避免shell脚本的角落情况(此过程称为builtinification)。
Git依赖并围绕[存储库]格式和[网络]协议构建。与语言绑定不同,其他语言中有Git的(部分或完整)重新实现(其中一些是部分重新实现,部分是围绕git命令的包装器):JGit(Java,由EGit,Eclipse Git插件使用),Grit(Ruby),Dulwich(Python),git#(C#)。

简而言之


32
hg非常努力地防止历史重写(只能使用扩展程序mq、histedit、rebase来完成),而git则可以直接进行历史重写(似乎社区的一部分甚至还鼓励这样做)。 - tonfa
82
我认为“改写历史”的说法太过负面化。在Git中,我鼓励人们考虑他们发布的历史记录。其他人需要使用那些历史记录。没有人(甚至包括你自己)对你的“糟糕,忘记了一个文件”提交感兴趣。也没有人关心你在跟踪上游分支并开发新功能时经历的一系列合并提交。这种东西会使历史记录(以及相关工具)更难理解,并且没有提供任何价值。 - Dustin
5
@Jakub:命名分支在git中是不存在的。它只是提交描述中的一个字段(并且是历史的一部分,因此除非更改哈希等内容,否则不可变)。类似于git分支是书签(“命名头”),但它们目前不支持远程传输(拉取时不会导入远程书签)。http://stevelosh.com/blog/entry/2009/8/30/a-guide-to-branching-in-mercurial/ 很好地解释了这一点。 - tonfa
29
Mercurial最初只支持每个仓库工作流程中的一个分支,这一点很明显。不,Mercurial最初并不支持命名的分支,但你始终可以在单个仓库中拥有任意多的匿名分支。相比之下,Git使匿名分支变得非常麻烦。如果你想完成任何工作并避免被垃圾收集器回收你的工作,你几乎必须为每个小分支想一个名字。 - Steve Losh
17
@SteveLosh:你似乎认为在Mercurial中拥有许多匿名分支是一件好事,但对我来说,这似乎是可怕的。你如何区分它们?而你似乎认为在Git中命名分支是一个巨大的难题,但如果你创建分支有一个目的,那么你已经有了一个现成的名称。如果没有目的,那就不要分支。我看不出Mercurial在这里提供任何好处,只看到痛苦和混乱。 - iconoclast
显示剩余25条评论

58

我认为通过观看这两个视频,您可以感受到这些系统在设计上的相似之处和不同之处:

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)。里面有很多有用的信息。


8
我建议您阅读那个Google代码页面上的所有评论。这些信息感觉有点偏颇,并不完全符合我的经验。我喜欢Mercurial(hg),曾经广泛使用了一年左右。现在我几乎完全使用Git。我需要完成的一些事情,Git使其变得容易,而Mercurial则几乎无法实现(尽管有些人喜欢称之为“复杂化”)。基本的Git和基本的Mercurial一样简单。 - Dustin
11
Dustin,或许你可以列举一些“Git易用,Mercurial不太友好”的例子? - Gregg Lind
1
@knittl 不,它不支持。主要是因为缺乏智能HTTP协议,使得在部署时会给他们带来烦恼(大部分Google前端都是基于HTTP的)。 - tonfa
2
@tonfa:Git的智能HTTP协议目前正在开发中(即:在git邮件列表上有补丁,并且它们在git.git存储库的“pu”=提议更新分支中)。 - Jakub Narębski
@Jakub,这个协议的描述在哪里可以找到?我们可能会对hg协议进行重大更改,因此看一下其他地方已经做了什么可能会有所帮助。 - tonfa
1
@tonfa:搜索“智能HTTP”(或在git邮件列表(档案)中搜索“智能HTTP的回归”)。例如,最新的协议描述在这里:http://thread.gmane.org/gmane.comp.version-control.git/129732/focus=129734 - Jakub Narębski

27

我经常同时使用这两种版本控制工具,它们之间的主要功能区别在于Git和Mercurial在存储库中命名分支的方式不同。对于Mercurial,分支名称与其变更集一起被克隆和拉取。当你在Mercurial中向一个新分支添加变更并将其推送到另一个存储库时,分支名称也会同时被推送。因此,在Mercurial中,分支名称是全局的,你必须使用书签扩展来拥有本地轻量级分支名称(如果你需要的话)。默认情况下,Mercurial使用匿名轻量级代码行,称为“heads”。而在Git中,分支名称及其到远程分支的唯一映射是本地存储的,你必须显式地管理它们,这意味着要知道如何做。这基本上就是Git比Mercurial更难学习和使用的原因。

正如其他人在这里提到的,还有很多次要的差异。但分支的问题是最大的区别所在。


2
另外,关于Mercurial中四种分支的良好解释,请参阅此帖子:http://stevelosh.com/blog/entry/2009/8/30/a-guide-to-branching-in-mercurial/。 - Martin Geisler

12
阅读了许多关于Mercurial更易使用的言论(我仍然相信,毕竟网络社区也是这样认为的),但当我开始使用Git和Mercurial时,我发现在命令行下适应Git相对更容易些(我一开始用TortoiseHg学习了Mercurial),主要是因为对我来说,git命令的命名很恰当且数量较少。 Mercurial对每个执行不同任务的命令进行了不同的命名,而Git命令可以根据情况具有多种用途(例如,checkout)。尽管Git曾经更难学习,但现在它们之间的差异几乎可以忽略不计。如果使用像TortoiseHg这样的良好GUI客户端,则使用Mercurial会更加容易,并且我不必记住有点令人困惑的命令。我不打算详细解释每个命令如何变化,但这里有两个全面的列表:1来自Mercurial的官方网站2来自wikivs
╔═════════════════════════════╦════════════════════════════════════════════════════════════════════════════════════════════════╗
║           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                                                                                        ║
╚═════════════════════════════╩════════════════════════════════════════════════════════════════════════════════════════════════╝

Git会在内部保存每个已提交文件版本的记录,而Hg只保存可以占用更小空间的变更集。与Hg相比,Git使更改历史记录更容易,但这是一种爱恨交加的特性。我喜欢Hg的前者和Git的后者。
我在Hg中错过的是Git子模块功能。Hg有子仓库,但那并不完全是Git子模块。
两者周围的生态系统也会影响人们的选择:Git更受欢迎(但这很琐碎),Git有GitHub,而Mercurial有BitBucket,Mercurial有TortoiseHg,我没有看到与Git同样好的替代品。
每种方法都有其优点和缺点,使用任何一种都不会失去。

11

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


9
补充信息,Mercurial的核心同样是用C语言编写的(但它可能比Git的核心更小) 。 - tonfa
1
我在Windows上使用git-svn毫无问题。这是使用Cygwin的(如果你问我,在Windows上使用git的唯一正确方法)。不能代表msysgit。 - Dan Moulding
@Dan Moulding:是的,我也遇到了msysgit的问题。也许需要尝试一下cygwin端口(我之前使用cygwin的经验不太好,所以我避免使用它)。谢谢建议! - elder_george
我个人不喜欢Cygwin侵入注册表来存储用户数据。将其运行在USB键上并保持本地c:\驱动器副本同步以便在需要比USB键更快时运行是一件很麻烦的事情。 :-/ - Chris K
1
我使用了上述提到的Visual Studio的Git插件,目前版本的性能良好。它通过调用命令行工具来完成工作,因此我认为它在大型项目上不会显著降低性能。 - Stuart Ellis
@Chris Kaminski,请将您实际的安装位置替换为“W:\”,并将Cygwin安装到W:\。这样可以使其具有可移植性。 - Vi.

11
如果您需要良好的Windows支持,您可能更喜欢使用Mercurial。TortoiseHg(Windows资源管理器插件)成功地为一个相当复杂的工具提供了易于使用的图形界面。正如在这里所述,您还将获得Visual Studio插件。但是,上次我尝试时,在Windows上,SVN接口效果不佳。
如果您不介意命令行界面,我会推荐Git。不是出于技术原因,而是出于战略原因。Git的采用率要高得多。只需看看有多少著名的开源项目正在从cvs/svn转向Mercurial,以及有多少正在转向Git。看看与Mercurial托管相比,可以找到多少代码/项目托管提供商支持git。

1
如果您不喜欢使用命令行,也可以使用TortoiseGit。(但需要安装msysgit。) - Ben James
3
我们公司最终选择了Git,因为它在Windows上有很好的支持 - 请查看Git扩展。我有偏见,因为现在我是一个贡献者,但当我们开始使用它时我并不是。 - Jacob Stanley

8
我在现在的工作中使用Git已经一年多了,在之前的工作中使用Mercurial也有一年多的时间。从用户的角度来看,我将提供一些评估。
首先,两者都是分布式版本控制系统。分布式版本控制系统需要一种与传统版本控制系统不同的思维方式,但是一旦理解了它们,它们实际上在许多方面都比传统版本控制系统更好。因此,我认为Git和Mercurial都比Subversion、Perforce等传统版本控制系统优秀得多。分布式版本控制系统与传统版本控制系统之间的差异要大得多,远远超过Git和Mercurial之间的差异。
然而,Git和Mercurial之间也有显著的差异,使每个系统更适合自己的一组用例。
Mercurial更容易学习。在使用Mercurial几周后,我很少需要参考文档或笔记;即使使用Git一年后,我仍然经常需要参考我的笔记。Git要复杂得多。
这部分原因是Mercurial更加简洁。你很少需要在Mercurial中手动分支;如果需要,Mercurial会为你自动创建一个匿名分支。Mercurial术语更直观;你不必像在Git中那样担心“fetch”和“pull”的区别。Mercurial稍微不那么容易出错。在多个平台之间推送项目时,Git和Mercurial都存在文件名大小写敏感性问题;这些问题在Mercurial中很早就解决了,而在我最后一次检查时,它们还没有在Git中得到解决。你可以告诉Mercurial有关文件重命名的信息;对于Git来说,如果它不能自动检测到重命名(在我的经验中非常难以做到),则无法跟踪重命名。
Git的额外复杂性的另一个原因是它需要支持其他功能和更强大的功能。是的,在Git中处理分支比较复杂,但是另一方面,一旦你拥有了这些分支,使用这些分支进行几乎不可能在Mercurial中实现的操作并不太困难。重新定位分支就是其中之一:你可以移动你的分支,使其基础不再是你分支时的主干状态,而是当前的主干状态;这极大地简化了版本历史记录,特别是当许多人在同一个代码库上工作时,因为每个推送到主干的提交都可以被呈现为连续的,而不是交织在一起的。同样,将你的分支中的多个提交合并为一个单独的提交要容易得多,这可以帮助保持版本控制历史记录的清洁:理想情况下,所有关于一个功能的工作都应该出现在主干的一个提交中,替换开发人员可能在开发功能时进行的所有次要提交和子分支。
最终,我认为在Mercurial和Git之间做出选择应该取决于你的版本控制项目有多大,以同时工作的人数为衡量标准。例如,如果你有一组12个或更多人在单个单块Web应用程序上工作,Git更强大的分支管理工具将使其更适合你的项目。另一方面,如果你的团队正在开发一个异构分布式系统,并且每个组件在任何时候只有一到两个开发人员在工作,使用Mercurial存储库来处理每个组件项目将允许开发更顺利,减少存储库管理开销。

底线:如果你有一个大团队正在开发单个巨大的应用程序,请使用Git;如果你的单个应用程序很小,并且规模是由这些应用程序的数量而不是大小决定的,请使用Mercurial。


8

请查看一段时间前Scott Chacon的文章

我认为git有一个“更复杂”的声誉,但在我的经验中,它并不比必须的更复杂。 在我看来,git模型要容易理解得多(标记包含提交(以及指向零个或多个父提交的指针)包含树包含块和其他树... 完成)。

不仅是我的经验表明git并不比mercurial更令人困惑。 我建议再次阅读Scott Chacon关于此事的博客文章


1
Mercurial模型实际上几乎相同:changelog指向清单点到文件修订/ blob...完成。如果您正在比较磁盘上的格式,则可能没有考虑到packs文件,这比来自hg的简单revlog格式更棘手。 - tonfa
那个简化的模型忽略了标记,而在实际使用中,在hg中标记相当笨重(尽管我认为git tag有点令人困惑,因为默认情况下它不会创建标记对象)。对于具有大量文件名历史记录的项目,磁盘上的格式特别昂贵。 - Dustin
1
我认为该模型并没有忽略标记:在Mercurial中,标记是微不足道的——正如您所知,它只是一个文件,为SHA-1哈希值赋予名称。关于标记在系统中如何流动,没有猜测:它们随着推送和拉取一起移动。如果存在标记冲突,那么解决它也很简单:像解决其他冲突一样解决即可。毕竟,它只是文本文件中的一行。我认为这种模型的简单性是一个非常好的特点。 - Martin Geisler
达斯汀:是的,用户经常会因为在检出版本1.0时无法看到.hgtags中的1.0标签而感到困惑。然而,您不需要查看.hgtags,您会发现hg tags仍然列出所有标签。此外,这种行为是将标签存储在版本控制文件中的简单结果 - 再次强调,该模型易于理解并且非常可预测 - Martin Geisler
1
Martin Geisler 我认为 Mercurial 中标签的规则并不容易理解,因为它使用版本控制文件进行传输,并在其上添加特殊规则以使标签成为非版本化。 - Jakub Narębski

4

有一个与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。

(不,我没有使用统计数据来支持我以上的观点...这完全是基于我的主观看法)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接