为什么git在计算要推送的最小增量时忽略了HEAD?有什么方法可以解决这个问题(不需要创建一个命名的ref)吗?

toe95027  于 2022-11-27  发布在  Git
关注(0)|答案(1)|浏览(110)

tl;dr:以下是问题的再现:

#!/usr/bin/env bash
set -euo pipefail

git -c advice.detachedHead=false clone --depth=1 -b v2.38.0 https://github.com/git/git.git dummy_git1
cp -arT dummy_git1 dummy_git2

git -C dummy_git2 tag -d v2.38.0
git -C dummy_git1 branch dummy_branch1

echo "The command below should finish immediately, but it actually slowly copies all the blobs:" 1>&2
(set -x && git -C dummy_git1 push ../dummy_git2 dummy_branch1:dummy_branch1)

rm -rf dummy_git1 dummy_git2  # clean up

git push决定发送哪些blob时,它似乎考虑了所有(?)现有引用以传输最小量的数据,**HEAD**除外。
当远程的HEAD被附加到某个分支上时,这就不是问题了,因为该分支仍然被认为是delta的基础。但是,当远程的HEAD被分离,因此是指向相关提交的唯一指针时,出于某种原因,git并不考虑它。相反,它把所有的东西都推上去,就好像远程的没有任何blob一样。即使遥控器实际上已经具有所有的二进制大对象。
基本上,这意味着git在计算增量时从 refs 开始,而不是从 commits 开始,这会导致在远程数据库上已经存在必要的blob时,不必要的大数据传输和慢数据传输。
我有两个问题:
1.为什么会发生这种情况?
1.有没有办法解决这个问题,而不在远程上创建一个命名的引用?

gcuhipw9

gcuhipw91#

当远程HEAD连接到某个分支时,这不是问题,因为该分支仍然被视为增量的基础。
这有点接近,但并不完全正确。这不是远程仓库的HEAD是否附加到分支名称的问题,而是本地Git是否能找到合适的“瘦包”的问题。
1.为什么会发生这种情况?
1.有没有办法解决这个问题,而不在远程上创建一个命名的引用?
1的答案很复杂,但2的答案很简单:“不可以”。
这里有大量的细节,很容易遗漏(或故意掩盖)一些项目,所以我将要给予的描述可能会遗漏一些东西,但你需要记住,Git的所有工作都是 * 本地 * 完成的。此外,即使使用file:// URL或等效的URL,“本地”和“远程”Git示例仍然使用通常的协议(大多数情况下)相互通信。
让我们从这个开始:

对象ID(OID)是通用的

一个Git仓库由两个数据库组成,一个保存分支、标签和其他名称(将它们Map到OID,每个名称对应一个OID),另一个是对象数据库。两者都是简单的key-value stores,我们在这里基本上不关心第一个数据库(我们只使用它来查找OID),所以我们专注于第二个数据库。
第二个数据库使用OID作为关键字;与每个OID关联的数据是提交、树、blob或带注解标记的对象数据(包括类型和长度前缀)。在对象访问级别,对象数据始终是完整的:大对象(例如,100 MB)总是完整的100 MB。* 用于 * 对象的OID本身仅仅是对完整数据(包括报头)运行某种校验和算法的结果。
Git目前使用的是SHA-1,也可以选择使用SHA-256。SHA-256的支持还不太完善,而且没有办法相互转换,所以实际上OID都是SHA-1的哈希值。这在这里并不重要,但作为一个具体的例子,它很有帮助:两个不同的Git软件实现会在不同的时间交换OID,这样一个Git就可以知道另一个Git是否有某个对象。
智能运输机与非智能运输机Git提交图
在我们继续之前,我们需要提到 transports(ssh,https等)在Git中有两种风格:一个 dumb transport 一次只处理一个对象,并且总是发送或接收整个对象。在这里你根本不会得到任何增量压缩。因此,dumb transport不常被使用。

  • smart* 传输让两个Git实现更紧密地交互。服务器(接收git push,发送git fetch)和客户端(颠倒角色)可以发送“have”和“want”消息。(在此之前,他们也可以就 capabilities 达成一致,但我们不需要在这里担心这个。)

接下来,我们需要看一下Git提交图,该图由四种对象类型的对象组成,但这四种对象类型中有三种可以引用其他对象:

    • 带注解的标记 * 具有 * 目标 * 对象:它存储它正在标记的对象的散列ID(可以是任何其他对象类型,但通常是提交);
  • 一个 * 提交 * 具有 * 父提交 * 和一个 * 树 * 对象:它存储某个先前提交集合和一个树的散列ID;
  • 一个 tree 对象包含一个〈mode,hash-ID,name〉元组的平面列表,其中 mode 定义了哈希ID引用的对象类型,而 name 是一个名称组件(正斜杠之间的部分,例如在path/to/file中);和
  • blob 对象包含未解释的数据。

树指的是子树,也指的是文件(模式100644和模式100755),符号链接(模式120000)和gitlinks(模式160000)。文件和符号链接使用BLOB对象来保存文件数据或符号链接目标;而gitlinks是终端数据(不要连接到 this 存储库中的任何东西--它们只是假设存在于某个 other 存储库中的散列ID)。
通过从适当的名称(分支名称、标签名称、替换对象名称等)开始,并 * 遍历 * 这个图以产生传递闭包,我们可以找出需要哪些对象来使名称集“工作”。添加深度限制器(--depth *n*对于某个整数 * n *)让我们限制图的遍历。
要复制整个仓库,我们只需遍历所有 reachable objects,从所有名称中获取完整的传递闭包。如果我们想从较小的名称集合中获取或推送,我们可以只从这些名称中进行遍历。这会产生一组必须存在的OID。
显然,这不是超高效的,但这是我们的基本出发点。

