换向和可伸缩性

2020-12-21 05:36:33

我刚刚完成了Pijul一个重要功能的实现:克隆,推送和拉取部分存储库。在这篇文章中,我解释了为什么这很重要。

Pijul基于更改,也称为补丁或差异,这并不意味着其唯一的内部数据结构就是补丁,恰恰相反:只有通过脱离仅补丁的内部表示形式,我们才能解决算法问题基于补丁的系统固有的挑战。

但是,基于更改确实意味着Pijul的核心操作是根据更改定义的,并且Pijul的设计方式是使更改满足基本的直观属性,类似于代数运算。一个基本的事情是,应用更改是一个关联操作,例如矩阵乘法:一次应用$ A $和$ B $,然后再应用$ C $,与应用$ A $,然后$ B $和一次$ C $。在矩阵乘法中,$(AB)C = A(BC)$。此外,在Pijul中,所有更改都是可逆的(而只有一些矩阵是可逆的):对于任何更改$ A $,都有一个“逆向更改” $ A ^ {-1} $,因此应用$ A ^ {-1} $ $ A $之后的内容与两者都不应用相同。当然,$ A $和$ A ^ {-1} $都将出现在日志中,但是存储库的内容将与不应用$ A $或$ A ^ {-1} $相同。

用户需要版本控制系统的另一个属性,即换向。矩阵乘法很少换向1.在Git中,换向通常使用分支和重新基准进行模拟:实际上,将分支A重新置于另一个分支B之上确实意味着换向自A和B之间的分歧以来A的提交,以及自分歧以来B的提交。但是,这种转换并不是完美的,因为提交在重新设置基准时必须更改其哈希值。

在Pijul中事情变得更简单,因为本来可以独立编写的任何两个更改总是会上下班,这意味着如果可以在彼此不知情的情况下编写这两个更改,则可以以任何顺序应用它们。

如果我可以以任何顺序应用$ A $和$ B $,我怎么知道哪个顺序是正确的?

在Pijul中,此问题无关紧要:两个订单将产生完全相同的结果,唯一的不同是日志将按应用顺序对更改进行列出。我并不是说这并不重要,因为我很粗心,而是因为它确实是同一回事。

我不同意:这很重要,我仍然更喜欢对更改/提交使用“线性”顺序。

实际上,出于多种原因,每个人都希望查看存储库中的操作顺序。例如:

实际上,Pijul可以让您做到这一点,但是比Git更为严格。确实,在爱丽丝和鲍勃一起工作的情况下,爱丽丝做出了$ A $的更改,而鲍勃做出了$ B $。当他们在一起工作时,Alice会应用Bob的更改,从而产生日志$ AB $,而Bob会应用Alice的更改,从而产生日志$ BA $。在这种情况下,没有“真实的”线性历史记录,因为它们处理不同的事情,并且在不同的时间采取了不同的步骤。但是,他们俩都希望能够逐步,及时地回溯过去,而不仅仅是“按照鲍勃的顺序一步步走”。

直到最新版本为止,Pijul面临的最大挑战之一就是可伸缩性:即使像Pijul的源代码这样的中等大小的存储库也会占用大量磁盘空间。令人失望的是,因为正如我将要解释的那样,换向被设计成可以扩展到巨大的存储库大小……从理论上讲。同样的问题也使得无法进行大规模测试,这意味着随着时间的流逝,越过“ 0.x版本”似乎越来越不可能。

现在,这个阶段已基本落在我们身后,该理论的精妙之处终于变为现实。

特别是在Pijul中,每次更改都包含对其修改文件的引用。请注意,由于我们希望对存储库的操作(例如重命名文件)与文件内的编辑操作相对应,因此我们不按名称标识文件和目录,而是根据引入该文件的更改的哈希值或目录。

对于具有多个项目的存储库,这使得可以克隆和提取存储库的一部分,然后像处理整个库一样处理该部分。确实,假设我们有一个包含以下日志的存储库:

所有这些变化并不一定会减退;但是,由于B和C接触完全不同的文件(即y和z),因此它们可以并行生成,因此可以上下班。这意味着我们只能提取与特定文件(例如x)相关的更改,并进行以下历史记录:A,B,E 2。

此外,在该序列顶部进行的任何更改都将与C,D和F转换:的确,如果我们再次编辑文件x并产生更改G,那么由于G可以与C,D和F并行进行,因此我们可以在该历史记录中紧随B之后的任何补丁之后,按G,例如获取历史记录“ ABCDEFG”。

如本博客先前的文章所述,Pijul更改比diff包含更多信息,并且对图形而不是文件起作用。这意味着更改可以分为两部分,一个简短的部分,带有图形操作的二进制说明,然后是该更改插入的新内容。

更改格式设计为可分两个阶段下载:一个可以下载操作而无需下载内容。与此相关的一个问题是安全性:如果我们不下载内容,我们如何确保哈希正确?这是通过在更改的“操作”部分中包含更改的哈希,然后将更改的哈希值作为“操作”部分的哈希来完成的。

这样一来,某人一天就可以制作一个大型二进制文件的五个版本,每次更改都会删除整个文件,然后再次添加,操作部分仅包含不同版本的长度,而不包含实际字节。因此,为了获取文件的最新版本,客户端只需完全下载最新的更改,而只需下载先前更改的操作部分即可。

请注意,这使得以下“攻击”成为可能:服务器可能诱使客户端认为该服务器使用哈希$ A $进行了更改,该哈希将$ n $字节插入文件中,然后使用哈希$ B $进行了另一次更改,删除所有这些字节。客户下载$ A $和$ B $时,不需要下载内容。但是,如果客户端以后决定取消记录$ B $,则必须下载$ A $的内容,并且客户端将能够知道$ A $的哈希值不正确。这将使取消记录$ B $而不取消记录$ A $成为不可能。但是,如果$ A $进行了其他编辑,而其他更改取决于$ A $,则可能会出现问题。

对于非常大的存储库,还有一个痛苦点,那就是,为了克隆具有非常大的历史记录的存储库,必须一一下载并应用所有更改。即使我们的应用功能相当快3,这对于成千上万的更改仍然是有问题的。

在我的下一篇文章中,我将讨论一个已开始实施的解决此问题的方法。 例如,同时可对角化的矩阵可以,但是对于两个任意矩阵$ A $和$ B $,$ AB $通常与$ BA $不同。 ↩︎ 请注意,在这种情况下,我们也可以拉A,F,B,因为重命名文件时会对其进行编辑。 但是,我们必须以A开头,因为添加文件与编辑或重命名不可能并行进行。 ↩︎ apply的复杂度以$ O(| p | | c | \ log | H |)$为单位,其中$ | p | $是更改的大小,$ | c | $是最大冲突的大小,其中 涉及$ p $,$ | H | $是自存储库开始以来进行的编辑次数。 ↩︎