I have a local Git repository with three annotated tags: v0.1.0
, v0.1.1
, and v0.1.2
.
When I view my project's history with gitk
(Repository → Visualize master's history), I can see each tag assigned to the proper commit.
However, when I try to checkout my tags in Git GUI (Branch → Checkout... → Tags), the tag for v0.1.1
doesn't appear.
When I went to check each tag in gitk, I noticed that 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
.
It's worth noting that I've rewritten history on this tag to fix a typo. I edited my tag message using git tag <tag name> <tag name> -f -m "<new message>"
.
Why can't I see my v0.1.1
tag when checking out with Git GUI? Why does it appear as type tag
?
Tags can point to any object in the git repository. If your tag type is "tag", then you have a tag pointing to another tag.
Lightweight tags are not objects; thus, they have no hash ID of their own and nothing else (like another tag) can point to them. They are literally just easy-to-remember names pointing to some object's hash ID, a little less than a branch name.
However, annotated tags are objects; they are like commits, with their own message, author, created date and, most importantly, their own hash ID. This means that, somewhat confusingly, they can be tagged.
Sure enough, as you described in your comment, this is exactly what happened. Acting on the advice found in How do you rename a Git tag?, you did the following:
# avoid this...
git tag new old
Since old
was an annotated tag, the target for the new
tag will be the old
tag, not the commit that it was pointing to.
If you want to rename an annotated tag, you should use
git tag -a new old^{}
old^{}
will dereference the tag recursively until a non-tag object is found (in our case, a commit), and use that as the target object for new
.
To further illustrate: let's say you have a repo... oh, like this one: https://github.com/cyborgx37/sandbox/releases
In this repo you create an annotated tag like so:
> git tag -m "Version 0.1-beat" v0.1
Oh shoot... you misspelled "beta" and also you've decided that you want the tag name to be v0.1-b
. Since this has already been published, you decide to do the sane thing and just create a new tag. Following advice you found on the internet, you create the tag you actually wanted (I appended __tag
for reasons that will become clear) by copying the first tag:
> git tag -m "Version 0.1-beta" v0.1-b__tag v0.1
Only, these are annotated tags, meaning they are actual objects. So when you created v0.1-b__tag
, you actually pointed it at v0.1
. You can see the result clearly using cat-file
and show
.
Here's v0.1
:
> git cat-file -p v0.1
object 5cf4de319291579d4416da8e0eba8a2973f8b0cf
type commit # ⇦ v0.1 is a tag which points to a commit
tag v0.1
tagger JDB <[email protected]> 1521058797 -0400
Version 0.1-beat
> git show v0.1
# v0.1 is a tag
# ⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩
tag v0.1
Tagger: JDB <[email protected]>
Date: Wed Mar 14 16:19:57 2018 -0400
Version 0.1-beat
# which is pointing directly to a commit
# ⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩
commit 5cf4de319291579d4416da8e0eba8a2973f8b0cf (HEAD -> master, tag: v0.1-b__tag, tag: v0.1, origin/master)
Author: JDB <[email protected]>
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
Notice that v0.1-b__tag
is different both in its target type as well as its history:
> git cat-file -p v0.1-b__tag
object 889b82584b2294486f4956dfea17b05e6224fb7f
type tag # ⇦ v0.1-b__tag is a tag which points to a tag
tag v0.1-b__tag
tagger JDB <[email protected]> 1521059058 -0400
Version 0.1-beta
> git show v0.1-b__tag
# v0.1-b__tag is a tag
# ⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩
tag v0.1-b__tag
Tagger: JDB <[email protected]>
Date: Wed Mar 14 16:24:18 2018 -0400
Version 0.1-beta
# which is pointing to the v0.1 tag
# ⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩
tag v0.1
Tagger: JDB <[email protected]>
Date: Wed Mar 14 16:19:57 2018 -0400
Version 0.1-beat
# which is pointing to the intended target commit
# ⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩⇩
commit 5cf4de319291579d4416da8e0eba8a2973f8b0cf (HEAD -> master, tag: v0.1-b__tag, tag: v0.1, origin/master)
Author: JDB <[email protected]>
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
Apparently Git GUI is rather selective about what types of objects can be checked out (commits, not tags), so it's ignoring your tag pointing at another tag.
If you use the git tag -a new old^{}
approach I suggested above, you can avoid the drama and get what you wanted in the first place. I'll create a new tag, v0.1-b__commit
that points to v0.1
's commit, rather than to v0.1
directly:
> 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 <[email protected]> 1521059039 -0400
Version 0.1-beta
> git show v0.1-b__commit
tag v0.1-b__commit
Tagger: JDB <[email protected]>
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 <[email protected]>
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
I don't normally use any of the Git GUIs so the GUI-specific parts, I can't really answer—but your observation that there's a difference here between annotated tags and lightweight tags is spot-on, and yes, there should be some warning(s) in some of the answers to 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
andv0.1.2
listed them astype commit
, while the tag forv0.1.1
was listed as type tag. I suspect this may be the cause of my problem ...
Let's clear up the difference between these, and talk about the mechanisms behind tags.
In Git, the "true name" of any actual commit is the commit's hash ID. Hash IDs are long, ugly, impossible-to-remember strings, such as the ca5728b6...
showing in one of your GUI panes. I made a new, empty repository and made one commit in it:
$ 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
This identifies a commit, and we can see that using git cat-file -t
, which tells us about the type of each internal Git object:
$ git cat-file -t a912c
commit
You can abbreviate the big ugly IDs as long as the abbreviation is unique and is at least four letters.1
Anyway, now let's make two different tags, pointing to this same commit:
$ git tag -m "an annotated tag" annotag
$ git tag lightweight
and use git for-each-ref
to inspect them:
$ git for-each-ref
a912caa83de69ef8e5e3e06c3d74b6c409068572 commit refs/heads/master
dc4695ffede0a877fdc61dc06f5ad5c6d5cfc356 tag refs/tags/annotag
a912caa83de69ef8e5e3e06c3d74b6c409068572 commit refs/tags/lightweight
The annotated tag has a different hash ID than the lightweight tag.
The trick here is that the lightweight tag creates only a name in the reference database, in this case, refs/tags/lightweight
. Names in the reference database store hash IDs, so this one stores the hash ID of our single commit.
An annotated tag, on the other hand, exists as an actual repository object, so we can inspect its type and see its contents, using 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
Note that the annotated tag object, in the repository database keyed by hash ID and containing object data, contains the hash ID of the commit. There is, in effect, also a "lightweight-like" tag named refs/tags/annotag
pointing to the annotated tag object. But since it points to an annotated tag object, it's treated as an annotated tag.
When you make a new tag, you can point it to any existing object. Let's take a look at the objects associated with the single commit:
$ 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
This commit object refers to a tree object, which we can inspect:
$ git cat-file -p 4d73be7092200632865da23347ba0af4ac6c91f7
100644 blob 938c7cff87a9b753ae70d91412d3ead5c95ef932 README
and the tree points to a blob object, which we can also inspect:
$ git cat-file -p 938c7cff87a9b753ae70d91412d3ead5c95ef932
for testing tags
which is the content of the file README
. Let's tag that:
$ git tag the-file 938c7cff87a9b753ae70d91412d3ead5c95ef932
and inspect its type:
$ git cat-file -t the-file
blob
This is not the normal use of a tag, but it's allowed. Let's try making a lightweight tag for the annotated tag:
$ 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
This maybe-light
tag points to the annotated tag object that belongs to the annotated tag annotag
. Is maybe-light
an annotated tag? That depends on your point of view, doesn't it? I would say that it both is and isn't: it's a lightweight tag pointing to an annotated tag, but it's not the lightweight tag that goes by the same name as the annotated tag object, which claims right inside the object to be / belong-to annotag
. But I would also say that in a way, annotag
is both a lightweight and annotated tag: it's a lightweight tag that gives the ID of the annotated tag object. They use the same name so I'd call it an "annotated tag" and refer to refs/tags/annotag
as the tag name, the same way refs/tags/maybe-light
is a tag name.
In any case, we can also make more annotated tags pointing to any of these objects. If we make an annotated tag pointing to the other annotated tag, we end up with two annotated tag objects in the repository:
$ 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
You can see from this that anno2
has a new object, 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
Meanwhile, git for-each-ref
describes the maybe-light
tag as a tag
rather than a commit
: that just tells us that its immediate target object, without following through to further objects, is a tag, not a commit.
Let's make one more annotated tag, for the blob:
$ git tag -m "annotated blob" annoblob the-file
Since it's an annotated tag, git for-each-ref
says that its type is tag
(try it!).
Git calls the process of following a tag to its ultimate object "peeling the tag", and there is a special syntax for that:
$ git rev-parse annotag annotag^{} annoblob annoblob^{}
dc4695ffede0a877fdc61dc06f5ad5c6d5cfc356
a912caa83de69ef8e5e3e06c3d74b6c409068572
398b3b89e0377b8942e2f84c97a24afaad0dccb0
938c7cff87a9b753ae70d91412d3ead5c95ef932
Note that this is different from just following the tag once, as we see if we parse anno2
this way:
$ git rev-parse anno2^{}
a912caa83de69ef8e5e3e06c3d74b6c409068572
The a912...
is the ID of the commit, not the second annotated tag. Compare with:
$ git rev-parse anno2 anno2^{tag}
060527046d210f0219170cdc6354afe4834ddc6d
060527046d210f0219170cdc6354afe4834ddc6d
The first finds the ID of the object to which anno2
points; the second verifies that it's a database object of type tag
. Both are of course the same ID, and it is indeed an object of type tag
. We can ask specifically for a commit:
$ git rev-parse anno2^{commit}
a912caa83de69ef8e5e3e06c3d74b6c409068572
but if we do this with the name annoblob
we get an error:
$ git rev-parse annoblob^{commit}
error: annoblob^{commit}: expected commit type, but the object
dereferences to blob type
which is why the ^{}
syntax exists: it means follow tags until you reach a non-tag, whatever that is.
1The four-character limit means that if you name a branch cab
, you're OK. If you name it face
, though, is that a branch name or a raw hash ID? What if it could be more than one thing? See the gitrevisions documentation for hints, but the answer is: it depends on the command. If you spell out the reference, refs/heads/face
or even just heads/face
, it no longer resembles both a branch name and an abbreviated hash ID. Unfortunately git checkout
demands the unadorned name face
(but always treats it as a branch name, if it can).
A tag name is simply a name in the refs/tags/
name-space. The git tag
command can make new tag names. This name must point to some hash ID; the ID can be the ID of any existing object, or you can have git tag
make a new tag object.
A tag object or annotated tag object is an entity in the repository database. It has a unique hash ID, just like a commit. It has type tag
(vs a commit, which has type commit
). Its metadata consists of the target object, the tagger name, the tag name, any message you like, and an optional PGP signature.
The target object of a tag object is any existing object in the repository database. That object needs to exist when creating the tag object. This prevents the annotated tag from pointing to itself, or to a tag object you have not yet created, which prevents cycles in the graph.
Running git tag
to make a new tag creates either just the tag name pointing to some existing object, or the tag name pointing to a new tag object pointing to some existing object. The existing object, whatever it is, continues existing.
Running git tag -d
deletes only the tag name. The tag object, if there is one, remains in the repository. Like commit objects, it will eventually be garbage-collected and discarded if and only if there are no other references by which one can reach the tag object. (This happens some time in the future, when git gc
runs.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With