首页 > 解决方案 > Git:拉动后的第二个(空)FETCH_HEAD文件

问题描述

我对 Git 很陌生,我正在学习分支以及如何拉/推。这是我目前的工作流程:

在我的笔记本电脑上:

需要明确的是,我所做的事情的时间顺序是:

在这个阶段,一切似乎都很好。然而,我开始玩弄分支的概念。按年代顺序:

这种拉动似乎有效(git log 显示这两个文件夹具有相同的提交历史 - 所有提交都在那里)但我注意到我的文件夹 2 中有一个 FETCH_HEAD 文件。这个文件是空的。当我之前从文件夹 2 中推拉时,这个文件从未存在过。

我在这里错过了什么吗?我不太清楚我是否做错了什么,或者这是否与我在同一台笔记本电脑上使用 2 个文件夹的事实有关(即我的合作者使用相同的 Git 密码等)。我的意思是要查看一些 FETCH_HEAD 文件吗?

据我了解,如果您在笔记本电脑上创建本地分支,则可以推送它,而您的合作者可以使用 git fetch 拉取它。对吗?我只是在这里感到困惑,因为这似乎只是使用主分支的例行推拉。

抱歉,如果我的问题非常基本。如果有帮助,这是我拉入文件夹 2 时 Git 的输出:

# Output:
# remote: Counting objects: 11, done.
# remote: Compressing objects: 100% (4/4), done.
# remote: Total 11 (delta 6), reused 11 (delta 6), pack-reused 0
# Unpacking objects: 100% (11/11), done.
# From github.username/VC-exercise
#  * branch            master     -> FETCH_HEAD
#    4fadbae..d99886d  master     -> origin/master
# Updating 4fadbae..d99886d
# Fast-forward
#  README.md        | 2 ++
#  data/adapters.fa | 0
#  2 files changed, 2 insertions(+)
#  create mode 100644 data/adapters.fa

谢谢。

更新

我不够精确。当我在我的问题中谈论 FETCH_HEAD 时,我不是在谈论 .git/FETCH_HEAD。该文件存在于我的文件夹 2 中,但最重要的是,我的文件夹 2 中直接有一个名为 FETCH_HEAD 的空文件,位于我的所有脚本等旁边。这就是困扰。这当然不正常。

此外,当我在文件夹 1 中键入 git branch --all 时,我得到了这个,这对我来说看起来很正常:

*master
branch-I-made
remotes/origin/master

但是,当我在文件夹 2 中键入 git branch --all 时,我得到:

*master
remotes/origin/master -> origin/master
remotes/origin/master

“remotes/origin/master -> origin/master”是什么意思,这正常吗?

标签: gitpushbranchpull

解决方案


我不确定这个特定FETCH_HEAD文件来自哪里。正如我在评论和链接中指出的那样,该.git/FETCH_HEAD文件是如何git fetch留下轨道git pull以运行其第二个 Git 命令(通常git merge但您可以选择git rebase)。但是该文件隐藏在.git- 它不应该出现在您的工作树中。

(恐怕我没时间做这件事,所以很长。)

存储库主要是提交的集合(或数据库)

但是,如果我们把它放在一边,让我们看看 Git 存储库中有什么。请记住,每个存储库都是(至少在理论上1 )所有内容的完整、独立副本。好吧,几乎所有内容——我们稍后会看看没有共享的内容——但是每个存储库都有项目所有历史记录的完整副本。为了正确定义这一点,我们还要注意,在 Git 中,历史就是提交,而提交就是历史。 提交是 Git 保留的内容:存储库由提交组成。

每个提交本身就是其所有文件的逻辑完整快照。也就是说,一旦我们以某种方式命名了一个提交,Git 就可以提取我们在运行时保存的每个文件的确切版本git commit。每个提交还有一些与之关联的元数据:例如,提交作者的姓名和电子邮件地址。几乎所有的提交(实际上通常是除了一个提交之外的所有提交)都将其父提交的名称作为元数据的一部分存储。这给我们带来了一个关键点。


