Git 无法在标签上创建分支

Git failed to create a branch on a tag

我有以下分支:

xxx@box:~/src$ git branch
  jira_6500
* main
xxx@box:~/src$ git rev-parse main
bfd271932228f8ce33b68b82ffee5ee3b2386a17
xxx@box:~/src$ git rev-parse jira_6500
bfd271932228f8ce33b68b82ffee5ee3b2386a17
xxx@box:~/src$

我尝试从标签 v2.6.0-rc3 创建一个新分支,如下所示:

xxx@box:~/src$ git rev-parse v2.6.0-rc3
ff8db8992102ca7ce76f55169d06173c888c9447

xxx@box:~/src$ git checkout -b test001 v2.6.0-rc3
Switched to a new branch 'test001'
xxx@box:~/src$ git branch
  jira_6500
  main
* test001

然后我检查新分支的 rev hash。我希望与标签 v2.6.0-rc3 相同。但事实并非如此。它与 jira_6500 分支相同。

xxx@box:~/src$ git rev-parse test001
bfd271932228f8ce33b68b82ffee5ee3b2386a17

我按照下面的线程做了同样的事情。我记得我以前做过。

rev hash 怎么会错?

How to create a new branch from a tag?

您的分支创建 正在按照您希望的方式工作。您所看到的原因与 Git 标签的内部结构有关,这有点奇怪。

Git 在它的小心脏里,都是关于 commits,它们由哈希 ID 编号,通常以十六进制表示:bfd271932228f8ce33b68b82ffee5ee3b2386a17,例如。

为了使提交 工作 ,Git 需要两个更多的内部支持对象,Git 调用 treesblob。这些也有散列 ID。您通常不会 看到 这些哈希 ID:它们不会“泄漏”太多。 (不过,Blob 哈希 ID 确实出现在 git diff 输出的 index: 行中,如果您查找它们,您 可以 找到树哈希:none 其中一些是隐藏的。它们只是 get all up in your face 不像提交哈希 ID 那样。)

Tags,在 Git 中,标记一个提交——但是你可以在这里选择:一个 lightweight 标签直接保存一个提交哈希 ID,所以如果你有提交bfd27...,您可以创建一个轻量级标签来存储该哈希 ID。但是,如果您想存储 更多信息,Git 有一个支持对象,称为 标签对象带注释的标签对象。我们 Git 创建这些对象之一,存储额外的数据——例如 PGP 签名或其他任何东西——并且该对象获得 它自己的 唯一哈希 ID,例如 ff8db8992102ca7ce76f55169d06173c888c9447.

标签对象本身与注释数据一起存储 commit 哈希 ID,bfd271932228f8ce33b68b82ffee5ee3b2386a17。由于这些散列 ID 每个都唯一标识相应的对象,Git 可以使用 tag ID ff8db... 找到 commit对象,通过读取标记对象并查找存储的提交哈希 ID。 (不可能走另一条路:提交 bfd27... 在我们创建任何指向它的标签之前就已经确定了,因此我们不能 add 那些标记 ID 稍后提交。因此,与 Git 一样,我们必须向后工作,从较新的对象到较旧的对象。)

使用git rev-parse v2.6.0-rc3,你得到注释标签对象的哈希ID。从这里 Git 可以找到提交。标签名称可以直接指向一个提交——同样,这使它成为一个轻量级标签——或者指向一个标签对象,使标签名称成为一个带注释的标签。 Git 两种方式都可以找到提交。

Branch 名称与标签名称不同,它们受到限制:它们可能只包含一些(现有的)commit 的哈希 ID。因此,在创建新分支名称时,如果您给 Git 带注释的标记对象的哈希 ID,或者解析为 带注释的标记对象的名称,则 Git 继续跟随带注释的标记对象到它的目标,这需要是一个提交。1

所以这正是您在这里看到的。创建分支名称跟随标记到标记的提交。其他分支名称也指向同一个提交——这是正常的。当您使用 git checkoutgit switch 获得这些分支名称之一并进行 new 提交时,Git 将创建新的照常提交,作为git commit的最后一步,会将新提交的哈希ID写入当前分支名称,导致分支前进。

使用 git checkout v2.6.0-rc3git switch --detach v2.6.0-rc3 检查标签会将 Git 置于 分离 HEAD 模式,其中 HEAD 包含提交的原始哈希 ID。在这种情况下,进行新提交会将新提交的哈希 ID 直接存储在特殊名称 HEAD 中,而不是存储在任何分支名称中。这意味着重新附加 HEAD——用分支名称而不是提交哈希 ID 覆盖 HEAD 存储槽——“丢失”了新的提交,这就是为什么你通常不这样做的原因不要在分离头模式下做新工作。2

这里还有最后一件事要提,那就是 git rev-parse 有很多句法技巧来处理这个问题。它们都包含在 the gitrevisions documentation 中,但在这里快速概述相关内容很有用:

  • git rev-parse v2.6.0-rc3 只是让你得到任何 v2.6.0-rc3 解析为的 ID:在这种情况下,refs/tags/v2.6.0-rc3 解析为带注释的标签。

  • git rev-parse v2.6.0-rc3^{commit} 找到与 v2.6.0-rc3 关联的 commit:也就是说,如果这是一个标签,它会剥离标签并要求结果是提交。

  • git rev-parse v2.6.0-rc3^{tree}找到与v2.6.0-rc3关联的:即如果这是一个标签,则剥离该标签;如果现在这是一个提交,它会找到存储在 in 中的顶级树;它要求最终结果是树的哈希ID。

  • git rev-parse v2.6.0-rc3^{}找到v2.6.0-rc3关联的hash ID,如果是tag,则剥离tag(然后成功停止并产生hash ID,不管找到的对象的类型)。

在这种情况下,git branch test001 v2.6.0-rc3git checkout -b test001 v2.6.0-rc3 在内部具有与使用 v2.6.0-rc3^{commit}git rev-parse 相同的效果。

这些语法技巧适用于大多数 Git 命令:在任何可能需要哈希 ID 的地方,您都可以使用名称,并且您提供的任何名称都经过相同的过程 git rev-parse 用于转换它变成一个哈希 ID。


1带注释的标签可以直接指向树或 blob 对象。如果这样做,则不能使用它们来创建新的分支名称。带注释的标签对象还可能包含另一个带注释的标签对象的哈希 ID 作为其目标哈希 ID;在这种情况下,Git 继续间接寻址,直到找到最终对象。这种重复的间接调用称为 peeling 标签,其概念取自剥洋葱的想法,一层一层地剥,直到你找出里面是什么。当然,就洋葱而言,当你剥掉所有的层时,除了气味什么都没有留下。

2这里的例外包括:

  • git rebase 故意使用这种模式来构建一个新的提交链。完成后,git rebase 强制要重新定位的分支名称指向最后一个新提交。

  • 如果你愿意,你可以在这种模式下工作一段时间,然后自己创建一个新的分支名称,或者强制一些现有的分支名称指向新的提交。

  • 如果你确实在分离头模式下工作错误,你可以使用git reflog找到你想要的提交并创建一个分支找到它的名称(或标签名称!)。

Git主要只是提供了这里的机制,你可以在此基础上构建任何你喜欢的东西。