Git HEAD 指的是分支与提交

Git HEAD referring to branch vs to commit

根据Pro Git Book

In Git, HEAD is a pointer to the local branch you’re currently on.

这与Git for Computer Scientists一致:

The HEAD ref is special in that it actually points to another ref. It is a pointer to the currently active branch.

但是 turns out that:

HEAD is not the latest revision, it's the current revision. Usually, it's the latest revision of the current branch, but it doesn't have to be.

For example:

If you check out something older (such as a tag like git checkout v1.1) then your HEAD changes to the commit of that tag. It may not be the latest commit.

因此 HEAD 可以将 指向一个分支 或者 一个提交。当 HEAD 引用分支 X 与 HEAD 引用分支 X 的实际头部提交时,git 命令的行为是否有任何不同? (在类似 C 的表示法中,我说的是 **HEAD 指的是某个提交与 *HEAD 指的是同一个提交的情况。)

好的定义是 HEAD 总是指向签出的内容。

如果你在一个分支上工作,它就是指向这个分支。因此,当您签出另一个分支时,HEAD 现在指向这个新分支。

但是你也可以直接check out一个commit,只是为了验证过去的情况,然后HEAD指向这个commit。您处于我们所说的 "detached head" 状态。阅读更多相关信息。但是,是的,这种状态不是为了工作和创建新的提交。 git 命令的行为在这种情况下并没有真正的不同,但结果可能是:如果你检出一个分支,你将失去在这个状态下创建的所有提交的踪迹,并且必须从重新记录。

您可以自己看到,因为 HEAD 由文件 .git\HEAD 实现。只需打开文件并查看其内容...

HEAD 只是一个引用,很像 master 或(如果存在的话)branch,但有两个额外的特殊属性:

  1. HEAD 通常是一个 符号引用 git symbolic-ref)。符号引用只是一个包含另一个名称的名称,而不是哈希 ID。当读取或写入符号引用时,Git 通常会说“哦,好吧,这个是 symbolic,所以我现在就去读取或写入另一个。 “

    显然这会导致无限循环:如果引用 a 说“看 b”,而 b 说“看 a”,您可以追逐永远。但是只要你不这样做,或者让 HEAD 成为 唯一的 符号引用,你就会没事,因为你不能让 HEAD 指向 HEAD。此外,符号引用也不是很好:如果你让分支 glorp 指向 master 然后要求删除 glorp,Git 会删除 master !我们稍后会看到这实际上是一件好事。

  2. 许多Git命令内置了文字字符串HEAD,文件本身非常重要——用在很多地方——它实际上是一个测试目录本身是否是 Git 存储库。这意味着如果某些事情(例如特别不合时宜的崩溃)清除了您的 HEAD 文件,Git 将不再相信您的 .git 目录是一个存储库! (没什么大不了的,通常:只要把文件放回去,一切都会好起来的。)

每当您进行 new 提交时,Git 使用的基础过程是:1

  1. HEAD 读取提交 ID。这是 当前提交 :如果您处于“分离 HEAD”模式,原始提交 ID 在 HEAD 中,这就是 Git 得到的。如果你在一个分支上,那么 HEAD 包含分支的名称,Git 跟随分支名称的间接寻址并读取它,给出该分支的最尖端提交。无论哪种方式,这都是当前提交。

  2. 写出提交 (git write-tree), and write the new commit itself (git commit-tree) 所需的所有树,并将其父 ID 设置为在步骤 1 中获得的 ID(加上任何额外的父代,如果这是合并提交),其树设置为在步骤 2 中获得的 ID,其提交消息设置为适当的任何内容。

  3. 将从 git commit-tree 获得的新提交 ID 写入 HEAD。如果 HEAD 是象征性的——也就是说,你在一个分支上——这会写入分支名称。现在分支名称指向该分支的新的 tip-most 提交!

    但请注意,在第 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

请注意,在“之前”的图片中,当前提交有 三个 个名称:HEADnewbrmaster 都指向它(尽管 HEAD 必须先通过 newbr)。


1也就是正常的git commit的流程。如果你使用 git commit --amend,这个过程只是稍微修改一下:不是从 HEAD 读取 ID,Git 查找当前提交的父级,并在步骤中使用这些 ID 3. 这意味着新提交一旦完成,与当前提交具有​​相同的父级。通过 HEAD 将新提交的 ID 写入分支, 看起来 已更改提交。但它并没有,真的:它只是把“旧的当前”提交推到了一边。

如果您通过两个或多个分支名称指向 相同 提交的示例,您将确切地看到如何以及为什么在 git commit --amend 上使用 published 提交——你已经推送到另一个存储库的提交,而其他人现在已经知道了——可能会有问题。 (Exercise/hint: 在更新 HEAD 时,第 3 步中更改了多少个分支名称引用?)

2除非,就是你用git checkout --orphan。这样做是将 HEAD 置于与在新的空存储库中相同的特殊状态:HEAD 现在包含 实际上还不存在的分支的名称。也就是说,它是对不存在的分支的符号引用。上面的三步提交序列知道如何处理无法从 HEAD 读取 ID:它使用 no parent 进行新提交,然后将新 ID 写入HEAD,它具有实际创建分支的副作用。

这解决了新的空存储库的 bootstrap 问题:分支名称只能指向实际提交;但是 master,在一个新的空存储库中,根本无法指向任何提交,因为根本没有任何提交。因此,在您进行第一次提交之前,新存储库实际上没有 master 分支,即使 HEAD 已设置为 master 分支。