为什么我使用 Git GUI 结帐时没有列出我的标签?

Why isn't my tag listed when I checkout with Git GUI?

我有一个本地 Git 存储库,其中包含三个带注释的标签:v0.1.0v0.1.1v0.1.2.

当我使用 gitk 查看我的项目历史时(存储库 → 可视化大师的历史),我可以看到分配给正确提交的每个标签。

但是,当我尝试在 Git GUI 中签出我的标签时(分支 → 签出... → 标签),v0.1.1 的标签没有出现。

当我去检查 gitk 中的每个标签时,我注意到 v0.1.0v0.1.2 的详细信息将它们列为 type commit,而 v0.1.1 的标签被列为 type tag.

值得注意的是,我重写了此标签的历史记录以修复拼写错误。我使用 git tag <tag name> <tag name> -f -m "<new message>".

编辑了我的标签消息

为什么在使用 Git GUI 结帐时看不到我的 v0.1.1 标签?为什么显示为type tag?

标签可以 point to any object 在 git 存储库中。如果您的标签类型是 "tag",那么您有一个标签指向另一个标签。

轻量级标签不是对象;因此,它们没有自己的哈希 ID,也没有其他任何东西(如另一个标签)可以指向它们。它们实际上只是 easy-to-remember 指向某个对象的哈希 ID 的名称,比分支名称小一点。

然而,带注释的标签个对象;它们就像提交一样,有自己的消息、作者、创建日期,最重要的是,它们有自己的哈希 ID。这意味着,有些令人困惑的是,它们可以被标记。

果然,正如您在 , this is exactly what happened. Acting on the advice found in How do you rename a Git tag? 中所述,您做了以下操作:

# avoid this...
git tag new old

由于 old 是一个带注释的标签,new 标签的目标将是 old 标签,而不是它指向的提交。

如果你想重命名一个带注释的标签,你应该使用

git tag -a new old^{}

old^{}dereference the tag recursively until a non-tag object is found(在我们的例子中是提交),并将其用作 new.

的目标对象

为了进一步说明:假设您有一个存储库...哦,就像这个:https://github.com/cyborgx37/sandbox/releases

在此 repo 中,您创建一个带注释的标签,如下所示:

> git tag -m "Version 0.1-beat" v0.1

糟糕...你拼错了 "beta",而且你还决定要将标签名称设为 v0.1-b。由于这已经发布,您决定执行 sane thing and just create a new tag. Following advice you found on the internet,通过复制第一个标签来创建您真正想要的标签(我附加 __tag 的原因将会变得很清楚):

> git tag -m "Version 0.1-beta" v0.1-b__tag v0.1

只是,这些是带注释的标签,意味着它们是真实的对象。所以当你创建v0.1-b__tag时,你实际上指向了v0.1。使用 cat-fileshow.

可以清楚地看到结果

这里是v0.1:

> git cat-file -p v0.1

object 5cf4de319291579d4416da8e0eba8a2973f8b0cf
type commit
tag v0.1
tagger JDB <jd@domain.com> 1521058797 -0400

Version 0.1-beat
> git show v0.1

tag v0.1
Tagger: JDB <jd@domain.com>
Date:   Wed Mar 14 16:19:57 2018 -0400

Version 0.1-beat

commit 5cf4de319291579d4416da8e0eba8a2973f8b0cf (HEAD -> master, tag: v0.1-b__tag, tag: v0.1, origin/master)
Author: JDB <jd@domain.com>
Date:   Tue Oct 10 12:17:00 2017 -0400

    add gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..42d9955
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+file.txt

请注意 v0.1-b__tag 在其目标类型和历史记录方面都不同:

> git cat-file -p v0.1-b__tag

object 889b82584b2294486f4956dfea17b05e6224fb7f
type tag
tag v0.1-b__tag
tagger JDB <jd@domain.com> 1521059058 -0400

Version 0.1-beta
> git show v0.1-b__tag

