Does every git tag have an associated commit id?

Viewed 483

I ran:

$ git log master

I got:

commit 7940b82568e1d28a76557fbb403c46c3ef3cfe90 (tag: prod-api-2019-04-23, master)
Author: Kent Mills <kent.clark@mwzx.com>
Date:   Tue Apr 23 10:33:12 2019 -0700

I am wondering - wrt git tags - does every git tag have a unique commit id associated with it?

For example, if I run git tag how can I see what the commit id's are for those tags? Something like git tag --full or something.

3 Answers

Tags come in one or two parts:

  • There's a tag name, which is a reference, much like a branch name, except that branch b has full name refs/heads/b while tag t has full name refs/tags/t.

    All references store one (1) hash ID. Branch names are constrained to store only commit hash IDs, even though there are four object types: commit, tree, blob, and annotated tag. Tag names are not constrained this way.

  • If the hash ID stored in the tag reference is the ID of a tag object, the tag object itself stores more data. The data include:

    • the name of the tagger;
    • an optional PGP key;
    • the hash ID of another Git object.


    The other Git object here is usually a commit, but that's not a hard and fast requirement: like a tag reference, the tag object can point to another object of any type, even another tag object.

When a tag name points to a tag object, Git calls that an annotated tag. Otherwise, Git calls that reference a lightweight tag.

To turn a reference name into its object, use git rev-parse:

git rev-parse refs/heads/master
git rev-parse refs/tags/v1.2

for instance. By default, git rev-parse finds the target object hash ID and stops there, but you can instruct it to keep going:

  • The suffix ^{commit} tells git rev-parse that if the object itself is not a commit, but might lead to a commit, keep working. If the object eventually leads to a commit—e.g., if the object is a tag object and its target is another tag object and that second tag object's target is a commit—this "following" process succeeds, and git rev-parse prints the ID of the commit. If the object eventually leads elsewhere—e.g., if the object is a tag object, but the target of this tag object is a blob—the operation fails and git rev-parse prints an error message to stderr, and no hash ID to stdout.

  • The suffix ^{tree} or ^{blob} tells Git to follow tags or commits to their tree or to a blob. Since every commit object has exactly one tree, the ^{tree} suffix succeeds if the name points to a tag object that points to a commit. It also succeeds if the name points directly to a tree—possible for both lightweight and annotated tags. The ^{blob} suffix can only succeed if the tag points to a blob (directly, as a lightweight tag, or indirectly through some number of tag objects), though, because there's no well-defined way to move from a commit or tree to one particular blob: trees typically hold many blob hash IDs.

  • The suffix ^{} directs git rev-parse to peel a tag. That is, if the name is a tag name and the first level object is a tag object, Git should follow that object. If the resulting object is another tag object, Git should follow that one as well. Git should repeat this until the tag resolves to any non-tag object—any commit, tree, or blob. Then git rev-parse should produce that non-tag object's hash ID.

Several other Git commands can be told to peel tags, and some do it even without you telling them. For instance, git ls-remote calls up another Git and has the other Git tell your Git about its references—including tag names—and their object hash IDs. When their Git tells your Git about one of their tag names, their Git produces both the top level tag object, if any, and the peeled object hash ID. This shows up as:

<hash1> refs/tags/<tag>
<hash2> refs/tags/<tag>^{}

Here hash1 is the object ID of the annotated tag object, and hash2 is the object ID of the final non-tag object obtained by the peeling process (there must by definition always be such an object).

If you don't see this annotation in git ls-remote output for a tag object, that means their tag is a lightweight tag, pointing directly to the commit, tree, or blob object whose hash ID is listed.

The git for-each-ref command has the * directive to follow tags:

If fieldname is prefixed with an asterisk (*) and the ref points at a tag object, use the value for the field in the object which the tag object refers to (instead of the field in the tag object).

It's not clear from the documentation whether this fully peels a tag, or just goes one level down: if annotated tag X has as its target the annotated-tag-object of annotated tag Y that has, as Y's target, commit C, does %(*objectname) produce hash Y, or hash C?

does every git tag have a unique commit id associated with it?

A git tag is just an alias for a commit (hash). The alias of the tag should be unique. (as Torek mentioned, this could also point something else than a commit, as long as it has an object id)

So every git tag points to a commit, but there could be multiple tags to one commit.

For example, if I run git tag how can I see what the commit id's are for those tags?

