比较和对比
一段时间以来,我一直在为我的个人项目使用颠覆。
越来越多的我一直听到有关Git和Mercurial以及DVCS的优秀信息。
我想给整个DVCS一个旋风,但我对两种选择都不太熟悉。
Mercurial和Git有什么区别?
请注意,我并没有试图找出哪一个是“最好的”,或者甚至是我应该从哪一个开始。 我主要寻找他们相似的地方和他们不同的地方,因为我有兴趣了解他们在实施和哲学方面的差异。
免责声明:我使用Git,在git邮件列表中关注Git开发,甚至贡献一点Git(主要是gitweb)。 我从文档中知道Mercurial,并从FreeNode上关于#revctrl IRC频道的讨论中了解了一些。
感谢所有关于#mercurial IRC频道的人员,他们为这篇文章提供了有关Mercurial的帮助
概要
这里最好有一些表格的语法,比如Markdown的PHPMarkdown / MultiMarkdown / Maruku扩展
.hgtags
文件,对每个存储库标签使用特殊规则,并且还支持.hg/localtags
本地标签; 在Git标签中refs驻留在refs/tags/
namespace中,默认情况下会自动提取并需要显式推送。 Meritial与Git有一些不同之处,但还有其他一些东西使它们相似。 两个项目都借鉴了彼此的想法。 例如Mercurial中的hg bisect
命令(以前的分叉扩展)受Git中的git bisect
命令的启发,而git bundle
想法受hg bundle
启发。
存储库结构,存储修订
在Git的对象数据库中有四种类型的对象:包含文件内容的blob对象,存储目录结构的分层树对象,包括文件名和文件权限的相关部分(文件的可执行权限,作为符号链接) ,包含作者信息的提交对象,指向由提交(通过项目的顶级目录的树对象)表示的存储库的状态的快照的指针以及对零个或多个父提交的引用,以及引用其他对象的标记对象,并且可以使用PGP / GPG进行签名。
Git使用两种存储对象的方式:松散格式,其中每个对象存储在一个单独的文件中(这些文件只写一次,从不修改),以及打包格式,其中许多对象存储在单个文件中。 操作的原子性由以下事实提供:在写入对象之后写入对新对象的引用(使用创建+重命名技巧,原子性)。
Git存储库需要使用git gc
进行定期维护(以减少磁盘空间并提高性能),尽管现在Git可以自动执行此操作。 (这种方法可以更好地压缩存储库。)
Mercurial(据我了解)将一个文件的历史记录存储在一个文件日志中(我认为,与额外的元数据,如重命名跟踪和一些帮助信息一起); 它使用称为manifest的平坦结构来存储目录结构,以及称为changelog的结构,其存储关于变更集(修订版)的信息,包括提交消息和零,一个或两个父母。
Mercurial使用事务日志来提供操作的原子性,并依靠截断文件在失败或中断操作后进行清理。 Revlogs只能追加。
看一下Git和Mercurial中的存储库结构,可以看到Git更像是对象数据库(或内容寻址文件系统),而Mercurial更像传统的固定字段关系数据库。
区别:
在Git中,树形对象形成了一个层次结构; 在Mercurial清单文件是扁平结构。 在Git blob对象中存储文件内容的一个版本 ; 在Mercurial文件日志中存储单个文件的整个历史记录 (如果我们在这里没有考虑重命名的任何复杂情况)。 这意味着有不同的操作区域,Git比Mercurial快,所有其他事物都被认为是相同的(比如合并,或者显示项目历史),以及Mercurial比Git快的区域(比如应用补丁或者显示单个文件的历史)。 这个问题对最终用户可能不重要。
由于Mercurial的更新日志结构的固定记录结构,Mercurial中的提交最多只能有两个父母 ; 在Git中提交可以有两个以上的父母(所谓的“章鱼合并”)。 虽然您可以(理论上)通过一系列双亲合并来替换章鱼合并,但在Mercurial和Git存储库之间转换时可能会导致复杂化。
据我所知Mercurial没有相当于Git的注释标签 (标签对象)。 注释标签的特殊情况是带签名的标签 (带有PGP / GPG签名); 在Mercurial中可以使用GpgExtension完成,该扩展与Mercurial一起发布。 你不能在Git中标记 Mercurial中的非提交对象 ,但这并不是非常重要,我认为(某些git存储库使用标记的blob来分发公共PGP密钥来验证签名标记)。
参考文献:分支和标签
在Git引用(分支,远程跟踪分支和标签)驻留在提交的DAG之外(因为它们应该)。 refs/heads/
namespace( 本地分支 )中的引用指向提交,通常通过“git commit”更新; 他们指向分行的小费(头),这就是为什么这样的名字。 refs/remotes/<remotename>/
namespace( 远程跟踪分支 )中的引用指向提交,跟随远程仓库<remotename>
分支,并通过“git fetch”或等效项更新。 refs/tags/
namespace( 标签 )中的引用通常指向提交(轻量级标签)或标签对象(注释和签名标签),并且不打算更改。
标签
在Mercurial中,您可以使用标记给持久化名称进行修订; 标签的存储方式与忽略模式类似。 这意味着全局可见标签存储在版本库中的版本控制.hgtags
文件中。 这有两个后果:首先,Mercurial必须对此文件使用特殊规则来获取所有标记的当前列表并更新此文件(例如,它读取最近提交的文件修订版本,当前未检出版本); 其次,您必须对此文件进行更改,以便让其他用户/其他存储库(据我了解)显示新标签。
Mercurial还支持存储在hg/localtags
本地标签,这些标签对他人不可见(当然也不可转让)
在Git中,标签是固定的(常量)引用,存储在refs/tags/
namespace中的其他对象(通常是标签对象,这些对象又指向提交)。 默认情况下,当获取或推送一组修订版时,git会自动提取或推送指向正在提取或推送的修订版的标签。 不过,您可以在某种程度上控制哪些代码被抓取或推送。
Git将轻量级标签(直接指向提交)和带注释的标签(指向标签对象,其中包含标签消息,其中可能包含PGP签名,然后指向提交)略有不同,例如默认情况下它仅在描述时考虑注释标签使用“git描述”提交。
Git在Mercurial中没有严格等效的本地标签。 尽管如此,git的最佳做法建议设置单独的公共裸仓库,在其中推送已准备好的更改,并从中进行克隆和提取。 这意味着您不会推送的标签(和分支)对您的存储库是私有的。 另一方面,您也可以使用除heads
, remotes
或tags
以外的名称空间,例如local-tags
的本地标签。
个人观点:在我看来,标签应该位于修订图之外,因为它们是外部的(它们是指向修订图的指针)。 标签应该是非版本的,但可以转让。 Mercurial选择使用类似于忽略文件的机制,意味着它要么专门处理.hgtags
(文件树中的内容是可转移的,但普通的是版本化的),或者只有本地标签( .hg/localtags
不是版本控制的,但是.hg/localtags
)。
分行
在Git 本地分支 (分支提示或分支头)是一个提交的命名引用,其中可以增加新的提交。 分支也可以指活跃的开发线,即从分支尖端可达的所有提交。 本地分支位于refs/heads/
namespace中,因此例如'master'分支的完全限定名称是'refs / heads / master'。
Git中的当前分支(表示检出分支,以及新分配将分支的分支)是由HEAD引用引用的分支。 可以让HEAD直接指向提交,而不是符号引用; 在匿名匿名分支上的这种情况叫做detached HEAD(“git branch”表明你在'(no branch)'上)。
在Mercurial中有匿名分支(分支头),并且可以使用书签(通过书签扩展)。 这些书签分支纯粹是本地的,这些名称(不超过1.6版)不能通过Mercurial转让。 您可以使用rsync或scp将.hg/bookmarks
文件复制到远程存储库。 您还可以使用hg id -r <bookmark> <url>
来获取书签当前提示的修订ID。
由于1.6书签可以推/拉。 BookmarksExtension页面有关于使用远程存储库的一节。 Mercurial书签名称是全局性的,但Git中的'remote'定义也描述了分支名称从远程存储库中的名称映射到本地远程跟踪分支的名称; 例如refs/heads/*:refs/remotes/origin/*
映射意味着可以在'origin / master'远程跟踪的远程存储库中找到'master'分支的状态('refs / heads / master')分支('refs / remotes / origin / master')。
Mercurial也被称为命名分支,其中分支名称嵌入在提交中(在变更集中)。 这个名字是全球性的(在取回时转移)。 这些分支名称将永久记录为变更集元数据的一部分。 使用现代Mercurial,您可以关闭“命名分支”并停止录制分支名称。 在这个机制中,分支的提示可以实时计算。
Mercurial的“命名分支”在我看来应该被称为提交标签 ,因为它就是这样。 有些情况下,“命名分支”可以有多个提示(多个无子提交),也可以由修订图的几个不相交部分组成。
在Git中没有这些Mercurial“嵌入式分支”的等价物; 而且Git的理念是,虽然可以说分支包含一些提交,但并不意味着提交属于某个分支。
请注意,Mercurial文档仍建议至少为长期分支(每个存储库工作流程的单个分支)使用单独的克隆(单独的存储库),即通过克隆进行分支。
分支推动
Mercurial默认推动所有头 。 如果您想推送一个分支( 单头 ),则必须指定要推送的分支的最新修订版本。 您可以通过其修订版本号(本地存储库),版本标识符,书签名称(本地存储库,不会传输)或嵌入分支名称(命名分支)来指定分支技巧。
据我了解,如果你按照Mercurial的说法推送一系列包含标记为在某个“命名分支”上的提交的修订版本,那么你将在存储库中推送这个“命名分支”。 这意味着此类嵌入式分支(“命名分支”)的名称是全局的(关于给定存储库/项目的克隆)。
默认情况下(取决于push.default
配置变量)“git push”或“git push <remote>”Git会推送匹配的分支 ,也就是说,只有那些已经存在于远程仓库中的本地分支才会被推入。 你可以使用--all
选项来git-push(“git push --all”)来推送所有分支 ,你可以使用“git push <remote> <branch>”来推送给定的单个分支 ,并且你可以使用“ git push <remote> HEAD“推送当前分支 。
以上所有假设Git没有配置通过remote.<remotename>.push
推送哪些分支remote.<remotename>.push
配置变量。
获取分支
注意:这里我使用Git术语,其中“获取”意味着从远程存储库下载更改,而不将这些更改与本地工作集成。 这就是“ git fetch
”和“ hg pull
”的作用。
如果我理解正确,默认情况下Mercurial从远程存储库获取所有头 ,但是您可以指定要通过“ hg pull --rev <rev> <url>
”或“ hg pull <url>#<rev>
”获取的分支以得到单个分支 。 您可以使用修订标识符,“命名分支”名称(嵌入在changelog中的分支)或书签名称来指定<rev>。 但是,书签名称(至少当前)不会被传送。 您获得的所有“命名分支”修订都属于转移。 “hg pull”存储它作为匿名匿名头部提取的分支的提示。
在GIT中由默认值(由“GIT中克隆”,以及用于使用“git的远程添加”创建遥控器远程创建“原点”)“ git fetch
”(或“ git fetch <remote>
”)会从远程存储库的所有分支 (从refs/heads/
namespace),并将它们存储在refs/remotes/
namespace中。 这意味着例如远程“起源”中名为'master'(全名:'refs / heads / master')的分支将被存储(保存)为'origin / master'远程跟踪分支(全名:'refs /遥控器/来源/主“)。
你可以通过使用git fetch <remote> <branch>
来git fetch <remote> <branch>
Git中的单个分支 - Git会将请求的分支存储在FETCH_HEAD中,这与Mercurial未命名的头相似。
这些只是强大的refspec Git语法默认情况下的示例:使用refspecs,您可以指定和/或配置要获取的分支以及存储它们的位置。 例如,默认的“获取所有分支”情况由'+ refs / heads / *:refs / remotes / origin / *'通配符refspec表示,“fetch single branch”表示'refs / heads / <branch>'的简写形式:'' 。 Refspecs用于将远程存储库中分支(ref)的名称映射到本地refs名称。 但你不需要知道(很多)关于refspecs能够有效地使用Git(主要感谢“git remote”命令)。
个人观点:我个人认为Mercurial中的“命名分支”(含有变更元数据中的分支名称)是其全局命名空间的误导性设计,尤其是对于分布式版本控制系统。 例如,让我们看看Alice和Bob在它们的存储库中具有名为“for-joe”的“命名分支”的情况,这些分支没有任何共同之处。 然而,在Joe的存储库中,这两个分支将作为单个分支被虐待。 所以你不知怎么想出了防止分支名称冲突的惯例。 这对Git来说并不是问题,在Joe的仓库'for-joe'分支来自Alice的时候会是'alice / for-joe',而从Bob来说,它会是'bob / for-joe'。 另请参见将分支名称与Mercurial wiki上引发的分支标识问题分离。
Mercurial的“书签分支”目前缺乏内核分发机制。
区别:
这个领域是Mercurial和Git之间的主要区别之一,正如james woodyatt和Steve Losh在答案中所说的那样。 默认情况下,Mercurial使用匿名轻量级代码行,其术语称为“头”。 Git使用轻量级命名分支,使用内射映射将远程存储库中分支的名称映射到远程跟踪分支的名称。 Git“强迫”你命名分支(除了单一的未命名分支,称为detached HEAD),但我认为这适用于分支繁重的工作流程,比如主题分支工作流,这意味着单个存储库范例中的多个分支。
命名修订
在Git中有很多命名修订的方式(例如在git rev-parse manpage中描述):
^
到修订参数意味着提交对象的第一个父代, ^n
合并提交的第n个父代。 后缀~n
修订参数指在直的第一父行的提交的第n祖先。 可以将这些后缀组合起来,从符号引用的路径后面形成修订说明符,例如'pu〜3 ^ 2〜3' 还有涉及reflog的修订说明符,这里没有提到。 在Git中的每个对象,无论是承诺,标签,树或斑点有其SHA-1标识符; 有特殊的语法,例如'next:Documentation'或'next:README'来引用指定版本中的树(目录)或blob(文件内容)。
Mercurial也有很多命名变更集的方法(例如在hg手册页中描述):
差异
正如你可以看到比较上面的列表Mercurial提供版本号,本地存储库,而Git没有。 另一方面,Mercurial仅从'tip'(当前分支)提供相对偏移量,这是本地存储库(至少没有ParentrevspecExtension),而Git允许从任何提示中指定任何后续提交。
最新版本在Git中被命名为HEAD,在Mercurial中被命名为“tip” Git中没有null修订。 Mercurial和Git都可以拥有许多根目录(可以有多个无父级提交;这通常是以前独立项目加入的结果)。
另见:以利亚博客上的许多不同种类的修订说明文章(newren's)。
个人观点:我认为版本号被高估了(至少对于分布式开发和/或非线性/分支历史)。 首先,对于分布式版本控制系统,它们必须是存储库本地的,或者需要以特殊的方式将一些存储库作为中央编号机构处理。 其次,具有较长历史的较大项目可以有5位数范围内的修订数量,因此它们仅提供略微优于6-7个字符的修订标识符,并且意味着严格排序,而修订仅部分排序(我的意思是这里修订版n和n + 1不需要是父母和子女)。
修订范围
在Git中,修订范围是拓扑 。 常见的A..B
语法是线性历史意味着从A开始(但不包括A),结束于B(即范围从下开放)的修订范围,是对于^AB
简写(“语法糖”),其对于历史遍历命令来说,意味着所有可从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
; 它意味着AB --not $(git merge-base AB)
不是AB --not $(git merge-base AB)
,这意味着所有可从A或B访问的提交,但不包括从它们两个可访问的提交(可从公共祖先访问)。
重命名
Mercurial使用重命名跟踪来处理文件重命名。 这意味着在提交时保存关于文件被重命名的信息; 在Mercurial中,这些信息以文件日志(文件revlog)元数据中的“增强型差异”形式保存。 这样做的后果是你必须使用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支持两种用于访问远程存储库的协议:
git-daemon
),需要在服务器上安装git。 这些协议中的交换包括客户端和服务器协商他们共有的对象,然后生成并发送一个包文件。 现代Git包含对“智能”HTTP协议的支持。 git update-server-info
生成的额外信息(通常从一个钩子运行)。 交换包括客户端在提交链中走动,并根据需要下载松散的对象和包文件。 不足之处在于它下载的内容超过了严格要求(例如,在只有单个包文件的情况下,即使仅获取少量修订版,也会下载整个文件),并且可能需要很多连接才能完成。 扩展:脚本功能与扩展(插件)
Mercurial是用Python实现的,为了提高性能,一些核心代码用C编写。 它提供了用于编写扩展 (插件)的API作为添加额外功能的一种方式。 某些功能(如“书签分支”或签名修订版)在Mercurial分发的扩展中提供,并需要将其打开。
Git是用C , Perl和shell脚本实现的 。 Git提供了许多适用于脚本的低级命令(管道)。 引入新特性的通常方式是将其编写为Perl或shell脚本,并且当用户界面稳定时,为了性能,可移植性以及shell脚本避开角落案例(此过程称为内建),将其重写为C。
Git依赖并围绕[资源库]格式和[网络]协议构建。 JGit(Java,由EGit,Eclipse Git Plugin使用),Grit(Ruby)以及其他语言(部分或全部)重新实现Git(其中一些部分重新实现, ,Dulwich(Python),git#(C#)。
TL; DR
我认为,通过对这两个视频进行分析,你可以感受到这些系统的相似或不同:
Linus Torvalds on Git(http://www.youtube.com/watch?v=4XpnKHJAok8)
Bryan O'Sullivan在Mercurial上(http://www.youtube.com/watch?v=JExtkqzEoHY)
它们两者在设计上非常相似,但在实现方式上非常不同。
我使用Mercurial。 就我理解Git而言,git的一个主要不同之处在于它跟踪文件的内容而不是文件本身。 Linus说,如果你将一个函数从一个文件移动到另一个文件,Git会告诉你整个移动过程中单个函数的历史记录。
他们还说git比HTTP慢,但它拥有自己的网络协议和服务器。
作为SVN厚客户端,Git比Mercurial更好。 你可以拉和推SVN服务器。 该功能在Mercurial中仍在开发中
Mercurial和Git都有非常好的网络托管解决方案(BitBucket和GitHub),但Google Code只支持Mercurial。 顺便说一下,他们对Mercurial和Git进行了非常详细的比较,他们在决定支持哪一种(http://code.google.com/p/support/wiki/DVCSAnalysis)时做了相应的比较。 它有很多很好的信息。
我前一段时间写了一篇关于Mercurial分支模型的博客文章,并将其与git的分支模型进行了比较。 也许你会发现它很有趣:http://stevelosh.com/blog/entry/2009/8/30/a-guide-to-branching-in-mercurial/
链接地址: http://www.djcxy.com/p/37533.html上一篇: Compare and Contrast