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
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 ...
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).
Summary
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.)