tag v0.1-b__tag
Tagger: JDB <jd@domain.com>
Date:   Wed Mar 14 16:24:18 2018 -0400

Version 0.1-beta

tag v0.1
Tagger: JDB <jd@domain.com>
Date:   Wed Mar 14 16:19:57 2018 -0400

Version 0.1-beat

commit 5cf4de319291579d4416da8e0eba8a2973f8b0cf (HEAD -> master, tag: v0.1-b__tag, tag: v0.1, origin/master)
Author: JDB <jd@domain.com>
Date:   Tue Oct 10 12:17:00 2017 -0400

    add gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..42d9955
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+file.txt

显然 Git GUI 对于可以签出的对象类型(提交,而不是标签)相当有选择性,因此它会忽略您指向另一个标签的标签。

如果你使用我上面建议的 git tag -a new old^{} 方法,你可以避免戏剧性的事情,并在第一时间得到你想要的。我将创建一个新标签 v0.1-b__commit 指向 v0.1 的提交,而不是直接指向 v0.1

> git tag -m "Version 0.1-beta" v0.1-b__commit v0.1^{}
> git cat-file -p v0.1-b__commit

object 5cf4de319291579d4416da8e0eba8a2973f8b0cf
type commit
tag v0.1-b__commit
tagger JDB <jd@domain.com> 1521059039 -0400

Version 0.1-beta
> git show v0.1-b__commit

tag v0.1-b__commit
Tagger: JDB <jd@domain.com>
Date:   Wed Mar 14 16:23:59 2018 -0400

Version 0.1-beta

commit 5cf4de319291579d4416da8e0eba8a2973f8b0cf (HEAD -> master, tag: v0.1-b__tag, tag: v0.1-b__commit, tag: v0.1, origin/master)
Author: JDB <jd@domain.com>
Date:   Tue Oct 10 12:17:00 2017 -0400

    add gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..42d9955
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+file.txt

我通常不使用任何 Git 图形用户界面,所以 GUI-specific 部分,我无法真正回答——但你观察到 之间存在差异带注释的标签轻量级标签是spot-on,是的,How do you rename a Git tag?[的一些答案中应该有一些警告]

When I went to check each tag in gitk, I noticed that the tag details were slightly different. The details for v0.1.0 and v0.1.2 listed them as type commit, while the tag for v0.1.1 was listed as type tag. I suspect this may be the cause of my problem ...

让我们理清它们之间的区别,并谈谈标签背后的机制。

在Git中,任何实际提交的"true name"是提交的哈希ID。哈希 ID 是又长又丑的 impossible-to-remember 字符串,例如显示在您的一个 GUI 窗格中的 ca5728b6...。我创建了一个新的空存储库并在其中进行了一次提交:

$ git init
Initialized empty Git repository in ...
$ echo for testing tags > README
$ git add README
$ git commit -m initial
[master (root-commit) a912caa] initial
 1 file changed, 1 insertion(+)
 create mode 100644 README
$ git rev-parse HEAD
a912caa83de69ef8e5e3e06c3d74b6c409068572

这标识了一个提交,我们可以看到使用 git cat-file -t,它告诉我们每个内部 Git 对象的 类型

$ git cat-file -t a912c
commit

大丑ID可以缩写,只要缩写是独一无二的,至少4个字母即可。1

无论如何,现在让我们制作两个不同的标签,指向同一个提交:

$ git tag -m "an annotated tag" annotag
$ git tag lightweight

并使用 git for-each-ref 检查它们:

$ git for-each-ref
a912caa83de69ef8e5e3e06c3d74b6c409068572 commit refs/heads/master
dc4695ffede0a877fdc61dc06f5ad5c6d5cfc356 tag    refs/tags/annotag
a912caa83de69ef8e5e3e06c3d74b6c409068572 commit refs/tags/lightweight

带注释的标签与轻量级标签具有不同的哈希 ID。

这里的技巧是轻量级标签在参考数据库中创建一个名称,在本例中为refs/tags/lightweight。参考数据库中的名称存储哈希 ID,因此这个存储我们单次提交的哈希 ID。

