如何更新 git 浅克隆?

How to update a git shallow clone?

背景

(对于 tl;dr,请参阅下面的#questions)

我有多个 git 存储库浅克隆。我使用的是浅克隆,因为它比深克隆小得多。每个克隆做大约 git clone --single-branch --depth 1 <git-repo-url> <dir-name>.

这很好用,只是我不知道如何更新它。

当我通过标签克隆时,更新没有意义,因为标签在时间点被冻结(据我所知)。在这种情况下,如果我想更新,这意味着我想通过另一个标签克隆,所以我只是 rm -rf <dir-name> 并再次克隆。

当我克隆了 master 分支的 HEAD 然后想更新它时,事情变得更加复杂。

我试过 git pull --depth 1 但是虽然我没有向远程存储库推送任何东西,但它抱怨它不知道我是谁。

我尝试了 git fetch --depth 1,但虽然它似乎更新了一些东西,但我检查它不是最新的(远程存储库中的某些文件与我的克隆中的内容不同)。

在 之后,我尝试了 git fetch --depth 1; git reset --hard origin/master,但有两件事:首先我不明白为什么需要 git reset,其次,虽然文件似乎是最新的, 一些旧文件仍然存在,git clean -df 不删除这些文件。

问题

使用 git clone --single-branch --depth 1 <git-repo-url> <dir-name> 创建克隆。如何更新它以达到与 rm -rf <dir-name>; git clone --single-branch --depth 1 <git-repo-url> <dir-name> 相同的结果?或者 rm -rf <dir-name> 并再次克隆是唯一的方法吗?

备注

这不是 How to update a shallow cloned submodule without increasing main repo size 的副本,因为答案不符合我的期望,而且我使用的是简单的存储库,而不是子模块(我不知道)。

TL;DR

假设您有一个现有的 --depth 1 存储库,该存储库是从 b运行ch B 克隆的,并且您希望 Git 作为如果您删除了 re-cloned,您可以使用以下命令序列:

git fetch --depth 1
git reset --hard origin/B
git clean -dfx

(例如,git reset --hard origin/master——我不能在上面的 code-literal 部分使用斜体)。您应该能够在其他两个命令之前或之后的任何时候执行 git clean 步骤,但是 git reset 必须在 git fetch.

之后

[slightly reworded and formatted] Given a clone created with git clone --single-branch --depth 1 <em>url</em> <em>directory</em>, how can I update it to achieve the same result as rm -rf <em>directory</em>; git clone --single-branch --depth 1 <em>url</em> <em>directory</em>?

请注意,使用 --depth 1 时,--single-branch 默认值 。 (单个)b运行ch 是您用 -b 给出的那个。关于将 -b 与标签一起使用,这里有一个很长的旁白,但我会把它留到以后再说。如果你使用-b,你的Git会询问“上游”Git——[=374=的Git ]url—其中 b运行ch it 有 checked-out,假装你使用了 -b <em>thatb运行ch</em>。这意味着在使用 --single-branch without -b 时要小心以确保此上游存储库的当前 b运行ch 是合理的,这一点很重要,当然,当您 do 使用 -b 时,以确保您提供的 b运行ch 参数确实命名为 b运行ch,不是标签。

简单的答案基本上就是这个,有两个细微的变化:

After , I tried git fetch --depth 1; git reset --hard origin/master, but two things: first I don't understand why git reset is needed, second, although the files seems to be up to date, some old files remains, and git clean -df does not delete these files.

两个细微的变化是:确保使用 origin/<em>b运行chname</em>,并添加 -xgit clean -d -f -xgit clean -dfx)到 git clean 步骤。至于为什么,那就有点复杂了。

怎么回事

没有 --depth 1git fetch步骤调用另一个Git并从中得到一个b[=782=的列表]ch 名称和相应的提交哈希 ID。也就是说,它找到了 all 上游的 b运行ches 及其当前提交的列表。然后,因为你有一个 --single-branch 存储库,你的 Git 扔掉除了单个 b运行ch 之外的所有东西,并带来所有东西 Git 需要将当前提交连接回您存储库中已有的提交。

有了 --depth 1,您的Git 根本不会费心将新提交连接到旧的历史提交。相反,它只获得一个提交和完成该提交所需的其他 Git objects。然后它会写入一个额外的“浅移植”条目以将一次提交标记为新的 pseudo-root 提交。

