【问题标题】:Git HEAD referring to branch vs to commitGit HEAD 指的是分支与提交
【发布时间】:2017-01-31 21:55:57
【问题描述】:

根据Pro Git Book

在 Git 中,HEAD 是指向您当前所在的本地分支的指针。

这与Git for Computer Scientists一致:

HEAD ref 的特殊之处在于它实际上指向另一个 ref。它是一个指向当前活动分支的指针。

但是turns out that:

HEAD 不是最新版本,而是当前版本。通常,它是当前分支的最新版本,但不一定是。

For example:

如果你签出一些旧的东西(例如像 git checkout v1.1 这样的标签),那么你的 HEAD 会更改为该标签的提交。它可能不是最新的提交。

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

【问题讨论】:

  • HEAD 是指向当前签出分支的最新提交的引用,仅此而已(除非处于分离的 HEAD 状态)。
  • @Tim Biegeleisen 我指的是独立的HEAD 案例。仅在我的情况下,分离的 HEAD 恰好指向当前分支的最新提交。
  • 为什么这个用例会引起您的兴趣/关注?
  • @Tim Biegeleisen 因为HEAD 有时是提交指针,有时是提交指针的指针,这让我感觉不一致和困惑,而且我更容易习惯如果我理解边缘情况的行为。

标签: git git-branch


【解决方案1】:

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

  1. HEAD 通常是一个符号 引用(通常,没有其他引用是符号的,尽管您可以使用git symbolic-ref 进行任何您喜欢的符号引用)。符号引用只是一个包含另一个名称的名称,而不是哈希 ID。当读取或写入符号引用时,Git 通常会说“哦,好吧,这个是 symbolic,所以我现在就去读取或写入另一个。”

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

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

每当您进行 new 提交时,Git 使用的底层流程是:1

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

  2. 写出提交所需的所有树 (git write-tree),并写入新提交本身 (git commit-tree),其父 ID 设置为在步骤 1 中获得的 ID(加上任何额外的父级,如果这是一个合并提交),它的树设置为在步骤 2 中获得的 ID,它的提交消息设置为任何合适的。

  3. 将从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 -&gt; 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,这个过程会稍微修改一下: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>

【讨论】:

    【解决方案2】:

    良好的定义是 HEAD 始终指向已签出的内容。

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

    但是你也可以直接签出一个提交,只是为了验证过去的情况,然后 HEAD 指向这个提交。你处于我们所说的“分离头”状态。阅读更多相关信息。但是,是的,这种状态不能正常工作并创建新的提交。在这种情况下 git 命令的行为并没有真正的不同,但结果可能是:如果您签出一个分支,您将丢失在此状态下创建的所有提交的跟踪,并且必须从 reflog 中恢复它们。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-04-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-05
      • 1970-01-01
      相关资源
      最近更新 更多