水星,在幕后

2020-12-21 15:55:46

与许多版本控制系统不同,Mercurial构建于其上的概念非常简单,因此很容易理解该软件的实际工作原理。当然不必知道这些细节,因此跳过本章当然是安全的。但是,我认为您将通过发生的事的“心理模型”从软件中获得更多收益。

能够了解幕后发生的事情使我充满信心,Mercurial的设计经过精心设计,既安全又高效。同样重要的是,如果当我执行修订控制任务时很容易就可以很好地了解软件的功能,那么它的行为就不会让我感到惊讶。

在本章中,我们将首先介绍Mercurial设计背后的核心概念,然后继续讨论其实现的一些有趣细节。

当Mercurial跟踪对文件的修改时,它将该文件的历史记录存储在称为Filelog的元数据对象中。文件日志中的每个条目都包含足够的信息,以重建所跟踪文件的一个修订版本。 Filelogs作为文件存储在目录中。文件日志包含两种信息:修订数据和帮助Mercurial有效查找修订的索引。

一个很大的文件或具有很多历史记录的文件,其日志记录存储在单独的数据(“ .d”后缀)和索引(“ .i”后缀)文件中。对于没有太多历史记录的小文件,修订数据和索引合并在单个“ .i”文件中。图4.1``工作目录中的文件与存储库中的文件日志之间的关系''说明了工作目录中的文件与跟踪存储库中的文件历史的文件日志之间的对应关系。

Mercurial使用一种称为清单的结构来收集有关其跟踪文件的信息。清单中的每个条目都包含有关单个变更集中存在的文件的信息。一个条目记录更改集中存在哪些文件,每个文件的修订版以及其他一些文件元数据。

变更日志包含有关每个变更集的信息。每个修订记录谁提交了变更,变更集注释,与变更集相关的其他信息以及要使用的清单的修订。

在修订日志,清单或文件日志中,每个修订版都存储一个指向其直接父级的指针(如果是合并修订版,则指向其两个父级)。如上所述,这些结构的修订版之间也存在关系,并且它们本质上是分层的。

对于存储库中的每个变更集,变更日志中仅存储一个修订。变更日志的每个修订版都包含一个指向清单的单个修订版的指针。清单的修订版存储指向创建变更集时跟踪的每个文件日志的单个修订版的指针。这些关系在图4.2“元数据关系”中进行了说明。

如图所示,变更日志,清单或文件日志中的修订之间没有“一对一”的关系。如果Mercurial跟踪的文件在两个变更集之间没有更改,则清单的两个修订版中该文件的条目将指向其文件日志的相同修订版[3]。

变更日志,清单和文件日志的基础由称为revlog的单个结构提供。

修订日志使用增量机制提供了有效的修订存储。它没有存储每个修订版本的文件的完整副本,而是存储了将旧修订版本转换为新修订版本所需的更改。对于许多类型的文件数据,这些增量通常仅占文件完整副本大小的百分之几。

一些过时的修订控制系统只能使用增量文本文件。他们必须将二进制文件存储为完整的快照或编码为文本表示形式,这两种方法都是浪费的方法。 Mercurial可以有效地处理具有任意二进制内容的文件增量;它不需要将文本视为特殊字符。

Mercurial仅将数据附加到修订日志文件的末尾。写入文件后,它绝不会修改文件的一部分。与需要修改或重写数据的方案相比,这既健壮又高效。

此外,Mercurial将每次写入都视为可跨越多个文件的事务的一部分。事务是原子性的:要么整个事务成功,而且其效果一目了然,但读者却看不见,要么整个事务都被撤消。这种原子性保证意味着,如果您运行的是Mercurial的两个副本,其中一个正在读取数据,一个正在写入数据,那么读者将永远不会看到可能会混淆它的部分写入结果。

Mercurial仅附加到文件这一事实使得提供此事务保证更加容易。做这样的事情越容易,就应该对完成正确的事情更有信心。

Mercurial巧妙地避免了所有早期版本控制系统都存在的陷阱:检索效率低下的问题。大多数修订控制系统将修订的内容存储为针对“快照”的一系列增量修订。 (某些快照基于最早的修订版本,其他快照基于最新的修订版本。)要重建特定的修订版本,必须首先读取快照,然后读取快照与目标修订版本之间的每个修订版本。文件累积的历史记录越多,您必须阅读的修订版越多,因此重建特定修订版所花费的时间越长。

Mercurial应用于此问题的创新很简单但有效。自上次快照以来存储的增量信息的累积量超过固定阈值后,它将存储新的快照(当然是压缩的),而不是另一个增量。这样就可以快速重建文件的任何修订版。这种方法效果很好,以至于它已经被其他几个版本控制系统所复制。

图4.3``带有增量增量的修订日志快照''说明了这一想法。在revlog索引文件的条目中,Mercurial存储了数据文件中它必须读取以重建特定修订版本的条目范围。

如果您熟悉视频压缩,或者曾经通过数字电缆或卫星服务收看过电视节目,则可能会知道大多数视频压缩方案会将视频的每一帧存储为相对于其前一帧的增量。

Mercurial借用了这个想法,从而可以从快照和少量增量重建修订版本。