常规(non-shallow)克隆和获取

这些都与 Git 使用普通(non-shallow、non-single-branch)克隆时的行为有关:git fetch 调用上游 Git,获取所有内容的列表,然后将 您还没有的任何内容 带过来。这就是为什么初始克隆如此缓慢,而 fetch-to-update 通常如此之快的原因:一旦获得完整克隆,更新很少会带来太多:可能是几个提交,可能是几百个,并且大多数这些提交也不需要太多其他内容。

存储库的历史由提交形成。每个提交都将其 parent 提交(或合并,parent 提交,复数)命名为从“最新提交”向后的链以前的提交,一些 more-ancestral 提交,等等。链最终会在到达没有 parent 的提交时停止,例如在存储库中进行的第一次提交。这种提交是 root 提交。

也就是说,我们可以绘制一个提交图。在一个非常简单的存储库中,图形只是一条直线,所有箭头都指向后方:

o <- o <- o <- o   <-- master

名称 master 指向第四个也是最新的提交,它指向第三个,它又指向第二个,它又指向第一个。

每个提交都带有该提交中所有文件的完整快照。完全没有改变的文件在这些提交中 shared:第四次提交只是从第三次提交中“借用”未更改的版本,第三次提交从第二次提交中“借用”它,依此类推.因此,每个提交都会命名它需要的所有“Git objects”,并且 Git 要么在本地找到那些 objects——因为它已经有了它们——或者使用fetch 协议将它们从另一个上游 Git 带过来。有一种称为“打包”的压缩格式,以及一种称为“瘦包”的网络 t运行sfer 的特殊变体,它允许 Git 做得更好/更漂亮,但原理很简单:Git 需要所有且只需要那些 object 与它正在获取的新提交一起使用的那些。你的 Git 决定它是否有那些 objects,如果没有,从他们的 Git.

获得它们

一个more-complicated,more-complete图通常有几个点b运行ches,一些点合并,多个b运行ch名称指向不同的b运行ch提示:

        o--o   <-- feature/tall
       /
o--o--o---o    <-- master
    \    /
     o--o      <-- bug/short

这里b运行chbug/short合并回master,而b运行chfeature/tall还在开发中。 name bug/short 现在可以(可能)完全删除:如果我们完成对它的提交,我们就不再需要它了。 master 末尾的提交命名为 两个 之前的提交,包括 bug/short 末尾的提交,因此通过获取 master 我们将获取bug/short 提交。

请注意,简单图和稍微 more-complicated 图都只有一个根提交。这很典型:所有有提交的存储库都有 至少 一个根提交,因为第一个提交总是根提交;但是大多数存储库也只有 一个根提交。但是,您可以有不同的根提交,如下图所示:

 o--o
     \
o--o--o   <-- master

或这个:

 o--o     <-- orphan

o--o      <-- master

其实只有master的那一个大概是把orphan合并成master,然后删掉名字orphan.

移植和替换

Git 长期以来(可能不稳定)支持 grafts,它被(更好,actually-solid)支持所取代通用 替换 。为了具体地掌握它们,我们需要在上面添加每个提交都有自己唯一 ID 的概念。这些 ID 是丑陋的 40 个字符的 SHA-1 散列、face0ff... 等等。事实上,每个 Git object 都有一个唯一的 ID,尽管出于图形目的,我们只关心提交。

画图,那些大hash ID用起来太痛苦了,所以我们可以用one-letter名字AZ代替。让我们再次使用此图,但输入 one-letter 名称:

        E--H   <-- feature/tall
       /
A--B--D---G    <-- master
    \    /
     C--F      <-- bug/short

提交 H 指回提交 EEHparent) .提交 G,这是一个 合并提交 ——意味着它至少有两个 parent——指回 DF,等等。

注意 b运行ch namesfeature/tallmasterbug/short,每个指向 一次提交。名称 bug/short 指向提交 F。这就是提交 F 在 b运行ch bug/short 上的原因……但是提交 C 也是如此。提交 Cbug/short 上,因为它是 reachable 从名称。这个名字让我们到达 FF 让我们到达 C,所以 C 在 b运行ch bug/short.[=220 上=]