压缩、松散和填塞的物体

因为提交的文件通常非常类似于(虽然不是100%完全相同)* 以前 * 提交的文件,我们希望Git做一些奇特的压缩。Git的两个用法是zlibdelta encoding

Zlib压缩通常非常快,所以Git总是对每个对象都进行压缩。这包括Git所谓的 * 松散 * 对象,它们在其他情况下被存储为完整的对象数据。无论对象有多大,Git都会压缩它(包括其头部)以找到OID,假设这是新对象,然后将该对象作为一个文件存储在计算机的文件系统中,该文件的名称来自OID。
因此松散对象 * 不是 * delta编码的,如果我们对某个中等大小的源文件做了10个版本,创建了10个独立的松散对象,Git也许能够更有效地存储它们。因此Git会时不时地运行git pack-objects,从不同的对象创建一个 *pack文件 。为了创建这样的文件,Git会把相似的对象组合在一起,然后进行无损压缩。二进制数据增量压缩,从“较早”的对象中获取字节的运行以用于“较晚”的对象。这些增量可以被链接,例如,“最新”的对象可能说“从较早的对象获取4000字节”,但是打包的较早的对象以“从更早的对象获取500字节”开始。
请注意,这里的“较早”和“较晚”是任意的:在delta压缩方面,没有特别的理由使用在4月创建的对象先于5月创建的对象。实际上,我们倾向于使用较新的提交比使用较旧的提交更频繁,所以Git试图将较新的对象放在delta链的头部,而将较旧的对象放在尾部。这很棘手,因为对象本身没有日期信息:相反,Git在遍历图的过程中将提交和标签日期信息反向传播到对象中,这是一种尽力而为的方式。
现在,在任何一个给定的存储库中,还有一个次要规则:如果一个打包对象(例如,散列ID P3)引用另一个打包对象(散列ID P2),而后者引用另一个对象(散列ID P1),则 * 所有这三个 * 打包对象必须存在于该打包文件中。这意味着,如果您打开了一个打包文件,并试图从中读取对象,则不必打开任何 * 其他 * 对象:你需要的一切都在这里,在这个包文件里。
这对于fetch/push来说是不好的,所以Git在这个规则中添加了一个例外:
精简包 * 可以通过对象的散列ID来引用对象,但不包含这些对象。

智能传输使用精简包

我们现在可以考虑git push如何在您的情况下工作。假设一个智能传输-这是通常的情况,在这里也是有效的-我们有一个发送Git作为客户端启动,并调用服务器作为接收Git。
发送者现在要求接收者列出他们的分支、标签和其他名称,以及相应的OID。对于每个OID,作为发送者,我们可以假设他们拥有这些对象。
我们可以做进一步的假设 * 如果 * 它们告诉我们它们不是一个 * 浅 * 仓库:1如果它们有一些 commit 对象,它们也有 * 每个早期的commit *。它们有 * 所有的对象 *(树和blob对象),这些对象伴随着那些commit和所有早期的commit。
如果它们 * 是 * 一个浅仓库,我们只能假设它们有那些特定的提交。
我们现在查看自己的仓库,看看我们要发送哪些提交和/或其他对象。当然,我们需要发送 latest 的提交,我们明确地git push-ing;但是我们也需要发送我们之前的每个提交,直到我们命中了他们的提交。如果他们说他们已经提交了 X,对于一些 X,我们的图遍历命中了 X,我们不做浅克隆的情况,我们可以在这里停止!
我们现在有了一个本地的对象列表,我们认为他们需要这些对象。我们把这些对象做成一个 * 精简包 *,对我们检测到他们有的对象进行增量压缩,如果我们有这些对象的话。如果 * 我们 * 是一个浅克隆,我们可能一开始就没有对象,在这种情况下,我们不能做好增量压缩。
无论如何,我们构建一个精简包并将其发送过来。服务器在接收到作为git push一部分的精简包后,会使用其本地已有的对象,通过使用增量所需的任何增量基础对象“充实“精简包来”修复”精简包。
(The receiver现在会像往常一样运行pre-receive和update钩子,如果一切顺利,它会在git push结束时按照发送者的请求更新其名称数据库中的名称。在现代的Git中,这个包现在已经变胖了,会被移到接收者的对象数据库中,离开隔离区。在旧的Git中,它已经在那里了:没有隔离区。)
1Git最新推出的部分克隆可能会在这方面大开杀戒:这是一个设计选择,它决定了是否将部分克隆视为浅层克隆,或者甚至是禁止推送部分克隆。

这是您第一个问题的答案

缺少增量的、非常胖的“瘦包”的出现是因为我们的本地Git没有意识到他们--接收Git仓库--有足够的基对象集。
请注意,当我们(客户端)具有浅克隆,则我们通常需要深度2或更深的克隆来获得适当的检测,即使服务器有正确的名称和哈希ID,Git的算法也会更好--可以只查看提交哈希ID--但是Git的作者对图遍历做了一些深思熟虑的选择,以支持在比较罕见(浅克隆)的情况下使用较少的CPU。

相关问题