除了增量或快照信息外,修订日志条目还包含其表示的数据的加密哈希。这使得难以伪造修订的内容,并且易于检测意外损坏。

哈希不仅可以防止腐败;它们用作修订的标识符。您作为最终用户看到的变更集标识哈希来自变更日志的修订版。尽管文件日志和清单也使用哈希,但Mercurial仅在后台使用这些。

当Mercurial检索文件修订以及从另一个存储库中提取更改时,它们会验证哈希是否正确。如果遇到完整性问题,它将抱怨并停止所做的任何事情。

除了对检索效率有影响外,Mercurial对定期快照的使用使它对于部分数据损坏也更加健壮。如果某个修订日志由于硬件错误或系统错误而部分损坏,则通常可以在损坏的部分之前和之后,从该修订日志的未损坏部分中重建一些或大多数修订。对于仅增量存储模型,这是不可能的。

Mercurial修订日志中的每个条目都知道其直接祖先修订版本的身份,通常称为其父版本。实际上,修订版包含的空间不是一个父母,而是两个父母。 Mercurial使用一种特殊的哈希(称为“空ID”)来表示“此处没有父母”的想法。此哈希只是一串零。

在图4.4``修订日志的概念结构''中,您可以看到修订日志的概念结构的示例。文件日志,清单和更改日志都具有相同的结构;它们仅在每个增量或快照中存储的数据类型不同。

修订日志中的第一个修订版(在图像的底部)在其两个父插槽中均具有空ID。对于“普通”修订版,其第一个父级插槽包含其父修订版的ID,第二个父级插槽包含空ID,表示该修订版只有一个真实的父级。具有相同父ID的任何两个修订版都是分支。代表分支之间合并的修订版本在其父级插槽中具有两个常规修订版本ID。

在工作目录中,Mercurial存储来自特定更改集的存储库中文件的快照。

工作目录“知道”它包含哪个变更集。当您更新工作目录以包含特定的变更集时,Mercurial会查找清单的适当修订版,以找出提交变更集时正在跟踪的文件,以及每个文件的最新版本。然后,它将重新创建每个文件的副本,其内容与提交变更集时的内容相同。

dirstate是一个特殊的结构,其中包含Mercurial对工作目录的了解。它被维护为在存储库中命名的文件。工作目录更新到哪个目录详细信息,以及Mercurial在工作目录中跟踪的所有文件。通过记录签出时间和大小,Mercurial还可以使Mercurial快速注意到已更改的文件。

正如修订日志的修订版可以容纳两个父级,以便它可以代表一个普通修订版(具有一个父级)或两个较早修订版的合并一样,dirstate可以容纳两个父级。使用hg update命令时,更新到的变更集存储在“第一个父级”插槽中,而空ID存储在第二个中。 hg与另一个变更集合并时,第一个父级保持不变,而第二个父级将填充您要与之合并的变更集。 hg父母命令告诉你dirstate的父母是什么。

dirstate存储父信息不仅用于簿记目的。执行提交时,Mercurial将dirstate的父级用作新变更集的父级。

图4.5``工作目录可以有两个父目录''显示了工作目录的正常状态,其中有一个变更集作为父目录。该变更集是技巧,这是存储库中没有子级的最新变更集。

将工作目录视为“即将提交的变更集I”很有用。您告诉Mercurial您已添加,删除,重命名或复制的任何文件,将反映在该变更集中,以及对Mercurial已经在跟踪的任何文件的修改;新变更集将工作目录的父级作为其父级。

提交后,Mercurial将更新工作目录的父级,以便第一个父级是新变更集的ID,第二个是空ID。如图4.6``工作目录在提交后获得新的父母''所示。提交时,Mercurial不会触摸工作目录中的任何文件;它只是修改目录以记录其新父母。

将工作目录更新为当前提示以外的变更集是完全正常的。例如,您可能想知道上周二您的项目的样子,或者您可能正在浏览变更集以查看哪个项目引入了错误。在这种情况下,自然的做法是将工作目录更新为您感兴趣的变更集,然后直接检查工作目录中的文件以查看其内容,即提交该变更集时的内容。图4.7``工作目录已更新到较旧的变更集''中显示了此操作的效果。

将工作目录更新为较旧的变更集后,如果进行一些更改然后提交,会发生什么?水星的行为与我上面概述的相同。工作目录的父级将成为新变更集的父级。这个新的变更集没有子代,因此成为新的技巧。现在,存储库包含两个没有子代的变更集。我们称这些为首长。您可以在图4.8``同步到较早的变更集的提交之后''中看到此创建的结构。

如果您是Mercurial的新手,则应记住一个常见的“错误”,即使用hg pull命令时不带任何选项。默认情况下,hg pull命令不会更新工作目录,因此您会将新的变更集引入到存储库中,但是工作目录将与拉取之前的相同变更集保持同步。如果您进行了一些更改并随后提交,则将创建一个新的头,因为您的工作目录不会同步到当前提示。要结合拉动操作和更新操作,请运行hg pull -u。