但是请注意,提交 G,即 master 的提示,让我们提交 F。这意味着提交 Falso on b运行ch master这是Git中的一个关键概念:提交可能在一个多个,甚至[=374] =]no b运行ches. b运行ch 名称只是在提交图中开始的一种方式。还有其他方法,例如标签名称,refs/stash(它让您进入当前存储:每个存储实际上是几次提交)和引用日志(通常隐藏起来,因为它们通常只是杂乱无章).

然而,这也让我们进行移植和替换。嫁接只是一种有限的替换,shallow 存储库使用的是一种有限的嫁接形式。1 我不会在这里完整描述替换它们有点复杂,但总的来说,Git 对所有这些所做的是将移植或替换用作“instead-of”。对于 commits 的具体情况,我们在这里想要的是能够改变——或者至少,假装改变——parent ID 或任何提交的 ID ... 对于 shallow 存储库,我们希望能够假装所讨论的提交具有 no parents.


1浅存储库使用嫁接代码的方式不稳定。对于更一般的情况,我建议改用 git replace,因为那也是 而不是 不稳定的。移植物的唯一推荐用途是——或者至少是几年前——将它们放置足够长的时间以 运行 git filter-branch 复制 一个改变的—— grafted—history,之后你应该完全丢弃嫁接的历史。您也可以为此目的使用 git replace,但与移植物不同,您可以永久使用 git replace 或 semi-permanently、 而无需 git filter-branch.


制作浅层克隆

为了对上游存储库的当前状态进行深度 1 浅克隆,我们将选择三个 b运行ch 名称之一——feature/tallmaster、或 bug/short—然后 t运行 将其指定为提交 ID。然后我们会写一个特殊的移植条目,上面写着:“当你看到那个提交时,假装它有 no parent 提交,即是根提交。”

假设我们选择 master。名称 master 指向提交 G,因此要对提交 G 进行 shallow 克隆,我们从upstream Git 和往常一样,但是后来写了一个特别的嫁接e尝试声明提交 Gno parents。我们将其放入我们的存储库中,现在我们的图表如下所示:

G   <-- master, origin/master

那些parent ID其实还在G里面;只是每次我们Git使用或者给我们看历史的时候,它马上就“嫁接”了nothing-at-all,所以G好像是 根提交,用于历史跟踪目的。

更新我们之前制作的浅层克隆

但是如果我们已经有了一个(深度为 1 的浅层)克隆,并且我们想要 更新 怎么办?嗯,这不是真正的问题。假设我们在 master 指向提交 B 时,在新的 b运行ches 和错误修复之前对上游进行了浅层克隆。这意味着我们目前有这个:

B   <-- master, origin/master

虽然 B 的真实 parent 是 A,但我们有一个 shallow-clone 嫁接条目说“假装 B 是根提交”。现在我们 git fetch --depth 1,它查找上游的 master——我们 调用 origin/master 的东西——并看到提交 G。我们从上游获取提交 G 及其 object,但故意 不要 获取提交 DF .然后我们更新我们的 shallow-clone 移植条目说“假装 G 也是根提交”:

B   <-- master

G   <-- origin/master

我们的存储库现在有 两个 根提交:名称 master(仍然)指向提交 B,我们的 parents (仍然)假装是non-existent,名字origin/master指向G,我们假装它的parent是non-existent.

这就是你需要git reset

的原因

在普通的存储库中,您可能会使用 git pull,实际上是 git fetch 后跟 git merge。但是 git merge 需要历史记录,而我们有 none:我们用伪装的 root 提交伪造了 Git,它们背后没有历史记录。所以我们必须改用git reset

git reset的作用有点复杂,因为它最多可以影响三个不同的东西:a b运行ch nameindex,以及work-tree。我们已经看到 b运行ch 名称是什么:它们只是指向一个(一个,特定的)提交,我们称之为 b[=782= 的 tip ]通道。剩下索引和 work-tree.

work-tree 很容易解释:这是您所有文件所在的位置。就是这样:不多也不少。它在那里,因此您实际上可以使用 Git:Git 是关于永远存储每一次提交,以便它们都可以检索。但它们的格式对凡人来说毫无用处。要使用,一个文件——或者更典型地,整个提交的文件的价值——必须被提取成它的正常格式。 work-tree 就是发生这种情况的地方,然后您可以处理它并使用它进行新的提交。