另一方面,带注释的标签作为实际的存储库对象存在,因此我们可以使用 git cat-file:

检查其类型并查看其内容
$ git cat-file -t dc4695ffede0a877fdc61dc06f5ad5c6d5cfc356
tag
$ git cat-file -p dc4695ffede0a877fdc61dc06f5ad5c6d5cfc356 | sed 's/@/ /'
object a912caa83de69ef8e5e3e06c3d74b6c409068572
type commit
tag annotag
tagger Chris Torek <chris.torek gmail.com> 1521059496 -0700

an annotated tag

请注意,在 存储库 数据库中,由哈希 ID 键入并包含对象数据的带注释的标记对象包含提交的哈希 ID。实际上,还有一个名为 refs/tags/annotag 的 "lightweight-like" 标记指向带注释的标记对象。但是因为它指向一个带注释的标签对象,所以它被视为带注释的标签。

创建新标签时,可以将其指向任何现有对象。让我们看一下与单个提交关联的对象:

$ git cat-file -p HEAD | sed 's/@/ /'
tree 4d73be7092200632865da23347ba0af4ac6c91f7
author Chris Torek <chris.torek gmail.com> 1521053169 -0700
committer Chris Torek <chris.torek gmail.com> 1521053169 -0700

initial

这个提交对象引用了一个树对象,我们可以检查它:

$ git cat-file -p 4d73be7092200632865da23347ba0af4ac6c91f7
100644 blob 938c7cff87a9b753ae70d91412d3ead5c95ef932    README

并且树指向一个 blob 对象,我们也可以检查它:

$ git cat-file -p 938c7cff87a9b753ae70d91412d3ead5c95ef932
for testing tags

这是文件的内容README。让我们标记一下:

$ git tag the-file 938c7cff87a9b753ae70d91412d3ead5c95ef932

并检查其类型:

$ git cat-file -t the-file
blob

这不是标签的正常使用,但它是允许的。让我们尝试为带注释的标签制作一个轻量级标签:

$ git tag maybe-light annotag
$ git cat-file -t maybe-light
tag
$ git cat-file -p maybe-light | sed 's/@/ /'
object a912caa83de69ef8e5e3e06c3d74b6c409068572
type commit
tag annotag
tagger Chris Torek <chris.torek gmail.com> 1521059496 -0700

an annotated tag

这个maybe-light标签指向属于注释标签annotag的注释标签对象maybe-light 注释标签 吗?这取决于你的观点,不是吗?我会说它既是也不是:它是一个指向带注释的标签的轻量级标签,但它不是 与带注释的标签对象同名的轻量级标签,它声称在对象内部是 / belong-to annotag。但我还要说,在某种程度上,annotag 既是轻量级标签又是带注释的标签:它是一种轻量级标签,提供带注释的标签对象的 ID。它们使用相同的名称,所以我将其称为 "annotated tag" 并将 refs/tags/annotag 称为标签名称,与 refs/tags/maybe-light 是标签名称的方式相同。

无论如何,我们也可以制作更多的带注释的标签指向这些对象中的任何一个。如果我们使一个带注释的标签指向另一个带注释的标签,我们最终会在存储库中得到两个带注释的标签对象:

$ git tag -m "also annotated" anno2
$ git for-each-ref
a912caa83de69ef8e5e3e06c3d74b6c409068572 commit refs/heads/master
060527046d210f0219170cdc6354afe4834ddc6d tag    refs/tags/anno2
dc4695ffede0a877fdc61dc06f5ad5c6d5cfc356 tag    refs/tags/annotag
a912caa83de69ef8e5e3e06c3d74b6c409068572 commit refs/tags/lightweight
dc4695ffede0a877fdc61dc06f5ad5c6d5cfc356 tag    refs/tags/maybe-light
938c7cff87a9b753ae70d91412d3ead5c95ef932 blob   refs/tags/the-file

