小编典典

Git 和 Mercurial - 比较和对比

all

一段时间以来,我一直在为我的个人项目使用颠覆。

我越来越多地听到关于 Git 和 Mercurial 以及一般 DVCS 的好消息。

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

Mercurial 和 Git 之间有哪些区别?

注意:我 不是 想找出哪个是“最好的”,甚至不是我应该从哪个开始。我主要寻找它们相似和不同的关键领域,因为我很想知道它们在实现和理念方面有何不同。


阅读 103

收藏
2022-03-11

共1个答案

小编典典

免责声明: 我使用 Git,在 git mailing list 上关注 Git 开发,甚至为 Git 贡献一点(主要是 gitweb)。
我从文档中了解 Mercurial,从 FreeNode 上#revctrl IRC 频道的讨论中了解一些。

感谢#mercurial IRC 频道上所有为此文章提供有关 Mercurial 帮助的人



概括

在这里有一些表格语法会很好,比如 PHPMarkdown / MultiMarkdown / Markdown 的 Maruku 扩展

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

Mercurial 与 Git 有一些不同之处,但还有其他一些东西使它们相似。这两个项目都互相借鉴了想法。例如hg bisectMercurial 中的
command(以前bisect extension)受git bisectGit 中 command 的启发,而 idea ofgit bundle​​ 受hg bundle.

存储库结构,存储修订

在Git中,其对象数据库中有四种类型的对象:包含文件内容的 blob对象,存储目录结构的分层
对象,包括文件名和文件权限的相关部分(文件的可执行权限,作为符号链接) ,包含作者信息的 提交
对象,指向由提交表示的版本库状态快照的指针(通过项目顶层目录的树对象)和对零个或多个父提交的引用,以及引用其他对象的 标记 对象,并且可以使用 PGP
/ GPG 进行签名。

Git 使用两种存储对象的方式: 松散 格式,每个对象存储在单独的文件中(这些文件只写入一次,从不修改),以及 打包
格式,其中许多对象以增量压缩方式存储在单个文件中。操作的原子性由以下事实提供,即在写入对象后写入对新对象的引用(原子地,使用 create + rename
技巧)。

Git 存储库需要定期维护git gc(以减少磁盘空间并提高性能),尽管现在 Git 会自动执行此操作。(此方法提供了更好的存储库压缩。)

Mercurial(据我了解)将文件的历史记录存储在文件 日志 中(我认为,连同额外的元数据,如重命名跟踪和一些帮助信息);
它使用称为manifest 的平面结构来存储目录结构,并使用称为 changelog
的结构来存储有关变更集(修订)的信息,包括提交消息和零、一个或两个父级。

Mercurial 使用 事务日志 来提供操作的原子性,并依靠 截断 文件在操作失败或中断后进行清理。Revlog 只能追加。

对比 Git 与 Mercurial 中的存储库结构,可以看出 Git 更像对象数据库(或内容寻址文件系统),而 Mercurial
更像传统的固定字段关系数据库。

区别:
在 Git 中, 对象形成 层次 结构;在 Mercurial 清单 文件中是 扁平 结构。在 Git blob
对象中存储文件内容的 一个版本 ;在 Mercurial文件 日志 中存储 单个文件的整个历史记录
(如果我们在这里不考虑重命名的任何复杂性)。这意味着在不同的操作领域,Git 会比 Mercurial
快,所有其他事情都被认为是相同的(例如合并,或显示项目历史),以及 Mercurial 会比 Git 快的领域(例如应用补丁或显示单个文件的历史记录)。
这个问题对最终用户来说可能并不重要。

由于 Mercurial 的 changelog 结构的固定记录结构,Mercurial 中的提交 最多只能有两个父 级;Git
中的提交可以有两个以上的父级(所谓的“章鱼合并”)。虽然您可以(理论上)通过一系列双父合并替换章鱼合并,但这可能会在 Mercurial 和 Git
存储库之间转换时导致复杂化。

据我所知,Mercurial 没有来自 Git 的等效 注释标签 (标签对象)。带注释标签的一种特殊情况是 签名标签 (带有 PGP/GPG
签名);Mercurial 中的等价物可以使用GpgExtension来完成,该扩展与 Mercurial 一起分发。您不能像在 Git 中那样在 Mercurial 中
标记未提交对象 ,但这并不是很重要,我认为(一些 git 存储库使用标记的 blob 分发公共 PGP 密钥以用于验证签名标签)。

参考:分支和标签