索引有点难解释。这是 Git 特有的东西:其他版本控制系统没有,或者即使有类似的东西,他们也不会公开。 Git 确实如此。 Git 的索引本质上是您保留 next 提交的位置,但这意味着它开始持有 current 提交您已将其提取到 work-tree 中,Git 使用它来加快 Git 的速度。我们稍后会对此进行详细说明。

git reset --hard 的作用是影响 所有三个 :b运行ch 名称、索引和 work-tree。它移动 b运行ch 名称以便它指向一个(可能不同的)提交。然后它更新索引以匹配该提交,并更新 work-tree 以匹配新索引。

因此:

git reset --hard origin/master

告诉Git查找origin/master。由于我们 运行 我们的 git fetch,现在指向提交 G。 Git 然后使 our master——我们当前的(也是唯一的)b运行ch——也指向提交 G,然后更新我们的索引和work-tree。我们的图表现在看起来像这样:

B   [abandoned - but see below]

G   <-- master, origin/master

现在 masterorigin/master 都命名为 commit G,而 commit G 是 checked-out 到 work-tree。

为什么需要git clean -dfx

这里的答案有点复杂,但通常是“你不知道”(需要git clean)。

当您 需要 git clean 时,那是因为您或您 运行 将文件添加到您的 work-tree还没跟Git讲过。这些是 未跟踪 and/or 忽略 文件。使用 git clean -df 将删除 untracked 文件(和空目录);添加 -x 也会删除忽略的文件。

有关“未跟踪”和“忽略”之间的更多区别,请参阅 this answer

为什么不需要 git clean:索引

我在上面提到过,您通常不需要 运行 git clean。这是因为索引。正如我之前所说,Git 的索引主要是“下一次提交”。如果您从不添加自己的文件——如果您只是使用git checkout 检查您一直以来的各种现有提交,或者您使用 git fetch 添加的提交;或者,如果您正在使用 git reset --hard 移动 b运行ch 名称并将索引和 work-tree 切换到另一个提交——那么索引中的任何内容 现在 有没有因为一个更早的git checkout(或者git reset把它放到索引里,也放到work-tree.

换句话说,索引有一个短而快Git访问的摘要manifest描述当前work-tree. Git 使用它来了解现在 work-tree 中的内容。当您要求 Git 切换到另一个提交时,通过 git checkoutgit reset --hard,Git 可以快速将现有索引与新提交进行比较。任何 已更改 、Git 的文件必须从新提交中提取(并更新索引)。 新添加、Git 的任何文件也必须提取(并更新索引)。任何消失的文件——在现有索引中,但不在新提交中——Git必须删除 ...这就是 Git 所做的。 Git 根据当前索引与新提交之间的比较,更新、添加和删除 work-tree 中的那些文件。

这意味着如果您需要git clean,您一定在Git之外做了一些添加文件的事情。这些添加的文件不在索引中,因此 by definition,它们未被跟踪 and/or 被忽略。如果它们只是未被跟踪,git clean -f 会删除它们,但如果它们被忽略,则只有 git clean -fx 会删除它们。 (您希望 -d 仅删除清理期间为空或变空的目录。)

放弃的提交和垃圾收集

我提到并绘制了更新的浅图,当我们 git fetch --depth 1 然后 git reset --hard 时,我们最终 放弃 之前的深度- 1 个浅图提交。 (在我绘制的图表中,这是提交 B。)但是,在 Git 中,放弃的提交很少真正被放弃——至少,不是立即放弃。相反,一些像 ORIG_HEAD 这样的特殊名称会保留一段时间,并且每个 reference—b运行ches 和 tags 都是引用的形式—随身携带log 个“先前值”。

您可以使用 git reflog <em>refname</em> 显示每个 reflog。例如,git reflog master 不仅向您显示哪些提交 master 现在 ,而且还显示它在过去 命名的哪些提交 [=511] =]. HEAD 本身也有一个 reflog,这是 git reflog 默认显示的内容。

