为什么同一个标签会有两个不同的Git提交记录?

4

我想要找到一个特定标签所对应的 commit SHA。当我执行 show-ref 命令时,会得到以下输出:

$ git show-ref my_tag
6a390ca7bca7b52b2009069138873fdbc7922c1d refs/tags/my_tag

当我执行rev-list命令时,会得到以下输出:
$ git rev-list -n 1 my_tag
b6dcf8fa20296d146e9501ab9d25784879adeac8

提交的 SHA 不同,但我不明白为什么。看起来由 rev-list 生成的 b6dcf8 是正确的。如果我尝试使用 git checkout 6a390c 检出第一个提交,然后查看日志,我实际上并没有在 6a390c 上;显示的是 b6dcf8。有人能解释一下可能存在的原因吗?为什么当我尝试检出 6a390ca 时会被重定向到 b6dcf8。
更新: 我还注意到当我执行 git show my_tag 时,输出看起来像这样:
tag my_tag
Tagger: Me <me@me.com>
Date:   Mon Apr 4 14:43:46 2016 -0400

Tagging Release my_tag

tag my_tag_Build_1
Tagger: Me <me@me.com>
Date:   Thu Mar 31 10:46:18 2016 -0400

Tagging my_tag_Build_1

commit b6dcf8fa20296d146e9501ab9d25784879adeac8
Author: Me <me@me.com>
Date:   Wed Mar 30 18:12:10 2016 -0400

Remove secret_key_base values from secrets.yml

它正在选择两个标签my_tagmy_tag_Build_1。然而,如果我运行git tag,标签列表只有

my_tag

如果我运行 git show my_tag_Build_1,我会得到:

fatal: ambiguous argument 'my_tag_Build_1': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:

看起来git有些混乱了。也许my_tag_Build_1标签曾经存在过,但现在似乎不存在了。


1
我们无法看到您的存储库,但可以假定my_tag是一个带注释的标签,而6a390c是标签对象。 - hobbs
你是不是想说 git rev-list -n 1 my_tag - Schwern
是的,谢谢您发现了这个问题。我已经进行了更新。 - CodeSmith
3个回答

12

虽然Marcelo Ávila de Oliveira's answer已经正确,但我想添加另一个答案,因为我想画出图形部分。 :-)

通常我喜欢绘制这样的提交图,至少对于StackOverflow是这样:

...--A--B--C     <-- foobranch
         \
          D--E   <-- barbranch

这里有两个分支上的两个提示(最右边)提交,C和E,每个都有一个指向它们的分支名称。也就是说,refs/heads/foobranch包含提交C的ID,而refs/heads/barbranch包含提交E的ID。
轻量级标签
轻量级标签的工作方式与分支名称完全相同。如果我们添加标签bartag以指向提交E,则会得到:
...--A--B--C     <-- foobranch
         \
          D--E   <-- barbranch, tag: bartag

refs/heads/bartag(除非已经“打包”并存储在文件.git/packed-refs中,否则它是.git中的实际文件)还存储提交E的ID。轻量级标签和分支之间有三个区别:

  1. 轻量级标签的全名以refs/tags/开头,而不是refs/heads/
  2. 轻量级标签不应更改以指向另一个提交(Git只半强制执行此规则,在1.8之前的版本中执行效果较差,但分支名称通常会更改以指向不同的提交,而标签则不会)。
  3. Git强制执行分支名称仅能指向提交对象的规则。通常标签名称也只指向提交对象,但可能会指向树或blob。还有一种对象类型——注释标签对象——但这使得标签不同!保留这个想法,让我们完成这个部分。
一个轻量级标签就是一个引用,其全名为refs/tags/...。这个外部引用存在于某个地方,通常是像.git/refs/tags/bartag这样的单独文件,并且它指向存储在仓库中的Git对象(.git/objects/...,可能被打包成一个包文件)。当它指向一个提交时,这通常是正常情况,它会将我们带入提交DAG:标签定位到提交,这可以让我们获得工作树,并且还可以通过跟随“parent”ID来探索之前(祖先)的提交,从提交E返回到D

注释标签

注释标签使用几乎相同的图片,除了现在,轻量级标签bartag不再直接指向提交,而是现在Git将一个注释标签对象存储到仓库中。这个注释标签对象有自己的数据(日期、标记者、消息、可选数字签名和任何其他你喜欢的内容),并且还存储一个哈希ID。哈希ID是标签的目标(或Git拼写的object)。

我对这些绘画没有特定的偏好风格,所以我会随便创造一些:

...--A--B--C     <-- foobranch
         \
          D--E   <-- barbranch
             ^
             :
             t   <-- tag: annotag

在这里,Git存储了一个新的注释标签对象t到仓库中,现在我们有了外部引用refs/tags/annotag指向t。与此同时,是标签对象t指向提交E

这意味着标签annotag涉及到两个哈希值:注释标签对象的ID和提交E的ID。同样,引用指向注释标签对象,而对象指向下一个东西,在这种情况下,指向提交E

与轻量级标签一样,注释标签对象可以指向除提交以外的其他对象类型。轻量级标签不能指向注释标签对象,但这只是因为当引用指向注释对象时,我们不再将其称为“轻量级”标签,而是称其为“注释”标签。然而,注释标签对象可以指向另一个注释标签对象。让我们这样做,让zomgtag指向对象t

...--A--B--C     <-- foobranch
         \
          D--E   <-- barbranch
             ^
             :
             t   <-- tag: annotag
             ^
             :
             z   <-- tag: zomgtag

