HEAD 只是一个参考,很像master 或(如果存在)branch,但有两个额外的特殊属性:
-
HEAD 通常是一个符号 引用(通常,没有其他引用是符号的,尽管您可以使用git symbolic-ref 进行任何您喜欢的符号引用)。符号引用只是一个包含另一个名称的名称,而不是哈希 ID。当读取或写入符号引用时,Git 通常会说“哦,好吧,这个是 symbolic,所以我现在就去读取或写入另一个。”
显然这会导致无限循环:如果引用a 说“看看b”而b 说“看看a”,你可以永远来回追逐。但只要你不这样做,或者让HEAD 成为only 符号引用,你就可以了,因为你不能让HEAD 指向HEAD .此外,符号引用也不能很好地工作:如果您将分支glorp 指向master,然后要求删除glorp,Git 会删除master!我们稍后会看到这实际上是一件好事。
-
文字字符串HEAD 内置于许多 Git 命令中,文件本身非常重要——在很多地方都使用过——它实际上是一个目录本身是否是 Git 存储库的测试。这意味着如果某些东西(例如特别不合时宜的崩溃)清除了您的 HEAD 文件,Git 将不再相信您的 .git 目录是一个存储库! (没什么大不了的,通常:只要把文件放回去,一切都会好起来的。)
每当您进行 new 提交时,Git 使用的底层流程是:1
-
从HEAD 读取提交 ID。这是当前提交:如果您处于“分离 HEAD”模式,原始提交 ID 位于 HEAD,这就是 Git 得到的。如果你在一个分支上,HEAD 包含分支的名称,Git 会跟随分支名称的间接性并读取它,给出该分支的最尖端提交。无论哪种方式,这都是当前的提交。
-
写出提交所需的所有树 (git write-tree),并写入新提交本身 (git commit-tree),其父 ID 设置为在步骤 1 中获得的 ID(加上任何额外的父级,如果这是一个合并提交),它的树设置为在步骤 2 中获得的 ID,它的提交消息设置为任何合适的。
-
将从git commit-tree 获得的新提交的ID 写入HEAD。如果HEAD 是象征性的——即,你在一个分支上——这将改为分支名称。现在分支名称指向分支的新的最尖端提交!
但请注意,在第 3 步中,如果您处于“分离 HEAD”模式,Git 仍然会将新 ID 写入 HEAD。结果是HEAD 指向新分支的尖端。换句话说,“分离 HEAD”模式仅仅意味着 HEAD 包含 anonymous 分支的尖端的 ID。添加新提交的工作方式与往常完全相同,更新当前分支。只是当前分支只有HEAD这个名字。 (这个是一个名字,它不是一个分支的名字。具体来说,所有分支的名字都以refs/heads/开头。因为HEAD没有,所以它不是branch 名称,它只是一个参考。如果名称以 refs/remotes/ 开头,它是一个远程跟踪分支名称,如果它以 refs/tags/ 开头,它是一个标签,但 HEAD 没有根本不是从任何东西开始的,所以它只是一个参考。)
您的反对意见也可以换一种说法:
但这意味着许多分支都可以指向一个提交 ID!
没错。这完全正常,每次创建新分支时都会发生这种情况:2
...--o--o--o <-- HEAD, master
\
o <-- branch
如果HEAD 被“分离”并且我们进行新的提交:
o <-- HEAD
/
...--o--o--o <-- master
\
o <-- branch
如果HEAD没有分离——如果相反,它指向master——并且我们在进行新提交之前执行git checkout -b newbr,那么我们从这个开始(并且这次我画HEAD -> newbr来表示HEAD是象征性的,指向newbr):
...--o--o--o <-- HEAD -> newbr, master
\
o <-- branch
在提交之后我们有:
o <-- HEAD -> newbr
/
...--o--o--o <-- master
\
o <-- branch
请注意,在“之前”图片中,我们有 三个 名称用于当前提交:HEAD、newbr 和 master 都指向它(尽管 HEAD 有先通过newbr)。
1也就是说,这是一个普通git commit的过程。如果你使用git commit --amend,这个过程会稍微修改一下:Git 不是从HEAD 读取 ID,而是查找当前提交的父级,并在步骤 3 中使用这些 ID。这意味着新的提交,完成后,与当前提交具有相同的父级。通过HEAD 将新提交的 ID 写入分支,这似乎更改了一个提交。但实际上并没有:它只是将“旧当前”提交推到一边。
如果您通过一个示例使用两个或多个分支名称指向 same 提交,您将确切了解如何以及为什么在 已发布 上使用 git commit --amend提交——你已经推送到另一个存储库的提交,并且其他人现在已经按名称提交——可能会出现问题。 (练习/提示:更新HEAD时,步骤3中更改了多少分支名称引用?)
2除非,也就是说,你使用git checkout --orphan。这样做是将HEAD 置于新的空存储库中的相同特殊状态:HEAD 现在包含实际上还不存在的分支的名称。也就是说,它是对不存在的分支的符号引用。上面的三步提交序列知道如何处理从 HEAD 读取 ID 失败的问题:它使用 no 父级进行新提交,然后将新 ID 写入 HEAD,这具有实际创建分支的副作用。
这解决了新的空存储库的引导问题:分支名称只能指向实际提交;但是master,在一个新的空存储库中,根本无法指向任何提交,因为根本没有。因此,在您进行第一次提交之前,一个新的存储库实际上并没有 master 分支,即使设置了 HEAD 以便您在master 分支上。 p>