You could do:

git show-ref --tags -d

example output:

12d265006e50c61512421e88269f4811d52eaf36 refs/tags/v1.0
06d7af2f64d89551ef6324bc44de2454d228f0a8 refs/tags/v1.0-rc1
41137858e7ab5dc353d2478e2454d20735b050a1 refs/tags/v1.0-rc2
066031c9448170466ce8acd4bb5767a1ad02e2e6 refs/tags/v1.0-rc3
0fa020943977759c2fec1575f6fffa6a7799f44f refs/tags/v1.0-rc4
7cf72a2765525cadf2629eb42241d05304330052 refs/tags/v1.0-rc5
00a88fea533e7dfd12b8b17a82f1b57a486ebd00 refs/tags/v1.0-rc6
67a2d071f078be52776a44fe5105b999a8701944 refs/tags/v1.0-rc7
7f9e410a3bc272f4bfc29ea7a3a830883b47f1c2 refs/tags/v1.0.0-rtm-beta5
a0fc4b158f888c0813a9d4867e0627d1525b3fb1 refs/tags/v1.0.0-rtm-beta6
b77b36388895ac1e61d4b886e02e98861010c569 refs/tags/v1.0.0-rtm-beta7
ebc4180644081f893518bda2c0cab1c0d763cb5e refs/tags/v1.0.1
d5547d8cec7e2e7e7c912a8b56ab54cb92933f57 refs/tags/v1.0.2
3d0d78f4a8ae88c9b573d08aaeabc1430780236c refs/tags/v1.1.0
9f59232807ad963b63a3b634e7ebd2256a1ba0e8 refs/tags/v1.2.0
197deb1adf90bced16d04833b671b7e628505fe7 refs/tags/v1.2.1
759c5c2bc687a63fbb41e50de4e8e40ded449c5d refs/tags/v1.3.0
f009ec166256707f6887e65536d9d63f14894bbb refs/tags/v1.4.0
2560e6d313f2cf6800201b7dcfefe424beb2c675 refs/tags/v1.5.0
896e71519655361ef9b513af4d2538fa7b7c87be refs/tags/v1.5.1

If you need more control, you could use the --format (format options listed here)

git tag --format '%(refname:strip=2) %(objectname)'

output:

v1.0 12d265006e50c61512421e88269f4811d52eaf36
v1.0-rc1 06d7af2f64d89551ef6324bc44de2454d228f0a8
v1.0-rc2 41137858e7ab5dc353d2478e2454d20735b050a1
v1.0-rc3 066031c9448170466ce8acd4bb5767a1ad02e2e6
v1.0-rc4 0fa020943977759c2fec1575f6fffa6a7799f44f
v1.0-rc5 7cf72a2765525cadf2629eb42241d05304330052
v1.0-rc6 00a88fea533e7dfd12b8b17a82f1b57a486ebd00
v1.0-rc7 67a2d071f078be52776a44fe5105b999a8701944
v1.0.0-rtm-beta5 7f9e410a3bc272f4bfc29ea7a3a830883b47f1c2
v1.0.0-rtm-beta6 a0fc4b158f888c0813a9d4867e0627d1525b3fb1
v1.0.0-rtm-beta7 b77b36388895ac1e61d4b886e02e98861010c569
v1.0.1 ebc4180644081f893518bda2c0cab1c0d763cb5e
v1.0.2 d5547d8cec7e2e7e7c912a8b56ab54cb92933f57
v1.1.0 3d0d78f4a8ae88c9b573d08aaeabc1430780236c
v1.2.0 9f59232807ad963b63a3b634e7ebd2256a1ba0e8
v1.2.1 197deb1adf90bced16d04833b671b7e628505fe7
v1.3.0 759c5c2bc687a63fbb41e50de4e8e40ded449c5d
v1.4.0 f009ec166256707f6887e65536d9d63f14894bbb
v1.5.0 2560e6d313f2cf6800201b7dcfefe424beb2c675
v1.5.1 896e71519655361ef9b513af4d2538fa7b7c87be

A tag is just like a branch... in other words: a branch/tag is just a pointer to a revision... the main difference is that tags aren't normally moved around whereas branches move around all the time (even with something as simple as when you commit)... and I'm sure somebody will come up with a simpler trick, but this shows where each tag is pointing to:

git tag | xargs git show --summary
Related