简而言之
如果你已经克隆了一个从分支B克隆的--depth 1
仓库,并且希望Git表现得像你已经删除并重新克隆了一样,可以使用以下命令序列:
git fetch --depth 1
git reset --hard origin/B
git clean -dfx
(e.g.,
git reset --hard origin/master
— 我无法在上面的代码文字区域中使用斜体字)。您可以在其他两个命令之前或之后执行
git clean
步骤,但
git reset
必须在
git fetch
之后执行。
长
[稍作改动和格式化] 假设已经使用git clone --single-branch --depth 1 url directory
创建了一个克隆副本,如何更新它以达到与rm -rf directory; git clone --single-branch --depth 1 url directory
相同的结果?
请注意,使用
--depth 1
时,默认情况下是
仅有一个分支。你可以用
-b
指定一个分支,否则Git会向"upstream" Git(
url处的Git)询问其当前所检出的分支,并假装你使用了
-b thatbranch
。因此,在使用
--single-branch
无需 -b
时,一定要确保该上游存储库的当前分支是合理的;当你使用
-b
时,确保所给的参数确实是一个分支名而不是标签名。
简单来说,答案基本上就是这个,只有两个细微的变化:
我在
https://dev59.com/PnnZa4cB1Zd3GeqPw_kE#20508591后尝试了
git fetch --depth 1; git reset --hard origin/master
,但有两个问题:首先我不理解为什么需要
git reset
,其次,尽管文件似乎是最新的,但仍然存在一些旧文件,并且
git clean -df
无法删除这些文件。两个小改变是:确保使用
origin/branchname
,并在
git clean
步骤中添加
-x
(
git clean -d -f -x
或
git clean -dfx
)。至于
why,那就有点复杂了。
发生了什么
没有使用
--depth 1
参数时,
git fetch
步骤会调用其他 Git 并从中获取分支名称和相应的提交哈希 ID 列表。也就是说,它会找到上游的所有分支及其当前提交的列表。然后,由于您有一个
--single-branch
存储库,
您的 Git 会丢弃除单个分支以外的所有内容,并将 Git 需要连接该当前提交与您已经在存储库中拥有的提交的所有内容带过来。
使用
--depth 1
参数时,您的 Git 不再连接新提交到旧历史提交。相反,它仅获取一个提交和其他 Git 对象,以完成该提交。然后,它会写入一个额外的“浅嫁接”条目,将该提交标记为一个新的伪根提交。
常规(非浅层)克隆和提取
这些都与在使用普通(非浅层、非单分支)克隆时Git的行为有关:
git fetch
调用上游Git,获取所有内容,然后带回
您没有的任何内容。这就是为什么初始克隆如此缓慢,而更新通常很快的原因:一旦您获得完整的克隆,更新很少有很多要传输的内容:也许只有几个提交,也许只有几百个,而且大多数提交也不需要其他内容。
存储库的历史记录由提交形成。每个提交命名其父提交(或对于合并,是父提交,复数),在从“最新提交”到上一个提交,再到某个更祖先的提交的链中向后移动。当它达到没有父项的提交时,链最终停止,例如在存储库中首次进行的第一个提交。这种提交是一个根提交。
也就是说,我们可以绘制提交图。在一个非常简单的存储库中,图形只是一条直线,所有箭头都指向后面:
o <- o <- o <- o <
名称
master
指向第四个也是最新的提交,它指向第三个提交,第三个提交指向第二个提交,第二个提交指向第一个提交。
每个提交都携带了一个包含所有文件的完整快照。未发生任何改变的文件在这些提交之间是共享的:第四个提交只是从第三个提交中“借用”未更改的版本,而第三个提交又从第二个提交中“借用”,依此类推。因此,每个提交都命名了它所需的所有“Git对象”,Git要么在本地找到这些对象(因为它已经拥有它们),要么使用
fetch
协议从上游Git获取它们。有一种被称为“packing”的压缩格式和一种专门用于网络传输的特殊变体被称为“thin packs”,它使Git可以做得更好/更高级,但原理很简单:Git需要与其拾取的新提交相关的所有对象。您的Git决定是否拥有这些对象,如果没有,则从它们的Git获取它们。
一个更复杂、更完整的图通常有几个分支点,一些合并点,以及指向不同分支末端的多个分支名称。
o--o <-- feature/tall
/
o--o--o---o <-- master
\ /
o--o <-- bug/short
这里将分支
bug/short
合并回
master
,而分支
feature/tall
仍在开发中。如果我们已经完成了对
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 长期以来一直支持(可能不稳定的)
移植,后来用通用的
替换(实际上非常可靠)来代替。为了具体理解它们,我们需要添加一个概念,即每个提交都有自己独特的 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中的一个关键概念是:提交可以在一个、多个或甚至没有分支上。分支名称只是在提交图中开始的一种方式。还有其他方法,例如标签名称、
refs/stash
(它可以让您进入当前的stash:每个stash实际上是一对提交)和reflogs(通常被隐藏起来,因为它们通常只是杂乱无章的)。
这也涉及到移植和替换。移植只是一种有限的替换方式,而
1浅层存储库使用了一种有限的移植形式。我不会在这里详细描述替换,因为它们有点更加复杂,但总的来说,Git对所有这些都使用移植或替换作为“代替”。对于
提交的特定情况,我们想要的是能够更改或至少
假装更改任何提交的父ID或ID... 对于
浅层存储库,我们希望能够假装该提交没有
父级。
1浅层存储库使用嫁接代码的方式并不稳定。对于更一般的情况,我建议使用git replace
,因为它也是稳定的。嫁接的唯一推荐用途是——或者至少在多年前是——将它们放在那里只用足够长的时间来运行git filter-branch
以复制已更改的历史记录,之后您应该完全丢弃嫁接的历史记录。您也可以使用git replace
来达到此目的,但与嫁接不同的是,您可以永久性地或半永久性地使用git replace
,无需使用git filter-branch
。
创建浅克隆
要创建一个深度为1的当前上游存储库状态的浅克隆,我们将选择三个分支名称之一 - feature/tall
, master
或bug/short
- 并将其转换为提交ID。 然后,我们将编写一个特殊的嫁接条目,其中说:“当您看到该提交时,请假装它没有父提交,即为根提交。”
假设我们选择master
。 名称master
指向提交G
,因此要对提交G
进行浅克隆,我们像往常一样从上游Git获取提交G
,但然后编写一个特殊的嫁接条目,声称提交G
没有父提交。 我们将其放入我们的存储库中,现在我们的图形如下:
G <
这些父ID实际上仍在G
内部;只是每次我们让Git使用或显示历史记录时,它会立即“嫁接”一个空的内容,以便于跟踪历史记录,使得G
似乎是一个根提交。
更新我们之前制作的浅克隆
但是,如果我们已经有了一个(深度为1的浅)克隆,并且我们想要更新它呢?好吧,这并不是真正的问题。假设我们在新分支和错误修复之前,当master
指向提交B
时,制作了一个上游的浅克隆。这意味着我们当前拥有以下内容:
B <
虽然B
的真正父节点是A
,但我们有一个浅克隆嫁接条目,说“假装B
是一个根提交”。现在我们运行git fetch --depth 1
,它查找上游的master
——我们称之为origin/master
的东西——并看到提交G
。我们从上游获取提交G
及其对象,但故意不获取提交D
和F
。然后我们更新我们的浅克隆嫁接条目,说“假装G
也是一个根提交”:
B <
G <
我们的存储库现在有两个根提交:两个。名称master
(仍然)指向提交B
,我们(仍然)假装它们的父项不存在,而名称origin/master
指向G
,我们假装它们的父项不存在。
这就是为什么你需要使用 git reset
在普通存储库中,您可能会使用git pull
,它实际上是git fetch
后跟git merge
。 但是git merge
需要历史记录,而我们没有:我们用虚假的根提交欺骗了Git,并且它们背后没有历史记录。因此,我们必须使用git reset
代替。
git reset
的操作有点复杂,因为它可以影响到三个不同的事物:一个分支名称,索引和工作目录。 我们已经看到了分支名称是什么:它们只是指向(一个、特定的)提交,我们称之为该分支的tip。 这就剩下索引和工作目录了。
工作树很容易解释:它是您的所有文件所在的地方。这就是全部,没有更多也没有更少。它存在的意义在于让您实际使用Git:Git的全部内容都存储在其中,永远不会丢失,因此可以检索到所有提交。但是它们以一种对普通人无用的格式存在。为了被使用,文件(或更常见的是一个完整的提交文件)必须被提取成正常的格式。工作树就是这样的地方,然后您可以在其上工作并使用它来创建新的提交。
索引有点难以解释。它是Git特有的东西:其他版本控制系统没有它,或者如果它们有类似的东西,它们不会暴露出来。Git有。Git的索引基本上是您要进行的下一个提交,但这意味着它开始时保存了您已经提取到工作树中的当前提交,并且Git使用它来使Git更快。我们稍后会详细介绍这个。
git reset --hard
命令会影响分支名称、索引和工作区这三个部分。它将分支名称“移动”到指向另一个(可能不同的)提交。然后,它会更新索引以匹配该提交,并更新工作区以匹配新的索引。
因此:
git reset --hard origin/master
告诉Git查找origin/master
。由于我们运行了git fetch
,现在它指向提交G
。然后Git将我们的master——我们当前(也是唯一的)分支——也指向提交G
,然后更新我们的索引和工作树。我们的图现在看起来像这样:
B [abandoned - but see below]
G <
现在,master
和origin/master
都指向提交G
,提交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快速访问的摘要或清单,描述了当前的工作树。Git使用它来知道现在工作树中有什么。当您通过git checkout
或git reset --hard
要求Git切换到另一个提交时,Git可以快速比较现有索引与新提交。任何已经更改的文件,Git必须从新提交中提取(并更新索引)。任何新增的文件,Git也必须提取(并更新索引)。任何消失的文件——即存在于现有索引中但不存在于新提交中的文件——Git必须删除……这就是Git所做的。Git根据当前索引和新提交之间的比较,在工作树中更新、添加和删除这些文件。
这意味着,如果你确实需要使用git clean
,那么你必须在Git之外做了一些添加文件的操作。这些添加的文件不在索引中,因此根据定义,它们是未跟踪和/或被忽略的。如果它们只是未跟踪的,git clean -f
将删除它们,但如果它们被忽略了,只有git clean -fx
才能删除它们。(你需要使用-d
来删除在清理过程中变为空的目录。)
放弃提交和垃圾回收
我提到并在更新的浅层图中绘制了,当我们使用
git fetch --depth 1
然后使用
git reset --hard
时,我们最终会放弃先前的深度为1的浅层图提交。(在我绘制的图中,这是提交
B
。)但是,在Git中,被放弃的提交很少真正被放弃-至少不是立即放弃。相反,一些特殊名称(如
ORIG_HEAD
)会暂时保留它们,并且每个引用-分支和标签都是引用的形式-都携带着“先前值”的日志。
您可以使用
git reflog refname
显示每个reflog。例如,
git reflog master
不仅显示
master
现在指向哪个提交,还显示它过去指向的提交。还有一个
HEAD
本身的reflog,这是
git reflog
默认显示的内容。
Reflog条目最终会过期。它们的确切持续时间因情况而异,但默认情况下,在某些情况下,它们在30天后可以过期,在其他情况下则在90天后可以过期。一旦它们过期,这些reflog条目将不再保护已弃用的提交(或对于带注释的标记引用,带注释的标记对象 - 标记不应该移动,因此不应该发生这种情况,但如果确实发生了 - 如果您强制Git移动标记,则以与所有其他引用相同的方式处理)。
一旦任何Git对象 - 提交、带注释的标记、“树”或“blob”(文件) - 真正地未被引用,Git就允许真正地将其删除。
2 只有在这一点上,提交和文件的底层存储库数据才会消失。即使在这种情况下,只有当运行
git gc
时,它才会发生。因此,使用
git fetch --depth 1
更新的浅存储库与使用
--depth 1
的新克隆不完全相同:浅存储库可能仍具有原始提交的某些残留名称,并且在这些名称过期或被清除之前,不会删除额外的存储库对象。
除了参考检查,对象在过期之前也需要最少的时间。默认为两周。这可以防止
git gc
删除Git正在创建但尚未建立引用的临时对象。例如,在进行新的提交时,Git首先将索引转换为一系列相互引用但没有顶级引用的
tree
对象。然后它创建一个新的
commit
对象,该对象引用顶级树,但是还没有任何对象引用该提交。最后,它更新当前分支名称。在此最后一步完成之前,树和新提交是不可访问的!
--single-branch
和/或浅克隆的特殊考虑事项
我在上面提到,git clone -b
给出的名称可以是一个tag。对于普通(非浅克隆或非单分支)克隆来说,这可以按照预期工作:你得到一个常规克隆,然后Git通过tag名称进行git checkout
。结果是通常的游离HEAD,位于完全普通的克隆中。
然而,使用浅克隆或单分支克隆时,会有一些不寻常的后果。这些后果在某种程度上都是Git让实现显示出来的结果。
首先,如果您使用--single-branch
,Git将修改新仓库中的正常fetch
配置。正常的fetch
配置取决于您选择的remote的名称,但默认值为origin
,因此我在这里只使用origin
。它的读取方式如下:
fetch = +refs/heads/*:refs/remotes/origin/*
再次强调,这是一个普通(非单分支)克隆的正常配置。此配置告诉git fetch
要获取什么,即“所有分支”。然而,当您使用--single-branch
时,您将得到一个仅涉及一个分支的获取行:
fetch = +refs/heads/zorg:refs/remotes/origin/zorg
如果你正在克隆zorg分支,无论你克隆哪个分支,都会进入fetch行。每次将来的git fetch都会遵守这个行,所以你不会获取其他分支。如果你想以后获取其他分支,你需要修改这行或添加更多行。
其次,如果你使用--single-branch并且你要克隆的是一个标签,Git将会放置一个相当奇怪的fetch行。例如,使用git clone --single-branch -b v2.1 ...我得到:
fetch = +refs/tags/v2.1:refs/tags/v2.1
这意味着您将不会得到任何分支,除非有人移动了标签4,git 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
。(这很像在
每个人都叫布鲁斯的团体。)
标签不需要经过这一系列的步骤。标签
v2.1
只是被命名为
refs/tags/v2.1
。这意味着无法区分“他们”的标签和“你的”标签。你只能拥有你的标签或者他们的标签。只要没有人移动标签,这并不重要:如果你们两个都有相同的标签,它必须指向相同的对象。(如果有人开始移动标签,情况会变得很丑陋。)
无论如何,Git通过一个简单的规则实现标签的“正常”获取:
6当Git已经有一个提交时,如果某个标签名称指向该提交,Git也会复制该标签。对于普通克隆来说,第一个克隆会获取所有标签,然后随后的
git fetch
操作获取新标签。然而,浅层克隆根据定义省略了一些提交,即图中任何嫁接点以下的所有内容。这些提交不会获取标签。他们无法:要获取标签,您需要拥有提交。Git不允许(除了浅层嫁接之外)在没有实际提交的情况下获得提交ID。
3您可以在命令行上为git fetch
提供一些refspec,这些将覆盖默认设置。这仅适用于默认获取。您还可以在配置中使用多个fetch =
行,例如,仅获取特定的一组分支,尽管“取消限制”最初单个分支克隆的常规方法是恢复通常的+refs/heads/*:refs/remotes/origin/*
获取行。
4由于标签不应该移动,因此我们可以说“这没有任何作用”。但是,如果它们确实移动了,则refspec中的+
表示强制标志,因此标签最终会移动。
5别这样做。这很令人困惑。Git会处理它得很好——本地分支位于本地名称空间中,而远程跟踪分支位于远程跟踪名称空间中——但真的很令人困惑。
这个规则与文档不符。我测试的是Git版本2.10.1;更旧的Git可能使用不同的方法。自Git 2.26以来,可能会使用不同的规则,因为现在有一个更新、更高级的协议用于
git fetch
和
git push
。如果你关心标签的精确行为,你可能需要在你特定的Git版本上进行测试。
.git
目录,具体取决于很多因素。然而,有时重新打包只会使.git
目录变得更大。(这不是可能发生的,但确实会发生。) - torek