从这里可以看出anno2多了一个对象,0605...:

$ git cat-file -p 0605 | sed 's/@/ /'
object a912caa83de69ef8e5e3e06c3d74b6c409068572
type commit
tag anno2
tagger Chris Torek <chris.torek gmail.com> 1521060518 -0700

also annotated

与此同时,git for-each-refmaybe-light 标签描述为 tag 而不是 commit:这只是告诉我们它的直接目标对象,而没有遵循到进一步的对象,是一个标签,而不是一个提交。

让我们为 blob 再制作一个带注释的标签:

$ git tag -m "annotated blob" annoblob the-file

因为它是一个带注释的标签,git for-each-ref 说它的类型是 tag(试一试!)。

Git 调用跟踪标签到其最终对象 "peeling the tag" 的过程,并且有一个特殊的语法:

$ git rev-parse annotag annotag^{} annoblob annoblob^{}
dc4695ffede0a877fdc61dc06f5ad5c6d5cfc356
a912caa83de69ef8e5e3e06c3d74b6c409068572
398b3b89e0377b8942e2f84c97a24afaad0dccb0
938c7cff87a9b753ae70d91412d3ead5c95ef932

请注意,这与仅跟随标记 一次 不同,正如我们看到的,如果我们以这种方式解析 anno2

$ git rev-parse anno2^{}
a912caa83de69ef8e5e3e06c3d74b6c409068572

a912... 是提交的ID,不是第二个注释标签。比较:

$ git rev-parse anno2 anno2^{tag}
060527046d210f0219170cdc6354afe4834ddc6d
060527046d210f0219170cdc6354afe4834ddc6d

第一个找到anno2指向的对象的ID;第二个验证它是类型 tag 的数据库对象。两者当然是同一个ID,确实是tag类型的对象。我们可以专门要求提交:

$ git rev-parse anno2^{commit}
a912caa83de69ef8e5e3e06c3d74b6c409068572

但如果我们使用名称 annoblob 执行此操作,我们会收到错误消息:

$ git rev-parse annoblob^{commit}
error: annoblob^{commit}: expected commit type, but the object
 dereferences to blob type

这就是 ^{} 语法存在的原因:它意味着 跟随标签,直到到达 non-tag,无论它是什么。


1four-character限制意味着如果你命名一个分支cab,你就可以了。但是,如果您将其命名为 face,那是 分支名称吗? 还是 原始哈希 ID?如果不止一件事呢?提示见the gitrevisions documentation,但答案是:这取决于命令。如果您拼出引用,refs/heads/face 甚至只是 heads/face,它不再类似于分支名称 缩写的哈希 ID。不幸的是 git checkout 需要朴素的名称 face(但如果可以的话,总是将其视为分支名称)。


总结

一个标签名只是refs/tags/name-space中的一个名字。 git tag 命令可以创建新的标签名称。此名称必须指向某个哈希 ID; ID 可以是任何现有对象的 ID,或者您可以 git tag 创建一个新的标签对象。

一个标签对象带注释的标签对象是存储库数据库中的一个实体。它有一个唯一的哈希 ID,就像提交一样。它的类型为 tag(相对于提交,其类型为 commit)。它的元数据由目标对象、标记器名称、标记名称、您喜欢的任何消息和可选的 PGP 签名组成。

标记对象的目标对象 是存储库数据库中的任何现有对象。创建标签对象时该对象需要存在。这可以防止带注释的标签指向自身,或指向您尚未创建的标签对象,从而防止图中出现循环。

运行 git tag 创建一个新标签,要么只创建指向某个现有对象的标签名称,要么创建指向某个现有对象的新标签对象的标签名称。现有对象,无论它是什么,都将继续存在。

运行 git tag -d 仅删除 标签名称 。标签 object,如果有,则保留在存储库中。与提交对象一样,当且仅当没有其他引用可以到达标记对象时,它最终将成为 garbage-collected 并被丢弃。 (这会在将来某个时间发生,当 git gc 运行时。)