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
,但有两个额外的特殊属性:
HEAD
通常是一个 符号引用 git symbolic-ref
)。符号引用只是一个包含另一个名称的名称,而不是哈希 ID。当读取或写入符号引用时,Git 通常会说“哦,好吧,这个是 symbolic,所以我现在就去读取或写入另一个。 “
显然这会导致无限循环:如果引用 a
说“看 b
”,而 b
说“看 a
”,您可以追逐永远。但是只要你不这样做,或者让 HEAD
成为 唯一的 符号引用,你就会没事,因为你不能让 HEAD
指向 HEAD
。此外,符号引用也不是很好:如果你让分支 glorp
指向 master
然后要求删除 glorp
,Git 会删除 master
!我们稍后会看到这实际上是一件好事。
许多Git命令内置了文字字符串HEAD
,文件本身非常重要——用在很多地方——它实际上是一个测试目录本身是否是 Git 存储库。这意味着如果某些事情(例如特别不合时宜的崩溃)清除了您的 HEAD
文件,Git 将不再相信您的 .git
目录是一个存储库! (没什么大不了的,通常:只要把文件放回去,一切都会好起来的。)
每当您进行 new 提交时,Git 使用的基础过程是:1
从 HEAD
读取提交 ID。这是 当前提交 :如果您处于“分离 HEAD”模式,原始提交 ID 在 HEAD
中,这就是 Git 得到的。如果你在一个分支上,那么 HEAD
包含分支的名称,Git 跟随分支名称的间接寻址并读取它,给出该分支的最尖端提交。无论哪种方式,这都是当前提交。
写出提交 (git write-tree
), and write the new commit itself (git commit-tree
) 所需的所有树,并将其父 ID 设置为在步骤 1 中获得的 ID(加上任何额外的父代,如果这是合并提交),其树设置为在步骤 2 中获得的 ID,其提交消息设置为适当的任何内容。
将从 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
请注意,在“之前”的图片中,当前提交有 三个 个名称:HEAD
、newbr
和 master
都指向它(尽管 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
分支。
根据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
,但有两个额外的特殊属性:
HEAD
通常是一个 符号引用git symbolic-ref
)。符号引用只是一个包含另一个名称的名称,而不是哈希 ID。当读取或写入符号引用时,Git 通常会说“哦,好吧,这个是 symbolic,所以我现在就去读取或写入另一个。 “显然这会导致无限循环:如果引用
a
说“看b
”,而b
说“看a
”,您可以追逐永远。但是只要你不这样做,或者让HEAD
成为 唯一的 符号引用,你就会没事,因为你不能让HEAD
指向HEAD
。此外,符号引用也不是很好:如果你让分支glorp
指向master
然后要求删除glorp
,Git 会删除master
!我们稍后会看到这实际上是一件好事。许多Git命令内置了文字字符串
HEAD
,文件本身非常重要——用在很多地方——它实际上是一个测试目录本身是否是 Git 存储库。这意味着如果某些事情(例如特别不合时宜的崩溃)清除了您的HEAD
文件,Git 将不再相信您的.git
目录是一个存储库! (没什么大不了的,通常:只要把文件放回去,一切都会好起来的。)
每当您进行 new 提交时,Git 使用的基础过程是:1
从
HEAD
读取提交 ID。这是 当前提交 :如果您处于“分离 HEAD”模式,原始提交 ID 在HEAD
中,这就是 Git 得到的。如果你在一个分支上,那么HEAD
包含分支的名称,Git 跟随分支名称的间接寻址并读取它,给出该分支的最尖端提交。无论哪种方式,这都是当前提交。写出提交 (
git write-tree
), and write the new commit itself (git commit-tree
) 所需的所有树,并将其父 ID 设置为在步骤 1 中获得的 ID(加上任何额外的父代,如果这是合并提交),其树设置为在步骤 2 中获得的 ID,其提交消息设置为适当的任何内容。将从
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
请注意,在“之前”的图片中,当前提交有 三个 个名称:HEAD
、newbr
和 master
都指向它(尽管 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
分支。