TL;DR
假设您有一个从分支 B 克隆的现有 --depth 1 存储库,并且您希望 Git 的行为就像您删除并重新克隆一样,您可以使用以下命令序列:
git fetch --depth 1
git reset --hard origin/B
git clean -dfx
(例如,git reset --hard origin/master——我不能在上面的代码文字部分中使用斜体)。您应该可以在其他两个命令之前或之后的任何时间点执行git clean 步骤,但git reset 必须在git fetch 之后。
长
[稍微改写和格式化] 给定一个使用git clone --single-branch --depth 1 <em>url</em> <em>directory</em> 创建的克隆,我如何更新它以获得与rm -rf <em>directory</em>; git clone --single-branch --depth 1 <em>url</em> <em>directory</em> 相同的结果?
注意--single-branch 是使用--depth 1 时的默认。 (单个)分支是您使用-b 提供的分支。关于使用带有标签的-b,这里还有很长的一段话,但我将把它留到以后。如果你不使用-b,你的Git会询问“上游”Git——url处的Git——it检查了哪个分支-out,并假装你使用了-b <em>thatbranch</em>。这意味着在使用 --single-branch without -b 时一定要小心,以确保这个上游存储库的当前分支是合理的,当然,当你这样做 使用-b,以确保您提供的分支参数确实命名了一个分支,而不是一个标签。
简单的答案基本上就是这个,有两个细微的变化:
在https://stackoverflow.com/a/20508591/279335之后,我尝试了git fetch --depth 1; git reset --hard origin/master,但是有两件事:首先我不明白为什么需要git reset,其次,虽然文件似乎是最新的,但仍然有一些旧文件,以及@ 987654345@ 不会删除这些文件。
两个细微的变化是:确保使用origin/<em>branchname</em>,并将-x(git clean -d -f -x 或git clean -dfx)添加到git clean 步骤。至于为什么,那就有点复杂了。
发生了什么
没有--depth 1,git fetch 步骤调用另一个 Git 并从中获取分支名称和相应提交哈希 ID 的列表。也就是说,它会找到 all 上游分支及其当前提交的列表。然后,因为你有一个--single-branch 存储库,你的 Git 会抛出除单个分支之外的所有分支,并带来 Git 将当前提交连接回你已经拥有的提交所需的一切您的存储库。
有了 --depth 1,您的 Git 根本不会费心将新提交连接到旧的历史提交。相反,它只获取一个提交和完成该提交所需的其他 Git 对象。然后它会写入一个额外的“浅嫁接”条目,以将该提交标记为新的伪根提交。
常规(非浅层)克隆和获取
这些都与使用普通(非浅层、非单分支)克隆时 Git 的行为方式有关:git fetch 调用上游 Git,获取所有内容的列表,然后将其带过来任何你还没有的东西。这就是为什么初始克隆如此缓慢,而获取更新通常如此之快的原因:一旦你得到一个完整的克隆,更新很少会带来很多东西:可能是几个提交,可能是几百个,并且大多数提交也不需要太多其他内容。
存储库的历史是由提交形成的。每个提交命名它的 parent 提交(或对于合并,父提交,复数),在一个从“最新提交”到前一个提交,再到一些更祖先的提交的链中,以及很快。当到达没有父级的提交时,链最终会停止,例如在存储库中进行的第一次提交。这种提交是 root 提交。
也就是说,我们可以绘制提交图。在一个非常简单的存储库中,该图只是一条直线,所有箭头都指向后方:
o <- o <- o <- o <-- master
名称master 指向第四个也是最新的提交,它又指向第三个,又指向第二个,又指向第一个。
每个提交都带有该提交中所有文件的完整快照。完全没有更改的文件在这些提交中共享:第四次提交只是从第三次提交中“借用”未更改的版本,从第二次提交中“借用”它,依此类推。因此,每个提交都会命名它需要的所有“Git 对象”,Git 要么在本地找到这些对象——因为它已经拥有它们——要么使用fetch 协议将它们从另一个上游 Git 带过来。有一种称为“打包”的压缩格式,以及一种称为“瘦包”的网络传输的特殊变体,它允许 Git 做得更好/更漂亮,但原理很简单:Git 需要所有,而且只需要那些对象随着新的提交,它正在接受。你的 Git 决定它是否有这些对象,如果没有,从他们的 Git 获取它们。
一个更复杂、更完整的图通常有几个分支点,一些点合并,多个分支名称指向不同的分支提示:
o--o <-- feature/tall
/
o--o--o---o <-- master
\ /
o--o <-- bug/short
这里的分支bug/short 被合并回master,而分支feature/tall 仍在开发中。 name bug/short 现在可以(可能)被完全删除:如果我们完成了对它的提交,我们就不再需要它了。 master 顶端的提交命名两个 以前的提交,包括 bug/short 顶端的提交,因此通过获取 master,我们将获取 bug/short 提交。
请注意,简单图和稍微复杂的图都只有一个根提交。这很典型:所有有提交的存储库至少有一个根提交,因为第一个提交始终是根提交;但大多数存储库只有一个根提交。但是,您可以有不同的根提交,如下图所示:
o--o
\
o--o--o <-- master
或者这个:
o--o <-- orphan
o--o <-- master
实际上,只有master 的那个很可能是通过将orphan 合并到master,然后删除名称orphan 制成的。
移植和替换
长期以来,Git 一直(可能不稳定)对 grafts 的支持被替换为对通用 替换 的(更好,实际上更可靠)支持。为了具体掌握它们,我们需要在上面添加每个提交都有自己唯一 ID 的概念。这些 ID 是丑陋的 40 字符 SHA-1 哈希、face0ff... 等等。事实上,每个 Git 对象都有一个唯一的 ID,尽管出于图表目的,我们只关心提交。
对于绘制图形,那些大的哈希 ID 使用起来太痛苦了,所以我们可以使用单字母名称 A 到 Z 来代替。让我们再次使用该图,但输入一个字母的名称:
E--H <-- feature/tall
/
A--B--D---G <-- master
\ /
C--F <-- bug/short
提交H 指回提交E(E 是H 的父级)。提交G,这是一个合并提交——意味着它至少有两个父节点——指回D和F,等等。
请注意,分支名称、feature/tall、master 和bug/short,每个都指向单个提交。名称bug/short 指向提交F。这就是为什么提交 F 位于分支 bug/short 上的原因......但提交 C 也是如此。提交 C 位于 bug/short 上,因为它从名称中可达。这个名字让我们到达F,F 让我们到达C,所以C 位于分支bug/short。
但是请注意,提交 G,master 的提示,让我们提交 F。这意味着提交F 也是 在分支master 上。 这是 Git 中的一个关键概念:提交可能在 one、many 甚至 no 分支上。 A分支名称只是在提交图中开始的一种方式。还有其他方法,例如标签名称、refs/stash(它可以让您进入当前存储:每个存储实际上是几个提交)和 reflog(通常隐藏在视图之外,因为它们通常只是杂乱无章)。
然而,这也让我们进行移植和替换。移植只是一种有限的替换,shallow 存储库使用有限形式的移植。1我不会在这里完整描述替换,因为它们有点复杂,但总的来说,Git 对所有这些所做的是将移植或替换用作“替代”。对于 commits 的具体情况,我们在这里想要的是能够改变——或者至少,假装改变——任何提交的父 ID 或多个 ID。 . 对于 shallow 存储库,我们希望能够假装有问题的提交有 个父级。
1浅层存储库使用移植代码的方式不不稳定。对于更一般的情况,我建议改用git replace,因为这也是并且现在不不稳定。移植的唯一推荐用途是(或者至少是,几年前)将它们放置在足够长的时间以运行 git filter-branch 以复制一个改变的 - 移植的 - 历史,之后你应该只是完全抛弃嫁接的历史。您也可以为此目的使用git replace,但与移植不同的是,您可以永久或半永久地使用git replace,不需要git filter-branch。
制作浅层克隆
要对上游存储库的当前状态进行深度 1 浅层克隆,我们将选择三个分支名称之一——feature/tall、master 或 bug/short——并将其转换为提交 ID .然后我们将编写一个特殊的嫁接条目,上面写着:“当你看到那个提交时,假装它有 no 个父提交,即,是根提交。”
假设我们选择master。 master 名称指向 commit G,所以要对 commit G 进行 shallow 克隆,我们照常从上游 Git 获取 commit G,然后写一个特殊的嫁接声称提交 G 的条目有 no 父母。我们将它放入我们的存储库中,现在我们的图表如下所示:
G <-- master, origin/master
那些父ID实际上仍然在G中;只是每次我们使用 Git 或向我们展示历史记录时,它都会立即“移植”什么都没有,因此 G 似乎是根提交,用于历史跟踪目的.
更新我们之前制作的浅层克隆
但是如果我们已经有了一个(深度为 1 的浅)克隆,并且我们想要更新它呢?嗯,这不是一个真正的问题。假设我们在master 指向提交B 时,在新分支和错误修复之前对上游进行了浅层克隆。这意味着我们目前有这个:
B <-- master, origin/master
虽然B 的真正父级是A,但我们有一个浅克隆嫁接条目说“假装B 是根提交”。现在我们git fetch --depth 1,它查找上游的master——这个东西我们调用origin/master——并看到提交G。我们从上游抓取提交 G 及其对象,但故意不要抓取提交 D 和 F。然后我们更新我们的浅克隆移植条目,说“假装G 也是根提交”:
B <-- master
G <-- origin/master
我们的存储库现在有 两个 根提交:名称 master(仍然)指向提交 B,我们(仍然)假装其父项不存在,名称 @987654440 @ 指向 G,我们假装其父母不存在。
这就是为什么你需要git reset
在普通存储库中,您可能会使用git pull,实际上是git fetch,后跟git merge。但是git merge 需要历史,而我们没有:我们用假根提交伪造了 Git,而他们背后没有历史。所以我们必须改用git reset。
git reset 所做的有点复杂,因为它最多可以影响三个不同的东西:分支名称、索引和工作-树。我们已经看到了分支名称是什么:它们只是指向一个(一个,特定的)提交,我们称之为分支的 tip。剩下的就是索引和工作树。
工作树 很容易解释:它是所有文件所在的位置。就是这样:不多也不少。它的存在是为了让您真正使用 Git:Git 就是要永远存储每一个提交,以便它们都可以被检索。但它们的格式对凡人无用。为了使用,一个文件——或者更典型地,整个提交的文件价值——必须被提取成它的正常格式。工作树就是发生这种情况的地方,然后您可以对其进行处理并使用它进行新的提交。
index 有点难以解释。这是 Git 特有的东西:其他版本控制系统没有,或者如果他们有类似的东西,他们不会公开它。吉特可以。 Git 的索引本质上是你保存 next 提交的地方,但这意味着它开始保存你提取到工作树中的 current 提交,而 Git使用它来使 Git 更快。我们稍后会详细介绍。
git reset --hard 所做的是影响所有三个:分支名称、索引和工作树。它移动分支名称,使其指向(可能不同的)提交。然后它更新索引以匹配该提交,并更新工作树以匹配新索引。
因此:
git reset --hard origin/master
告诉 Git 查找 origin/master。由于我们运行了git fetch,现在指向提交G。然后,Git 使 我们的 主分支——我们当前的(也是唯一的)分支——也指向提交 G,然后更新我们的索引和工作树。我们的图表现在看起来像这样:
B [abandoned - but see below]
G <-- master, origin/master
现在master 和origin/master 都命名为commit G,commit G 是签出到工作树中的那个。
为什么需要git clean -dfx
这里的答案有点复杂,但通常是“你不知道”(需要git clean)。
当你确实需要git clean时,这是因为你——或者你运行的东西——向你的工作树添加了你没有告诉Git的文件。这些是未跟踪和/或忽略文件。使用git clean -df 将删除未跟踪 文件(和空目录);添加-x 也会删除被忽略的文件。
有关“未跟踪”和“忽略”之间区别的更多信息,请参阅this answer。
为什么你不需要git clean:索引
我在上面提到你通常不需要运行git clean。这是因为索引。正如我之前所说,Git 的索引主要是“下一次提交”。如果您从不添加自己的文件——如果您只是使用git checkout 来检查您一直拥有的各种现有提交,或者您使用git fetch 添加的;或者如果您使用git reset --hard 移动分支名称并将索引和工作树切换到另一个提交 - 那么索引中的任何内容现在都在那里因为 一个较早的git checkout(或git reset)将它放入索引中,也放入工作树中。
换句话说,索引有一个简短的——Git 可以快速访问的——summary 或 manifest 描述当前的工作树。 Git 使用它来知道现在工作树中的内容。当您通过git checkout 或git reset --hard 要求Git 切换到另一个提交时,Git 可以快速将现有索引与新提交进行比较。任何已更改的文件,Git 必须从新提交中提取(并更新索引)。任何新添加的文件,Git 还必须提取(并更新索引)。任何已消失的文件——在现有索引中,但不在新提交中——Git 必须 remove ...这就是 Git 所做的。 Git 根据当前索引和新提交之间的比较来更新、添加和删除工作树中的这些文件。
这意味着如果你确实需要git clean,那么你一定是在Git之外做了一些添加文件的事情。这些添加的文件不在索引中,因此by definition,它们未被跟踪和/或被忽略。如果它们只是未被跟踪,git clean -f 将删除它们,但如果它们被忽略,则只有 git clean -fx 将删除它们。 (您希望-d 仅删除在清理过程中为空或变为空的目录。)
放弃的提交和垃圾收集
我提到并绘制了更新的浅图,当我们git fetch --depth 1 和git reset --hard 时,我们结束了放弃之前的 depth-1 浅图提交。 (在我绘制的图表中,这是提交 B。)然而,在 Git 中,被放弃的提交很少被真正放弃——至少,不是马上被放弃。取而代之的是,像ORIG_HEAD 这样的一些特殊名称会在它们身上保留一段时间,并且每个reference(分支和标签都是引用形式)都带有一个log“以前的值”。
您可以使用git reflog <em>refname</em> 显示每个引用日志。例如,git reflog master 不仅会显示master 的哪个提交现在,而且还显示它在过去命名的哪个提交。 HEAD 本身也有一个 reflog,这是 git reflog 默认显示的内容。
Reflog 条目最终会过期。它们的确切持续时间各不相同,但默认情况下,在某些情况下,它们有资格在 30 天后到期,在其他情况下则在 90 天后到期。一旦它们过期,这些 reflog 条目就不再保护放弃的提交(或者,对于带注释的标记引用,带注释的标记对象 - 标记不 supposed 移动,因此这种情况不是 supposed 发生,但如果发生了——如果你强制 Git 移动一个标签——它只是以与所有其他引用相同的方式处理)。
一旦任何 Git 对象——提交、注释标签、“树”或“blob”(文件)——真的没有被引用,Git 就可以真正删除它。2 只有在这一点上,提交和文件的底层存储库数据才会消失。即便如此,它也只会在运行git gc 时发生。因此,使用git fetch --depth 1 更新的浅存储库与使用--depth 1 更新的新克隆完全相同:浅存储库可能对原始提交有一些挥之不去的名称,并且不会删除额外的存储库对象,直到这些名称过期或以其他方式被清除。
2除了引用检查之外,对象在过期之前也有一个最短时间。默认为两周。这可以防止 git gc 删除 Git 正在创建但尚未建立引用的临时对象。例如,在进行新提交时,Git 首先将索引转换为一系列 tree 对象,它们相互引用但没有顶级引用。然后它创建一个新的commit 对象,该对象引用顶级树,但还没有任何内容引用提交。最后,它更新当前分支名称。在最后一步完成之前,树和新提交是无法访问的!
--single-branch 和/或浅克隆的特殊注意事项
我在上面提到你给git clone -b 的名字可以引用一个标签。对于普通的(非浅层或非单分支)克隆,这和预期的一样:你得到一个普通的克隆,然后 Git 通过标签名称执行 git checkout。结果是通常分离的 HEAD,在一个完全普通的克隆中。
但是,对于浅层或单分支克隆,会产生一些不寻常的后果。在某种程度上,这些都是 Git 让实现显示出来的结果。
首先,如果您使用 --single-branch,Git 会更改新存储库中的正常 fetch 配置。正常的fetch 配置取决于您为remote 选择的名称,但默认为origin,所以我将在这里使用origin。上面写着:
fetch = +refs/heads/*:refs/remotes/origin/*
同样,这是正常(非单分支)克隆的正常配置。此配置告诉git fetch要获取什么,即“所有分支”。但是,当您使用 --single-branch 时,您会得到一个仅引用一个分支的 fetch 行:
fetch = +refs/heads/zorg:refs/remotes/origin/zorg
如果您要克隆 zorg 分支。
无论你克隆哪个分支,都会进入fetch 行。每个未来git fetch 都会遵循这一行,3 sup> 所以你不会获取任何其他分支。如果您确实想稍后获取其他分支,则必须更改此行,或添加更多行。
其次,如果你使用--single-branch,而你克隆的是一个标签,Git 会放入一个相当奇怪的fetch 行。例如,git clone --single-branch -b v2.1 ... 我得到:
fetch = +refs/tags/v2.1:refs/tags/v2.1
这意味着您将获得 no 分支,除非有人移动了标签,否则4git fetch 将什么都不做!
第三,由于git clone和git fetch获取标签的方式,默认的标签行为有点奇怪。请记住,标签只是对一个特定提交的引用,就像分支和所有其他引用一样。不过,分支和标签之间有两个主要区别:分支是预计 可以移动(而标签不是),而分支是重命名(而标签不会)。
请记住,在上述所有内容中,我们不断发现另一个(上游)Git 的master 变成了我们的origin/master,等等。这是重命名过程的示例。我们还通过fetch = 行简要地了解了如何重命名工作:我们的Git 使用他们的refs/heads/master 并将其更改为我们的refs/remotes/origin/master。这个名字不仅不同-看起来 (origin/master),而且实际上不能与我们的任何分支机构相同。如果我们创建一个名为origin/master 的分支,5 这个分支的“全名”实际上是refs/heads/origin/master,这与另一个全名refs/remotes/origin/master 不同。只有当 Git 使用较短的名称时,我们才有一个(常规的、本地的)分支名为 origin/master 和另一个不同的(远程跟踪)分支名为 origin/master。 (这很像在a group where everyone is named Bruce。)
标签不会经历这一切。标签 v2.1 刚刚命名为 refs/tags/v2.1。这意味着无法将“他们的”标签与“您的”标签分开。您可以拥有自己的标签,也可以拥有他们的标签。只要没有人移动一个标签,这没关系:如果你两个都有标签,它必须指向同一个对象。 (如果有人开始移动标签,事情就会变得很糟糕。)
无论如何,Git 通过一个简单的规则实现“正常”的标签获取:6当 Git 已经有提交时,如果某些标签 names该提交时,Git 也会复制标签。 对于普通克隆,第一个克隆会获取所有标签,然后后续的 git fetch 操作会获取 new 标签。然而,根据定义,浅层克隆会省略一些提交,即图中任何移植点以下的所有内容。这些提交不会获取标签。他们不能:要拥有标签,您需要提交。不允许 Git(除了通过浅层移植)在没有实际提交的情况下拥有提交的 ID。
3您可以在命令行上给git fetch 一些参考规范,这些参考规范将覆盖默认值。这仅适用于默认提取。您也可以在配置中使用多个fetch = 行,例如,仅获取一组特定的分支,尽管“取消限制”初始单分支克隆的正常方法是放回通常的+refs/heads/*:refs/remotes/origin/*取线。
4由于标签不应该移动,我们可以说“这没有任何作用”。但是,如果它们确实移动了,则 refspec 中的 + 表示强制标志,因此标签最终会移动。
5不要这样做。这很令人困惑。 Git 可以很好地处理它——本地分支在本地名称空间中,而远程跟踪分支在远程跟踪名称空间中——但这真的很混乱。
6此规则与文档不符。我针对 Git 2.10.1 版本进行了测试;较旧的 Git 可能使用不同的方法。从 2.26 开始的 Git 也可以使用不同的规则,因为有一个更新的、更高级的协议供 git fetch 和 git push 使用。如果您关心标签的精确行为,您可能需要在您的特定 Git 版本上对其进行测试。