Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

Project history in gitk

However, when I try to checkout my tags in Git GUI (Branch → Checkout... → Tags), the tag for v0.1.1 doesn't appear.

Git GUI tag list

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.

Details for tag v0.1.1

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?

like image 753
Stevoisiak Avatar asked Nov 29 '22 00:11

Stevoisiak


2 Answers

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
like image 51
JDB Avatar answered Dec 09 '22 09:12

JDB


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

like image 39
torek Avatar answered Dec 09 '22 08:12

torek