在 Git 中,引用(分支、远程跟踪分支和标签)位于提交的 DAG 之外(它们应该如此)。refs/heads/命名空间( 本地分支
)中的引用指向提交,通常由“git
commit”更新;他们指向分支的尖端(头部),这就是为什么这样的名字。命名空间中的引用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
最佳实践建议设置单独的公共裸存储库,将准备好的更改推送到其中,其他人从中克隆和获取。这意味着您不推送的标签(和分支)对于您的存储库是私有的。另一方面,您也可以使用除heads,remotes或以外的命名空间tags,例如local- tags用于本地标签。

个人观点: 在我看来,标签应该位于修订图之外,因为它们在修订图之外(它们是指向修订图的指针)。标签应该是非版本化的,但可以转移。Mercurial
选择使用类似于忽略文件的机制,这意味着它要么必须.hgtags特殊处理(树中的文件是可转移的,但通常它是版本化的),或者具有仅本地的标签(.hg/localtags是非版本化的,但不可转让)。

分支机构

在 Git中, 本地分支
(分支尖端或分支头)是对提交的命名引用,可以在其中增长新的提交。分支也可以表示活跃的开发线,即从分支尖端可到达的所有提交。本地分支驻留在refs/heads/命名空间中,因此例如“master”分支的完全限定名称是“refs/heads/master”。

Git 中的当前分支(意思是签出的分支,以及新提交的分支)是 HEAD ref 引用的分支。可以让 HEAD
直接指向提交,而不是符号引用;这种在匿名未命名分支上的情况称为 分离 HEAD (“git 分支”表明您在“(无分支)”上)。

在 Mercurial 中有匿名分支(分支头),并且可以使用书签(通过书签扩展)。此类 书签分支 纯粹是本地的,并且这些名称(直到 1.6 版)不能使用
Mercurial 进行转移。您可以使用 rsync 或 scp 将.hg/bookmarks文件复制到远程存储库。您还可以使用hg id -r <bookmark> <url>获取书签当前提示的修订 ID。