Reflog 条目最终会过期。它们的确切持续时间各不相同,但默认情况下它们有资格在某些情况下的 30 天和其他情况下的 90 天后到期。一旦它们过期,这些 reflog 条目将不再保护被放弃的提交(或者,对于带注释的标签引用,带注释的标签 object—标签不 应该 移动,所以这种情况 不应该 发生,但如果它发生了——如果你强制 Git 移动标签——它的处理方式与所有其他引用相同。

一旦任何 Git object——提交、带注释的标签、“树”或“blob”(文件)——确实未被引用,Git 被允许真正删除它。2 只有在这一点上,提交和文件的底层存储库数据才会消失。即使那样,它也只会在 运行s git gc 时发生。因此,使用 git fetch --depth 1 更新的浅存储库与使用 --depth 1 的新克隆 相当 并不相同:浅存储库可能对原始提交有一些挥之不去的名称,并且不会删除额外的存储库 objects 直到这些名称过期或以其他方式 cleared-out.


2除了参考检查,objects 在它们过期之前也有最短的 time。默认为两周。这可以防止 git gc 删除 Git 正在创建但尚未建立引用的临时 object。例如,在进行新提交时,Git 首先将索引变成一系列 tree object,它们相互引用但没有 top-level 引用。然后它创建一个新的 commit object 引用 top-level 树,但还没有引用提交。最后,它更新当前的 b运行ch 名称。在最后一步完成之前,树和新提交是无法访问的!


--single-branch and/or 浅层克隆的特殊注意事项

我在上面提到,您给 git clone -b 起的名字可以指代一个 标签 。对于普通(non-shallow 或 non-single-branch)克隆,这就像人们预期的那样工作:你得到一个常规克隆,然后 Git 通过标签名称执行 git checkout。结果是在一个完全普通的克隆中通常分离的 HEAD。

然而,对于浅克隆或 single-branch 克隆,会产生一些不寻常的后果。这些都是,在某种程度上,Git让实施显示出来的结果。

首先,如果您使用 --single-branch,Git 会改变新存储库中的正常 fetch 配置。正常的 fetch 配置取决于您为 远程 选择的名称,但默认是 origin 所以我在这里只使用 origin。上面写着:

fetch = +refs/heads/*:refs/remotes/origin/*

同样,这是正常(不是single-branch)克隆的正常配置。这个配置告诉git fetch要获取什么,也就是“all b运行ches”。但是,当您使用 --single-branch 时,您会得到一条仅引用 b运行ch:

的获取行
fetch = +refs/heads/zorg:refs/remotes/origin/zorg

如果您正在克隆 zorg b运行ch.

无论你克隆哪个 b运行ch,都会进入 fetch 行。 每个 future git fetch 将遵守这一行,3 所以你不会获取任何其他 b运行 ches。如果您以后 想获取其他 b运行 项,则必须更改此行或添加更多行。

其次,如果你使用--single-branch并且你克隆的是一个标签,Git将放入一个相当奇怪的fetch行.例如,使用 git clone --single-branch -b v2.1 ... 我得到:

fetch = +refs/tags/v2.1:refs/tags/v2.1

这意味着你会得到nob运行ches,除非有人移动了标签,4git fetch 什么都不做!

第三,由于git clonegit fetch获取标签的方式,默认标签行为有点奇怪。请记住,标签只是对一个特定提交的引用,就像 b运行ches 和所有其他引用一样。不过,b运行ches 和标签之间有两个主要区别:b运行ches 是 expected 移动(标签不是),b运行ches 得到 重命名(而标签没有)。

请记住,在以上所有内容中,我们不断发现其他(上游)Git 的 master 成为我们的 origin/master,依此类推。这是重命名过程的示例。通过 fetch = 行,我们还简要地、精确地 看到了重命名 的工作原理 :我们的 Git 将他们的 refs/heads/master 并将其更改为我们的 refs/remotes/origin/master。这个名字不仅不同-看起来 (origin/master),而且字面意思不能与我们的任何b运行切。如果我们创建一个 b运行ch 命名为 origin/master,5 这个 b运行ch 的“全名”实际上是 refs/heads/origin/master 这是不同的来自另一个全名 refs/remotes/origin/master。只有当 Git 使用较短的名称时,我们才有一个(常规,本地)b运行ch 命名为 origin/master 和另一个不同的 (remote-tracking) b运行通道名为 origin/master。 (这很像在 a group where everyone is named Bruce。)

标签不会经历这一切。标签 v2.1 刚刚命名为 refs/tags/v2.1。这意味着无法将“他们的”标签与“您的”标签分开。您可以拥有自己的标签或他们的标签。只要没有人 移动 一个标签,这就没有关系:如果你 both 都有标签,它必须指向 一样object。 (如果有人开始移动标签,事情就会变得很糟糕。)

无论如何,Git通过一个简单的规则实现标签的“正常”获取:6 当Git已经有一个提交,如果某个标签 names 提交,Git 也会复制该标签。 对于普通克隆,第一个克隆获取所有标签,然后随后的 git fetch 操作获得 new 标签。然而,浅克隆根据定义省略了一些提交,即图中任何 graft-point 下面的所有内容。这些提交不会选择标签。他们不能:要有标签,你需要有提交。 Git 不允许(除非通过浅移植)在没有实际提交的情况下拥有提交的 ID。


3您可以在命令行上给 git fetch 一些参考规范,这些将覆盖默认值。这仅适用于默认提取。您也可以在配置中使用多个 fetch = 行,例如,只获取一组特定的 b运行 ches,尽管通常的方法是“de-restrict” initially-single-branch clone 是放回通常的 +refs/heads/*:refs/remotes/origin/* fetch line.

4因为标签不应该移动,我们可以说“这什么都不做”。但是,如果它们确实移动了,refspec 中的 + 代表力标志,因此标签最终会移动。

5不要这样做。这很混乱。 Git 会处理得很好——本地 b运行ch 在本地名称 space 中,remote-tracking b运行ch 在 remote-tracking 名称中 space——但它确实令人困惑。

6此规则与文档不符。我针对 Git 版本 2.10.1 进行了测试;较旧的 Git 可能会使用不同的方法。 Git 因为 2.26 现在也可能使用不同的规则git fetchgit push 有一个更新、更高级的协议可供使用。如果您关心标签的精确行为,您可能需要在您的特定 Git 版本上对其进行测试。

关于浅克隆更新过程本身,请参阅 commit 649b0c3 表格 Git 2.12(2017 年第一季度)。
该提交是以下内容的一部分:

Commit 649b0c3, commit f2386c6, commit 6bc3d8c, commit 0afd307 (06 Dec 2016) by Nguyễn Thái Ngọc Duy (pclouds)。 参见 commit 1127b3c, commit 381aa8e (06 Dec 2016) by Rasmus Villemoes (ravi-prevas)(由 Junio C Hamano -- gitster -- in commit 3c9979b 合并,2016 年 12 月 21 日)

shallow.c

This paint_down() is part of step 6 of 58babff (shallow.c: the 8 steps to select new commits for .git/shallow - 2013-12-05).
When we fetch from a shallow repository, we need to know if one of the new/updated refs needs new "shallow commits" in .git/shallow (because we don't have enough history of those refs) and which one.

The question at step 6 is, what (new) shallow commits are required in other to maintain reachability throughout the repository without cutting our history short?
To answer, we mark all commits reachable from existing refs with UNINTERESTING ("rev-list --not --all"), mark shallow commits with BOTTOM, then for each new/updated refs, walk through the commit graph until we either hit UNINTERESTING or BOTTOM, marking the ref on the commit as we walk.

After all the walking is done, we check the new shallow commits. If we have not seen any new ref marked on a new shallow commit, we know all new/updated refs are reachable using just our history and .git/shallow.
The shallow commit in question is not needed and can be thrown away.

So, the code.

The loop here (to walk through commits) is basically:

  1. get one commit from the queue
  2. ignore if it's SEEN or UNINTERESTING
  3. mark it
  4. go through all the parents and..
    • 5.a a mark it if it's never marked before
    • 5.b put it back in the queue

What we do in this patch is drop step 5a because it is not necessary.
The commit being marked at 5a is put back on the queue, and will be marked at step 3 at the next iteration. The only case it will not be marked is when the commit is already marked UNINTERESTING (5a does not check this), which will be ignored at step 2.

如果目标是在不获取整个历史记录的情况下更新浅层克隆(但允许获取较短的历史记录),那么使用现代版本 git (>= 2.11.1) 的替代方法可以工作有:

  • --shallow-since=... 只获取早于给定日期的提交
  • --shallow-exclude=... 获取而不获取作为给定提交
  • 祖先的提交