1当您进行本地克隆时(与重复或类似操作相反https://ssh://Git 将使用各种技巧来共享底层存储库存储。通常它甚至会以一种不可见的方式执行此操作:如果您删除两个克隆中的一个,另一个保持不变。对于像 GitHub 这样的高级用户或 Web 提供商,Git 允许更高级的共享;在这种情况下,你需要知道你在做什么,因为共享这么多底层存储意味着有一些更重要的存储库相对于其它的。


提交的名称,或者实际上是任何 Git 对象,是一个哈希 ID

运行时,git log您将看到提交哈希 ID:

$ git log
commit e3331758f12da22f4103eec7efe1b5304a9be5e9 (HEAD -> master)
Author: Junio C Hamano ...

对于提交对象,此哈希 ID 保证对于该特定提交是唯一的。实际上,该哈希 ID 是提交的真实名称。它是 Git 用来在 Git 对象数据库中查找提交数据的密钥。这个数据库本质上只是一个键值存储,键是哈希 ID,值是对象内容。

有四种类型的 Git 对象:我们刚刚看到的提交,以及blob带注释的标记对象。其中两个不一定是唯一的,但所有四个都由它们的哈希 ID 标识。哈希 ID 看起来是随机的,但实际上是原始对象内容的加密校验和,包括对象的类型字段。由于每个提交都是唯一的,Git 保证每个哈希 ID 也是唯一的。2 Git 还可以通过将任何检索到的对象的计算校验和与用于检索它的哈希 ID 键进行比较来验证数据完整性:这些必须匹配,否则某些数据已损坏。

因为密钥内容的校验和,所以任何 Git 对象一旦存储在数据库中就不可能在物理上进行更改。改变任何东西,哪怕只是一点点,都会改变校验和,从而产生一个新的不同的键值对。这意味着存储在存储库中的每个提交和文件都是完全只读的:其中的任何内容都无法更改。


2如果您对散列有很多了解,就会知道由于鸽洞原理,这种保证在数学上是不可能的。Git 在这里真正做的是确保冲突是非常不可能的,然后拒绝让你创建一个有哈希冲突的对象。另请参阅新发现的 SHA-1 冲突如何影响 Git?


提交的内容大多被重定向到别处

提交的对象内容实际上非常简单。以下是 的内容e3331758f12da22f4103eec7efe1b5304a9be5e9,例如:

$ git cat-file -p e3331758f12da22f4103eec7efe1b5304a9be5e9 | sed 's/@/ /'
tree 313f70847d0dab2718d19201b5be3af52061c4da
parent 085d2abf57be3e424cad0b7dc8c27fe41921258e
author Junio C Hamano <gitster pobox.com> 1530215747 -0700
committer Junio C Hamano <gitster pobox.com> 1530215747 -0700

Second batch for 2.19 cycle

Signed-off-by: Junio C Hamano <gitster pobox.com>

再一次,我们看到了提交的元数据——作者姓名等等——加上行,它告诉我们在这次提交之前提交的哈希 ID 。快照本身隐藏在子对象中,通过线让 Git 找到提交的关联树对象。

树的内容要复杂得多,但我们不需要深入探讨任何细节。知道这就是 Git 如何存储与此提交相关的快照就足够了。树命名所有文件,使用适当的递归,并让 Git 能够通过blob对象检索每个文件的快照。这意味着,给定提交哈希 ID顶级树哈希 ID,Git 可以提取完整的快照。

提交本身只是为我们提供了所有元数据:谁提交了,何时提交;他们为它写的日志消息;和父哈希 ID,如果这是一个普通的单父提交。但是,每个提交都记录其父项这一事实为我们提供了其他重要的东西。

提交表单链

如果我们使用单个大写字母而不是明显随机的哈希 ID 来表示每个提交,我们可以非常简单地绘制普通提交。例如,在一个小型的 3-commit 存储库中,我们会这样:

A  <--B  <--C

CommitC是我们最后一次提交。它将提交的 ID 存储B为其父级。CommitB存储了 commit 的 ID A,并且由于 commitA是我们所做的第一个提交,它根本没有父级。(Git 称之为提交。)

请注意,这些链总是向后指向。Git 需要以某种方式知道提交的实际哈希 IDC可能是什么。这是分支名称进入图片的地方。

分支名称实际上是名称-值对,充当指向最后一次提交的指针

要添加master到我们的图片中,我们只需这样做:

A--B--C   <-- master

该名称master包含 commit 的实际哈希 ID C。从这里,Git 可以找到B,这允许 Git 找到A. A没有父母,所以行动停止:我们有我们的三个快照,我们都很好。

要添加的提交,我们首先让 Git 在C某处提取提交。我们使用它来构建一个新的 commit D,它将C的哈希 ID 存储为其父级;然后我们让 Git 将D哈希 ID 写入名称master

A--B--C--D   <-- master

如果我们在 make 之前添加一个新的分支名称D,我们的图片基本上是一样的:

A--B--C   <-- master, newbr

但是现在我们需要一种方法来记住哪个分支是当前分支,所以我们将单词附加HEAD到其中一个:

A--B--C   <-- master, newbr (HEAD)

现在,如果我们进行新的 commit D,一切都会像以前一样进行,但Git 更新的名称HEAD是附加的名称,给我们:

A--B--C   <-- master
       \
        D   <-- newbr (HEAD)

因此,存储库包含两个数据库,它们fetch可以push一起使用

最重要的数据库是包含 Git 对象的数据库,尤其是提交。提交是 Git 的命脉,它的存在理由。但是为了找到提交,Git 需要第二个键值数据库,其中键是名称——例如分支和标签名称——而值是哈希 ID。

这两个数据库是什么处理的git fetchgit push这两个操作都将两个 Git 存储库相互连接。Fetch 和 push 非常相似:都发送或接收提交(以及其他 Git 对象——树和 blob——根据需要使提交完成),然后都更新一组名称。第一个明显的区别是转移的方向: git fetch从另一个 Git 提交到我们的,而git push从我们的 Git 提交到另一个 Git。

但这里还有一点不对称。在我们的Git 中,我们既有分支名称,例如master,也有远程跟踪名称,例如origin/master. 这些从何而来?

分支名称来自我们创建它们。我们告诉我们的 Git:创建 name newbr,指向 commitC并且它这样做了。然后我们告诉我们的 Git 在当前 ( newbr) 分支上进行新的提交,它就这样做了。当我们告诉我们的 Git 创建它时,它本身就被创建了。但是——我们什么master时候创造了那个?事实证明,这有点棘手。让我们暂时搁置一下。

远程跟踪名称,例如origin/master,是我们的 Git在通过 name 与另一个 Git 对话时origin我们创建的东西。当我们第一次运行时,这个动作——克隆一些现有的存​​储库——告诉我们的 Git,一旦它创建了一个新的空存储库(没有提交也没有分支),它应该通过我们的名称和 URL调用另一个 Git给予,并从该 Git 中获取它的所有提交和分支等等。然后我们的 Git重命名它们的所有分支:它们变成了我们的. 如果他们有一个,他们就变成了我们的。git clone urloriginmasterorigin/masternewbrnewbrorigin/newbr

简而言之,这些远程跟踪名称是我们的 Git 记住他们的Git 所说分支的方式。具体来说,他们持有与Git 上的分支名称一起使用的哈希 ID,但重命名为我们的名称origin/*。这意味着它们的分支名称不会影响我们的分支名称——至少目前还没有。

Push 和 fetch 不是对称的,因为在它们的分支名称push上写入

但是,当我们运行git push origin newbrorgit push origin master时,我们让 Git 向他们的 Git 发送我们拥有但他们没有的任何提交,然后我们让 Git 要求他们的 Git 设置他们的 master. 他们的存储库,无论它在哪里,都没有传入推送的重命名方案。我们只是要求他们直接设置他们的分支,基于我们的任何提交哈希 IDmasternewbr名称(在我们给他们这些提交之后,当然也需要任何更早的提交)。

当我们从他们那里获取数据时,我们会使用我们的远程跟踪名称记住他们的分支。这样我们就不会打扰我们自己的分支名称。但是当我们向他们推送时,我们只是要求他们设置他们的分支。因此,虽然 fetch 和 push 与对称传输一样接近,但它们并不相同。

请注意,他们可以接受我们的请求,也可以拒绝它。如果他们确实接受了我们的推送,我们的 Git 将通过创建或更新我们自己的or来记住他们的masteror已更改。newbrorigin/masterorigin/newbr

还有一些未传输的数据项

每当我们对任何分支名称、远程跟踪名称、标签名称或我们的名称到哈希 ID参考数据库中的任何名称进行任何更改时,我们的 Git 都会记录这些参考更改的日志。这些旧名称-值对的引用日志或引用日志实际上是我们的 Git 维护的另一个数据库(或数据库集合)。一段时间后,值会从日志的末尾“下降”,这样 reflog 就不会无限制地增长:默认情况下,某些 reflog 值的限制为 90 天,而其他的限制为 30 天。3

还有一堆特别命名的引用,ORIG_HEAD、MERGE_HEAD、CHERRY_PICK_HEAD等4个,加上特殊文件,都存放在repository目录FETCH_HEAD的顶层。.git这些都不会通过 fetch 和 push 传输。然而,我们已经注意到了魔法名称的特殊作用HEAD(全部大写——还有另一个文件在 中.git),因为我们的 HEAD 被“附加到”Git 认为是当前分支的任何一个分支。

这里发生的是,在克隆和获取期间,接收 Git 可以看到发送 Git 的HEAD设置。Git 使用它git clone来选择要交给哪个分支名称git checkout。接收 Git 可以假设或直接告诉5发送 Git 的HEAD名称是哪个分支,并创建一个符号远程跟踪名称origin/HEAD,指向正确的远程跟踪名称,例如origin/MASTER. 这就是您在git branch --all输出中看到的。


3它们之间的关键区别在于是否可以从相应引用的当前值访问存储在所讨论的 reflog 条目中的哈希 ID。这个可达性概念是另一个关键的 Git 概念。有关这方面的更多信息,请参阅Think Like (a) Git

4这些特殊名称是否算作参考是有争议的。除了HEAD,他们都没有 reflogs。Git 说引用是任何名称,其完全扩展形式以 开头refs/,但HEAD有一个 reflog 并且不以 开头refs/,那么HEAD引用是吗?Git 在这一点上有点矛盾:有些部分说是,有些部分说不。

5这取决于两个 Git 安装的年龄/版本。自 Git 版本 1.8.4.3 以来,已经存在适当的符号 HEAD 支持。


其他需要理解的关键项目:索引和工作树

以上所有内容都与提交和 Git 的对象数据库以及引用(分支和标签名称等)数据库有关。我们还注意到,存储在提交快照中的文件采用特定于 Git 的特殊对象格式,在这种格式中它们是只读的。在这种格式中,它们会被压缩(有时是高度压缩的)。

但是,为了在 Git 存储库上工作,您还需要 Git 为您创建的另外两个项目:

  • Git 有一个关键的数据结构,它以不同的方式调用indexstaging area,或者有时是cache。这个数据结构——主要是一个文件——.git/index间接地持有当前提交中每个文件的副本。这些文件采用相同的高度压缩 Git 对象格式。然而,至关重要的是,它们可以被新的(压缩的)文件覆盖。

  • 为了让您实际查看和处理您的文件,Git 必须将它们解压缩为您计算机的普通格式。它将这些文件放入您的工作树中,这是您工作的地方。工作树中的文件不在提交中,但是进入工作树的任何内容的初始版本都来自提交(通过索引/暂存区域)。

Running告诉 Git 将给定的提交文件提取到索引中(以便索引现在与提交匹配),然后进入工作树(以便您可以查看和/或更改文件)。这导致 Git 称为分离的 HEAD,其中特殊名称不再包含分支的名称。相反,包含原始提交哈希 ID。git checkout commit-idHEADHEAD

就目前而言,这种特殊的工作模式很好,但这意味着当您创建新提交时,新提交的哈希 ID 不会记录到分支名称中。出于这个原因,通过将名称写入文件,提取存储在分支名称下的提示提交来工作。git checkout nameHEAD

当您第一次从某个地方克隆存储库时,您根本没有分支名称。的最后一步git clone是运行,通常是在哪里(但正如我们所见,来自另一个 Git)。但你还没有git checkout namenamemastermaster

在这一点上,git checkout做一件特别的事情:它会查看你所有的远程跟踪名称,看看是否有一个origin/master. 如果确实有一个这样的名字——当然有;你的 Git 刚刚将它们复制master到你的——你的origin/masterGit 现在在你自己的存储库中创建了一个新的分支名称,指向你说他们master指向的同一个提交。origin/mastermaster

就是你拥有一个 的方式master:你的 Git创建它作为你的git clone.


推荐阅读