我将“错误”一词用引号引起来,因为要纠正偶然创建新头的情况,您需要做的所有事情就是hg merge,然后是hg commit。换句话说,这几乎不会带来负面影响;对于新来者来说,这只是一个惊喜。稍后,我将讨论避免这种行为的其他方法,以及为何Mercurial会以这种最初令人惊讶的方式表现。

运行hg merge命令时,Mercurial会保持工作目录的第一个父级不变,并将第二个父级设置为您要与之合并的变更集,如图4.9“合并两个头”所示。

Mercurial还必须修改工作目录,以合并两个变更集中管理的文件。对于两个变更集清单中的每个文件,合并过程都进行了一些简化。

如果一个变更集修改了文件,而另一个变更集修改了文件,则在工作目录中创建文件的修改后的副本。

如果一个变更集删除了一个文件,而另一个变更集删除了(或也删除了它),则从工作目录中删除该文件。

如果一个变更集已删除文件,而另一个变更集已删除文件,请询问用户该怎么做:保留已修改的文件还是将其删除?

如果两个变更集都修改了文件,则调用外部合并程序以选择合并文件的新内容。这可能需要用户输入。

如果一个变更集已修改文件,而另一个变更集已重命名或复制了文件,请确保更改遵循文件的新名称。

有更多细节(合并有很多极端情况),但这是合并中最常见的选择。如您所见,大多数情况是完全自动的,并且实际上大多数合并都是自动完成的,而无需您输入解决任何冲突的方法。

当您考虑合并后提交时会发生什么情况时,工作目录再次是“即将提交的变更集I”。 hg merge命令完成后,工作目录具有两个父目录;这些将成为新变更集的父项。

Mercurial允许您执行多个合并,但是您必须在进行过程中提交每个合并的结果。这是必需的,因为Mercurial仅跟踪两个父版本和工作目录。尽管在技术上一次合并多个变更集是可行的,但Mercurial为简单起见避免了这一点。使用多路合并,用户混乱,麻烦的冲突解决以及合并的可怕混乱的风险将变得无法忍受。

随着时间的推移,数量惊人的修订控制系统很少或根本不关注文件名。例如,通常,如果在合并的一侧对文件进行了重命名,则来自另一侧的更改将被静默删除。

当您告诉Mercurial执行重命名或复制操作时,它会记录元数据。在合并过程中,它会在合并过程中使用此元数据来执行正确的操作。例如,如果我重命名文件,而您在不重命名的情况下对其进行编辑,则在我们合并工作时,该文件将被重命名并应用您的编辑。

在以上各节中,我试图突出显示Mercurial设计的一些最重要方面,以说明它对可靠性和性能非常关注。但是,对细节的关注不止于此。我个人认为Mercurial的构造还有许多其他方面。我将在此处详细说明其中的一些,与上面的“大门票”项目分开,以便您感兴趣时,可以更好地了解设计周密的设计思路系统。

在适当的时候,Mercurial将以压缩形式存储快照和增量。为此,它总是尝试压缩快照或增量,但仅在压缩版本小于未压缩版本时才存储压缩版本。

这意味着,Mercurial在存储压缩了本机格式的文件(例如zip存档或JPEG图像)时会做“正确的事情”。再次压缩这些类型的文件时,生成的文件通常比一次压缩后的文件大,因此Mercurial将存储纯zip或JPEG。

压缩文件的修订版之间的增量通常大于文件快照的增量,在这种情况下,Mercurial再次“正确”。它发现此类增量超出了应存储文件完整快照的阈值,因此它存储了快照,与仅采用纯增量方式相比,又节省了空间。

在磁盘上存储修订时,Mercurial使用“ deflate”压缩算法(流行的zip存档格式使用相同的压缩算法),该算法在良好的速度与可观的压缩比之间取得了平衡。但是,通过网络连接发送修订数据时,Mercurial会解压缩压缩的修订数据。

如果连接是通过HTTP进行的,那么Mercurial会使用提供更好压缩率的压缩算法(来自广泛使用的bzip2压缩包的Burrows-Wheeler算法)来重新压缩整个数据流。算法和整个流的压缩(而不是一次修订)的这种结合大大减少了要传输的字节数,从而在大多数类型的网络上产生了更好的网络性能。

如果连接是通过ssh进行的,则Mercurial不会重新压缩流,因为ssh本身已经可以执行此操作。您可以按照以下说明编辑主目录中的文件,以指示Mercurial始终使用ssh的压缩功能。

关于保证读者不会看到部分写入的内容,附加到文件并不是全部。如果您回想起图4.2``元数据关系'',则变更日志中的修订指向清单中的修订,清单中的修订指向文件日志中的修订。此层次结构是有意的。

编写者通过写入文件日志和清单数据来启动事务,并且在完成这些变更日志数据之前不写入任何变更日志数据。读取器首先读取变更日志数据,然后读取清单数据,然后读取文件日志数据。

由于编写者在写入变更日志之前始终完成文件日志和清单数据的写入,因此读者将永远不会从变更日志中读取指向部分写入的清单修订的指针,也永远不会从变更日志中读取指向部分写入的文件日志修订的指针。表现。

读/写顺序和原子性保证意味着Mercurial永远不需要

......