问题的根源在于
git clone
的
--depth
选项也会打开
--single-branch
。为了在克隆时取消单分支模式,请使用
--no-single-branch
。要在之后取消单分支模式,请参见
接受的答案 和
如何“撤消”--single-branch克隆?
请注意,在取消单分支克隆之后,您将不得不再次运行
git fetch --depth 1
。这将从您克隆的存储库中检索其余的分支名称,所有这些名称都变成远程跟踪名称;请参阅下面的详细信息,并允许您在每个这样的名称上运行
git checkout
以创建一个同名的本地分支。您还可以使用
git remote set-branches --add
将单个名称添加到现有远程;同样,您需要另一个
git fetch --depth
。
可选阅读:详细信息或者为什么上述方法有效。
一个Git仓库——严格来说,是一个非裸仓库——实际上由以下三部分组成:
- 一对数据库,如下所述;
- 一个索引,通过它Git知道要提交哪些文件,即跟踪哪些文件,尽管索引远不止是文件列表;和
- 一个工作树或工作目录,您可以在其中使用和修改您的文件。这些文件确实是您自己的,实际上并不存在于Git中。在Git内部的主数据库中的文件都是只读的,以一种特殊的压缩和去重形式存在,只有Git本身才能使用。
当您运行
git clone
时,您让Git将主数据库——保存所有提交和文件等的那个——基本上整体复制,但让它读取另一个数据库,解析它并理解它,并将一个不同的数据库写入到您的克隆中。
"
--depth
"标志影响主数据库,因此您不会完全复制它。 "
--single-branch
"标志 - 正如我们所指出的那样,
--depth
自动打开 - 影响次要数据库。 在继续之前,让我们为这两个数据库命名,以便我们不再引用一些尴尬的短语,比如
"第一方派对":
我一直称之为“主数据库”的东西是Git的
对象存储。这是一个简单的
键-值数据库,其中键是哈希ID,而值是Git的提交和其他内部对象。
1通常这是Git仓库的最大部分。
2
第二个数据库也是一个简单的键-值存储,键是名称,包括分支和标签名称,但几乎包括Git的所有其他名称
3,值是哈希ID。每个名称只存储一个哈希ID,因为这是所需的全部。
因此,简要概括一下,
git clone
命令——如果没有使用
--single-branch
和
--depth
标志的话——会调用另一个 Git,并让其列出其所有分支、标签和其他名称。然后,它将使用这些名称来查找原始存储库中的所有提交和其他 Git 对象,并让另一个 Git 发送所有这些对象。结果是一个对象数据库的完全副本。
4 现在,您已经拥有了另一个 Git 存储库中的所有提交。
同时,你自己的Git会获取他们所有的“名称”,并挑选其中一些“名称”以及处理它们。通常情况下,你的Git会获取他们所有的“分支”名称——其全名可能是像
refs/heads/master
、
refs/heads/topic
等等,然后将它们“重命名”为你自己的“远程跟踪”名称:如
refs/remotes/origin/master
、
refs/remotes/origin/topic
等等。接着,你的Git会创建自己独立的名称到哈希ID的数据库,其中没有任何分支名称。
5
最终结果是,在
git clone
这一步之后,你拥有了
所有的提交和
没有分支!不过,这种情况很快就会被
git clone
的最后一步纠正。只要你没有使用
--no-checkout
选项,
git clone
的最后一步就是运行
git checkout
,这一步实际上会
创建一个分支。你的Git创建的分支名称就是你用
-b
选项提供的名称。如果你没有提供
-b
选项,你的Git会询问另一个Git推荐哪个分支,如果其他方法都失败了,你的Git会假定你自己的默认初始分支名称。
6
每个提交对象都引用一个(单独的)“树”对象,该对象保存该提交的快照并具有元数据。每个树对象都保存一组部分文件名 - 名称组成部分,按需要将它们串在一起和另一个哈希ID。该哈希ID标识要么是另一个树形结构,要么是存储某个文件内容的“blob”对象。Git通过阅读所有必要的子树来构建文件的完整名称,并在其索引中存储完整文件名称,然后使用索引中所见的名称和blob哈希ID提取文件。这不是完整的说明,但这就是为什么Git无法存储空目录的原因:无法将其放入Git的索引中。
对象数据库还可以包含“注释标记对象”,每个对象都保存哈希ID,通常是提交的哈希ID。这就是Git提供其注释标记的方式。
有例外情况:由于某种原因保留积累新名称(如新分支和标签名称),但几乎从不获得任何新提交的旧存储库。但通常情况下,对象数据库是使用最多的地方,也是最初克隆的大部分时间使用的地方。
其他名称包括诸如注释,正在进行的二分法,某些交互式rebase期间需要的名称等等。基本上,任何存储单个哈希ID的名称都会进入此数据库。不会这样做的名称,例如像origin这样的远程名称,不会出现在这里。它们通常会出现在.git目录中的config文件中。
当前实现这个数据库的方式相当糟糕。有时,名称被存储为文件系统中的目录和文件名,这意味着在大小写不敏感的文件系统上(例如Windows和macOS系统上的默认文件系统),分支名称变成了大小写不敏感的。有时,名称存储在名为packed-refs的纯文本文件中,使它们始终区分大小写,就像Git一直打算的那样。一些特殊的名称,例如HEAD,根本不会进入packed-refs文件,而是始终作为单独的文件存储在.git目录中。目前正在进行工作以提供一个合适的数据库,以解决这里的一堆问题。
技术上说,结果可以并且通常会省略任何无法通过使用名称找到的对象。不过在这里我们忽略这个细微的区别。
您的 Git 通常也会省略所有非分支和非标签名称。它如何处理它们的标签名称是复杂的,但在正常(非单分支、非深度限制)克隆中,您通常会复制所有它们的标签名称。
这以前只是硬编码为 master,但现在变成可配置的。
如何影响 --single-branch
使用 --single-branch
选项,你的 Git 不会使用所有分支名称,而是只使用从你的 -b
选项中指定的一个分支名称,并采用相同的默认设置: 如果你没有提供 -b
,你的 Git 会询问对方的 Git 推荐哪个分支或者回退到另一个默认值。然后,你的 Git 将该分支名称转换为一个远程跟踪名称。它确保仅向他们的 Git 请求在该分支上存在于该 Git 存储库中的提交。
最终结果是你将得到一个远程跟踪名称和一些子集的所有提交。最后的 git checkout
步骤会创建一个本地分支名称:与选择提交子集时 Git 使用的名称相同。
如何影响 --depth
除了自动打开
--single-branch
之外(但请注意,您可以使用
--no-single-branch
关闭它),
--depth
的作用是创建一个
浅层克隆。要完全理解浅层克隆,我们必须涉及图形理论。(在这里我们不会深入探讨这个问题。)
在Git中,每个分支名称标识
恰好一个提交。但在Git中,如果我们忽略
“分支”究竟是什么意思?的问题(尽管我们不应该忽略它,但是在这里我们会忽略它),通常会有一堆提交。那这是如何工作的呢?
答案是,在Git中,每个
提交都包含某个
早期提交的哈希ID。在通常的简单情况下,我们最终得到了一长串提交,每个提交都指向
前面的一个提交。这条链中的
最后一个提交是分支的
末端或
末尾提交。
让我们画一个简单的链,其中使用一个大写字母代表每个提交的真实哈希 ID。哈希
H
将是链中的
最后一个提交,并且我们将称其为分支
br1
:
... <-F <-G <-H <-- br1
名字br1
保存了最新提交H
的哈希ID。这就是Git如何从对象数据库中获取它的方式(请记住,这是一个简单的键值存储:哈希ID是键)。但在提交H
的主体内部,Git存储了更早提交G
的哈希ID。因此,我们可以从H
获取G
的ID,并让Git在键值存储中查找提交G
。同时,提交G
有F
的ID,因此我们可以从G
向后遍历到F
。
这就是Git的工作方式:向后。像分支或标签或远程跟踪名这样的名称,保存了一个哈希ID。这是我们想要的提交,然后,如果我们想要所有提交,Git会从该提交向后遍历到上一个提交,然后继续向后遍历。名称让我们开始;提交本身提供了路径的其余部分。
我们走过的路径以及我们收集到的所有提交,都是该分支上可达的提交。当两个分支分叉时,它们都有一些共同的序列:
I--J <-- br1
/
...--F--G--H <-- shared
\
K--L <-- br2
在这里,提交记录从
H
开始都存在于
所有三个分支上,每个
br*
分支的最后两个提交记录是各自分支独有的。
这种
可达性思想是Git的核心。这也是
--depth
的工作原理。如果我们使用
--depth 1
,我们告诉Git:
当你从其他Git获取提交记录时,只往前走一步。 如果我们在这里使用
--depth 1
,我们会得到:
i
g
j
如果我们使用
--depth 2
,我们告诉Git:
当你从另一个Git获取提交时,向前走两步。 这次我们得到:
I--J <-- br1
/
f--G--H <-- shared
\
K--L <-- br2
请注意,如果
br2
有更多的提交是独有的,我们就不会从
br2
回到
shared
。
这里的小写字母表示Git知道有一个父节点,但这些父节点被标记为“故意缺失”。更准确地说,浅嫁接(commit)的哈希ID保存在名为
shallow
的文件中,该文件位于
.git
目录中。Git知道不要尝试从对象库加载这些提交,并且它们缺失并不是一个错误。通常情况下,这将是一个错误。
由于它们是故意缺失的,
git log
不能且不会显示这些提交,因此看起来好像浅嫁接的提交根本没有父节点。这在某种程度上是具有误导性的,但也是你应该期望的。在大多数情况下,这足够无害。
这意味着我们使用的是分支名称。如果我们使用了标签名称,那么这些提交是可从标签达到的;如果我们使用了远程跟踪名称,那么这些提交是可从远程跟踪名称达到的。由于所有名称都使用相同的系统,因此每个名称都提供了一些方法来达到一些提交。
通过 git fetch
操作获取提交记录
当我们使用 git clone
命令时,实际上运行了一个包含六个命令的序列,其中五个是 Git 命令:
mkdir
,创建一个新的空目录/文件夹;
git init
,在第一步创建的目录中创建一个新的空仓库;
git remote add
,添加名称为 origin
或其他我们选择的名称、URL 和 fetch
配置,这是我们更改以避免单分支性的配置;
git config
,如果需要,在 git clone
命令中指定配置选项;
git fetch
,获取提交记录并为步骤 3 中选择的分支或分支创建远程跟踪名称;
git checkout
,创建一个本地分支名称,并填充 Git 的索引和我们的工作树。
第五步将选项
--depth
传递给
git fetch
。因此,如果我们必须调整我们的
origin
远程配置,以取消单一分支克隆,因为第3步仅添加了一个特定分支的远程(请参阅
git remote文档),我们必须运行一个新的
git fetch
。这个新的
git fetch
需要相同的
--depth
选项。
结论:
--depth
选项用于
git clone
命令,它同时开启了
--single-branch
选项,该选项限制了从其他Git存储库获取的名称和提交,以及将
--depth
传递到提取步骤,该步骤限制了从其他Git存储库获得的提交图的深度。在克隆时使用
--no-single-branch
可以抑制名称限制,同时保持深度限制。如果您需要撤消名称限制,或者如果您使用
git remote
更新受限分支名称集合,则必须再次运行
git fetch
。如果您希望
git fetch
具有深度限制,则必须再次传递
--depth
。
请注意,
git fetch
确实尊重现有的浅嫁接点,因此在某些情况下,省略
--depth
是相当无害的。例如,如果您有一个单分支克隆的存储库,它看起来像这样:
...--V--W--X <-- main
\
Y--Z <-- topic
您的单分支克隆深度为1,位于主分支上,因此提交
W
被标记为浅植接点:
w--X <-- main
然后,如果不使用
--depth
参数添加
topic
,就会得到以下结果:
w--X <-- main
\
Y--Z <-- topic
那就是说,这一次
main
没有更深入了。但如果图表如下:
...--V--W--X <-- main
\
Y--Z <-- topic
如果您添加了
topic
并且没有使用新的
--depth
选项获取,那么您将会得到:
...--V w--X <-- main
\
Y--Z <-- topic
在你的克隆中,这意味着你需要获取提交
V
和更早的所有内容。请注意,提交
W
仍然被标记为缺失:由于它丢失了,
你的 Git 无法看到
w
会连接回
V
,你自己的 Git 将显示如下:
X <
..
"这并不是技术上的错误,只是有误导性。"
--depth
意味着--single-branch
,除非你还加上--no-single-branch
。 - torek--depth 1
意味着--single-branch
,所以你只得到一个分支的一个提交。在克隆时使用--no-single-branch
选项可以获取每个分支的一个提交(从而获得正确数量的远程跟踪名称),或者使用接受的答案更新现有的克隆,然后运行git fetch --depth 1
来更新事物,以便你对于每个远程跟踪名称都有一个提交。 - torek--depth
意味着--single-branch
因此需要--no-single-branch
来打败它。) - torekgit fetch --unshallow
,请参见 https://dev59.com/6mw15IYBdhLWcg3wJ4UR#6802238。 - Marcin Kłopotek