从 1.6 开始,书签可以被推/拉。BookmarksExtension页面有一个关于使用远程存储库的部分。Mercurial
中的书签名称是 global 有所不同,而 Git 中“远程”的定义还描述 了分支名称
从远程存储库中的名称到本地远程跟踪分支的名称的映射;例如refs/heads/*:refs/remotes/origin/*,映射意味着可以在“origin/master”远程跟踪分支(“refs/remotes/origin/master”)的远程存储库中找到“master”分支(“refs/heads/master”)的状态。

Mercurial 也有所谓的 命名分支 ,其中分支名称 嵌入
在提交中(在变更集中)。这样的名称是全局的(在获取时转移)。这些分支名称被永久记录为变更集元数据的一部分。使用现代
Mercurial,您可以关闭“命名分支”并停止记录分支名称。在这种机制中,分支的尖端是动态计算的。

在我看来,Mercurial 的“命名分支”应该被称为 _ 提交标签_
,因为它们就是这样。在某些情况下,“命名分支”可以有多个提示(多个无子提交),也可以由修订图的几个不相交部分组成。

Git 中没有与 Mercurial 的“嵌入式分支”等价的东西。此外,Git 的理念是,虽然可以说分支包含一些提交,但这并不意味着提交属于某个分支。

请注意,Mercurial 文档仍然建议至少对长期存在的分支(每个存储库工作流一个分支)使用单独的克隆(单独的存储库),也就是 通过 cloning
进行分支

推中的树枝

默认情况下,Mercurial 会推 所有头 。如果要推送单个分支( 单头
),则必须指定要推送的分支的最新版本。您可以通过其修订号(存储库本地)、修订标识符、书签名称(存储库本地,不会被转移)或嵌入式分支名称(命名分支)来指定分支提示。

据我了解,如果您推送包含在 Mercurial
用语中标记为某个“命名分支”的提交的一系列修订,您将在推送到的存储库中拥有这个“命名分支”。这意味着此类嵌入式分支(“命名分支”)的名称是 全局
的(相对于给定存储库/项目的克隆)。

默认情况下(取决于push.default配置变量)“git push”或“git push < remote >”Git 将推送
匹配的分支 ,即只有那些在您推送到的远程存储库中已经存在其等价物的本地分支。您可以使用--allgit-push 选项(“git push
–all”)来推送 所有分支 ,可以使用“git push < remote > < 分支 >”来推 送给定的单个分支
,并且可以使用“git push < remote > HEAD” 推送 当前分支

以上所有假设 Git 未配置通过remote.<remotename>.push 配置变量推送哪些分支。

抓取中的分支

注意: 这里我使用 Git 术语,其中“获取”是指从远程存储库下载更改 _而不 将这些更改与本地工作集成。这就是“ git fetch”和“
hg pull”的作用。_

如果我理解正确,默认情况下 Mercurial会从远程存储库中获取 所有头hg pull --rev <rev> <url>,但您可以指定要通过
” ” 或 ” hg pull <url>#<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/遥控器/来源/主人’)。

您可以使用以下命令在 Git中获取 单个分支git fetch <remote> <branch>- Git 会将请求的分支存储在
FETCH_HEAD 中,这类似于 Mercurial 未命名的头。

这些只是强大的 refspec Git 语法的默认示例:使用
refspecs,您可以指定和/或配置要获取的分支以及存储位置。例如,默认的“获取所有分支”案例由
‘+refs/heads/:refs/remotes/origin/‘ 通配符 refspec 表示,而“获取单个分支”是
‘refs/heads/:’ 的简写. Refspecs 用于将远程存储库中的分支 (refs) 名称映射到本地 refs
名称。但是你不需要(太多)了解 refspecs 就可以有效地使用 Git(主要感谢“git remote”命令)。

个人观点: 我个人认为 Mercurial 中的“命名分支”(分支名称嵌入到变更集元数据中)是带有全局命名空间的错误设计,尤其是对于 分布式
版本控制系统。例如,让我们假设 Alice 和 Bob 在他们的存储库中都有名为“for-joe”的“命名分支”,这些分支没有任何共同之处。然而,在 Joe
的存储库中,这两个分支将被视为一个分支。因此,您以某种方式提出了防止分支名称冲突的约定。这对 Git 来说不是问题,在 Joe 的存储库中,来自 Alice
的“for-joe”分支是“alice/for-joe”,而来自 Bob 的则是“bob/for-
joe”。

Mercurial 的“书签分支”目前缺乏核心分发机制。

差异:
这个领域是 Mercurial 和 Git 之间的主要差异之一,正如james
woodyatt
Steve
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
手册
页中描述):

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

差异
如您所见,比较上面的列表 Mercurial 提供版本号,是存储库本地的,而 Git 没有。另一方面,Mercurial 仅提供来自 ‘tip’
(当前分支)的相对偏移量,这些偏移量是存储库本地的(至少没有ParentrevspecExtension),而 Git 允许指定任何提示之后的任何提交。

最新版本在 Git 中命名为 HEAD,在 Mercurial 中命名为“tip”;Git 中没有空版本。Mercurial 和 Git
都可以有很多根(可以有多个无父提交;这通常是以前独立项目加入的结果)。

另请参阅: Elijah 博客 (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
中,此信息以“增强差异”形式保存在文件日志(文件 revlog)元数据中 这样做的结果是您必须使用hg rename/ hg mv
或者您需要记住运行hg addremove以执行基于相似性的重命名检测。

Git 在版本控制系统中是独一无二的,因为它使用 重命名检测
来处理文件重命名。这意味着文件被重命名的事实在需要时被检测到:在进行合并时,或在显示差异时(如果请求/配置)。这样做的好处是可以改进重命名检测算法,并且不会在提交时冻结。

--follow在显示单个文件的历史记录时,Git 和 Mercurial 都需要使用选项来跟随重命名。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:// 协议(by git-daemon),需要在服务器上安装 git。这些协议中的交换包括客户端和服务器协商它们共有的对象,然后生成和发送一个包文件。现代 Git 包括对“智能”HTTP 协议的支持。
  • “哑”协议 ,包括 HTTP 和 FTP(仅用于获取)和 HTTPS(用于通过 WebDAV 推送),不需要在服务器上安装 git,但它们确实需要存储库包含由git update-server-info(通常从挂钩运行)生成的额外信息)。交换包括客户端遍历提交链并根据需要下载松散的对象和包文件。缺点是它的下载量超过了严格要求(例如,在只有一个包文件的极端情况下,即使只获取几个修订版,它也会被整个下载),并且它可能需要许多连接才能完成。

扩展:可编写脚本与扩展(插件)

Mercurial 是用 Python 实现的,一些核心代码是用 C 编写的以提高性能。它提供了用于编写 扩展 (插件)的
API,作为添加额外功能的一种方式。一些功能,如“书签分支”或签名修订,在随 Mercurial 分发的扩展中提供,需要打开它。

Git 在 CPerlshell 脚本 中实现。Git 提供了许多适合在脚本中使用的低级命令( 管道)。
引入新功能的通常方法是将其编写为 Perl 或 shell 脚本,当用户界面稳定时,用 C 重写它以提高性能、可移植性,并在 shell
脚本的情况下避免极端情况(此过程称为 builtinification )。

Git 依赖并围绕 [repository] ​​格式和 [network] 协议构建。除了语言绑定之外,还有其他语言(部分或完全)对 Git 的
重新实现 (其中一些是部分重新实现,部分是 git 命令的包装):JGit(Java,由 EGit、Eclipse Git
插件使用)、Grit(Ruby) , 德威 (Python), git# (C#)。


TL;博士

2022-03-11