如果您删除其中一个...

现在让我们尝试删除标签annotag. Git的一个有趣之处在于,删除参考实际上不会删除底层对象。底层对象通常保留到存储库中有太多垃圾时,此时Git会为您运行git gc --auto。GC(垃圾回收器)查找未引用的对象并实际将它们删除。因此,这个GC是一种类似鬼神或者收割者的存在,可以将死去的对象重新回收利用,腾出可用磁盘空间。

这适用于分支名称引用,例如:删除分支名称只是放弃分支尖端提交,而不是实际删除它。此外,如果有其他方法可以到达该提交,则提交本身不会消失,即使Grim Collector过来了。如果仍然有一些链接,GC会将对象保留在原地。对于正常(未删除)的分支,在重新设置基础(将提交链复制到新链)时,原始提交链尖端ID存储在分支的reflog中,直到reflog条目过期为止,整个链才能访问。 (这意味着您可以在至少30天的默认情况下返回并恢复重新设置的提交,因为30天和90天是默认的reflog过期时间。)
但是,这些规则也适用于注释标记对象!因此,如果我们删除annotag,同时保留zomgtag,则现在的情况如下:
...--A--B--C     <-- foobranch
         \
          D--E   <-- barbranch
             ^
             :
             t
             ^
             :
             z   <-- tag: zomgtag

现在标签对象没有名称了,但是可以通过z访问,我们可以通过refs/tags/zomgtag访问到它,因此将永久保存在存储库中(除非zomgtag也被删除,这样将变成未引用状态)。

现在zomgtag涉及到两个Git对象:从外部引用开始,我们找到注释的标签对象z。从这里,我们找到注释的标签对象t,并且从我们找到提交E

Git有一种特殊的语法描述在the gitrevisions documentation中,用于“剥离”标签:zomgtag^{}。描述如下:

一个后缀符号^跟着一对空括号表示对象可能是一个标签,递归地解引用标签,直到找到一个非标签对象为止。
如果我们创建更多的注释标签,我们可以让refs/tags/wacky指向一个标签对象,该标签对象指向第二个标签对象,该标签对象又指向另一个标签对象,最终在跟随多个标签之后,指向z,然后指向t,再指向E。符号wacky^{}的意思是“查找非标签对象”(在这种情况下是提交,尽管作为终点的也可以是树或blob)。

1
您真是位真正的绅士。感谢您提供如此精彩的答案!我学到了很多。 - CodeSmith

4
你执行 "git show my_tag" 命令并看到两个标签,因为 my_tag 标签是使用 my_tag_Build_1 标签作为参数创建的,就像下面的命令一样:
git tag -m "Tagging Release my_tag" -a my_tag my_tag_Build_1

另一方面,我无法解释为什么"git tag"命令中没有显示出my_tag_Build_1。这真的很奇怪。


有道理。我敢打赌新的my_tag创建后,my_tag_Build_1被删除了。Git必须保留一些关于my_tag_Build_1的信息,以便在这种情况下使用。 - CodeSmith
这是正确的答案。中间标签没有显示出来,因为它已被删除,所以它唯一的外部名称就是显示出来的那个。 - torek

2
你的标签可能是打包标签,这意味着为了传输效率,一堆引用已经被放入一个文件中。根据gitrepository-layout 文档所述:
packed-refs
    records the same information as refs/heads/, refs/tags/, and friends record in
    a more efficient way. See git-pack-refs(1). This file is ignored if
    $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/packed-refs" will be used instead.

来自git-pack-refs

传统上,分支和标签的末端(统称为refs)会以每个ref一个文件的形式存储在$GIT_DIR/refs目录下的(子)目录中。虽然许多分支末端往往经常更新,但大多数标签和一些分支末端则从不更新。当一个仓库有数百或数千个标签时,这种每个ref一个文件的格式既浪费存储空间又影响性能。

查看.git/packed-refs文件,你会看到类似于以下内容。

6a390ca7bca7b52b2009069138873fdbc7922c1d refs/tags/my_tag
^b6dcf8fa20296d146e9501ab9d25784879adeac8

基本上,标签是指向提交的对象。您可以使用-d来取消引用标签。您应该看到类似于以下内容:

git show-ref -d my_tag
6a390ca7bca7b52b2009069138873fdbc7922c1d refs/tags/my_tag
b6dcf8fa20296d146e9501ab9d25784879adeac8 refs/tags/my_tag^{}

这不是完全正确的:任何带注释的标签都涉及两个Git对象:带注释的标签和该带注释的标签的目标。参考本身是否已打包并不重要。在这里所讨论的情况中,有三个对象,因为第一个带注释的标签指向第二个带注释的标签。 - torek
@torek 没错,压缩的引用对我来说仍然很新。我不确定它如何与注释交互。我在我的项目上进行了检查,该项目不使用带注释的标签(使用的是标签“v2.8.0”),但已被压缩。 - Schwern
@torek,我刚刚测试了一下,使用未打包的带注释标签和已打包的带注释标签,两者结果相同,都有一个间接级别。因此,OP可能处于以下任何情况之一:带注释的标签或打包的标签(带注释或不带注释)。无论哪种情况,git show-ref -d是关键。或者你是说这是一个指向已删除的带注释标签的标签,该标签又指向一个提交?哇。 - Schwern
是的,它是一个两级注释标签(标签链的长度没有实际限制,不过如果要创建非常长的链,最好使用循环中的 git mktag 命令,而不是使用 git tag -a 命令后删除外部引用)。 - torek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接