git 本身 — 命令行应用程序,当然是我们使用 Git 存储库最明显的方式,但这并不是唯一的方法。在 Visual Studio Team Services 中,在其中我们托管所有的 Git 存储库(包括 Windows)的地方,使用 libgit2 项目来处理 Git 存储库。
libgit2 是一个开源项目,现在主要由 GitHub 和 Microsoft 的员工维护,它的架构支持自定义数据库驱动程序进行存储库访问。这使得 Visual Studio Team Services 能够在 Azure blob 存储中非常有效地存储代码仓库,而不是仅仅在文件系统中备份(dump)空存储库。
当 Microsoft 将合并功能添加到 libgit2 时,我们可以高效地将 Azure 托管的存储库中的 pull 请求合并到 VSTS 中 — 我们希望处理来自大型存储库的 pull 请求。但是,尽管我们做了最佳规划,但仍然存在着在 Windows 存储库规模大小的项目上面临性能问题的地方。
当 Git 保存修订版本时,它并不会保存在两个修订版本之间改动的文件列表,或它们是如何改动的。相反,它会在每个版本中存储整个版本树的快照。这意味着当 git 显示你已重命名的一个文件时,它实际上是通过遍历两个不同版本中的所有文件,将每个被删除的文件与每个已添加的文件进行比较。如果已删除的文件与新添加的文件非常类似,则 git 判定你实际从旧的文件重新命名为新的文件。
这种重新命名检测在 merge 期间尤为重要 - 如果一个开发人员将文件从 foo.txt 重命名为 bar.txt,另一个开发人员对 foo.txt 进行了改动,那么你需要确保将这些更改包含在新的文件中。通过重命名检测,如你所愿,改动将包含在 bar.txt 中。没有重新命名检测,你将在 foo.txt 上(它在一个分支中被编辑而在另一个分支中被删除了)触发冲突,你将得到一个名为 bar.txt 的新文件。这根本不是你想要的。
不幸的是,重命名检测本质上是二次方复杂度的:你将每个已删除的文件与每个添加的文件进行比较,以确定哪个具有最佳的相似性匹配。为了避免类似情况开销会非常大,git 有一个名为 merge.renameLimit 的设置项,可以避免对于太大的 n 执行这种高昂的O(n^2)比较操作。
像 git 一样,libgit2 服从 merge.renameLimit 来进行昂贵的相似检测。但像 git 一样,libgit2 不用在使用 merge.renameLimit 进行精确重命名检测担忧。勿须比较两个文件的内容以确定它们是否相似,精确的重命名检测只是查看文件 ID,这个 ID 就是其内容的 SHA-1 哈希值。 相同的哈希值意味着相同的内容,因此你可以轻松地通过比较ID来确定文件是否被重命名。
不幸的是,libgit2 使用相同的 O(n^2) 算法,将每个已删除的文件的 ID 与每个添加的文件的 ID 进行精确重命名检测。 当 Windows 推送了一个非常大的重构,在那里它们重命名了一个目录的所有文件,精确的重命名检测将不可控,它对涉及的数千个文件 ID 进行二次时间比较,会导致该请求超时。
要处理这个看似简单的重构更改,我们要回头看看 libgit2 的重命名检测功能。我们遍历删除文件的列表,并构建一个哈希表,将他们的 ID 映射到旧文件名,而不是将每个已删除的文件与每个添加的文件进行比较。 然后我们遍历列表中添加的文件,寻找该散列中的 ID:如果发现,那么我们知道我们有一个重命名文件。
这种直接的改动将 O(n^2) 操作简化成线性时间的简单检测,这样 Windows 再次以这些大规模的重构来创建 pull 请求。