Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are there two different git commits for the same tag?

Tags:

git

I'm trying to determine the commit SHA associated with a particular tag. When I execute show-ref, I get the following output

$ git show-ref my_tag
6a390ca7bca7b52b2009069138873fdbc7922c1d refs/tags/my_tag

When I execute rev-list I get this output

$ git rev-list -n 1 my_tag
b6dcf8fa20296d146e9501ab9d25784879adeac8

The commit SHA's are different, but I don't understand why. It looks like b6dcf8, generated by rev-list, is the correct one. If I try and checkout the first commit with git checkout 6a390c and then look at the log, I'm not actually on 6a390c; b6dcf8 is displayed.

Can anyone explain why there might be a disconnect? Why am I redirected to b6dcf8 when I try and checkout 6a390ca.

Update

I also noticed that when I execute git show my_tag, I get output that looks like this

tag my_tag
Tagger: Me <[email protected]>
Date:   Mon Apr 4 14:43:46 2016 -0400

Tagging Release my_tag

tag my_tag_Build_1
Tagger: Me <[email protected]>
Date:   Thu Mar 31 10:46:18 2016 -0400

Tagging my_tag_Build_1

commit b6dcf8fa20296d146e9501ab9d25784879adeac8
Author: Me <[email protected]>
Date:   Wed Mar 30 18:12:10 2016 -0400

Remove secret_key_base values from secrets.yml

It's picking up two tags my_tag and my_tag_Build_1. However, if I run git tag, the list of tags only has

my_tag

If I run git show my_tag_Build_1, I get

fatal: ambiguous argument 'my_tag_Build_1': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:

It seems like git is confused. Maybe the my_tag_Build_1 tag existed at some point, but it doesn't seem to exist anymore.

like image 827
CodeSmith Avatar asked Dec 02 '22 14:12

CodeSmith


1 Answers

I'll add one more answer, even though Marcelo Ávila de Oliveira's answer is correct, because I want to draw the graph bits. :-)

Normally I like to draw commit graphs like this, at least for StackOverflow:

...--A--B--C     <-- foobranch
         \
          D--E   <-- barbranch

Here the two tip (rightmost) commits on the two branches, C and E, each have a branch name pointing to them. That is, refs/heads/foobranch contains the ID of commit C, and refs/heads/barbranch contains the ID of commit E.

Lightweight tags

Lightweight tags work exactly like branch names. If we add the tag bartag to point to commit E, we get:

...--A--B--C     <-- foobranch
         \
          D--E   <-- barbranch, tag: bartag

where refs/heads/bartag (an actual file in .git unless it has become "packed" and is now stored in the file .git/packed-refs instead) also stores the ID of commit E. There are three differences between a lightweight tag and a branch:

  1. The lightweight tag's full name starts with refs/tags/ instead of refs/heads/.
  2. The lightweight tag should never change to point to another commit (this is only semi-enforced by Git, and poorly so in versions prior to 1.8.something, but branch names do normally change which commit they point to, vs tags, which don't).
  3. Git enforces a rule that branch names shall only point to commit objects. Normally tag names also only point to commit objects, but it's possible to make one that points to a tree or blob. There's one more object type—the annotated tag object—but that makes the tag different! Hold on to that thought, and let's finish this part up.

A lightweight tag, then, is simply a reference whose full name is spelled refs/tags/.... This external reference exists somewhere—often as a separate file like .git/refs/tags/bartag—and it points to a Git object in the repository (.git/objects/..., possibly packed into a pack-file). When it points to a commit, which is the normal case, this gets us into the commit DAG: the tag locates the commit, which can get us a work-tree, and also lets us explore earlier (ancestor) commits by following the "parent" ID, going from commit E back to D.

Annotated tags

An annotated tag uses almost the same picture, except now, instead of the lightweight tag bartag pointing directly to a commit, now Git stores an annotated tag object into the repository. This annotated tag object has its own data (date, tagger, message, optional digital signature, and whatever else you like), and also stores one hash ID. The hash ID is the target (or object, as Git spells it) for the tag.

I don't have a preferred style for drawing these here, so I'll just make something up:

...--A--B--C     <-- foobranch
         \
          D--E   <-- barbranch
             ^
             :
             t   <-- tag: annotag

Here, Git has stored a new annotated tag object t in the repository, and now we have the external reference refs/tags/annotag pointing to t. Meanwhile it's the tag object t that points to commit E.

This means there are two hash IDs involved with tag annotag: the ID of the annotated tag object, and the ID of commit E. Again, the reference points to the annotated tag object, and the object points to the next thing—in this case, to commit E.

As with lightweight tags, though, annotated tag objects can point to other object types than commits. A lightweight tag cannot point to an annotated tag object, but that's only because, when the reference points to an annotated object, we no longer call it a "lightweight" tag, we now call it an "annotated" tag. An annotated tag object, however, can point to another annotated tag object. Let's do that and make zomgtag point to object t:

...--A--B--C     <-- foobranch
         \
          D--E   <-- barbranch
             ^
             :
             t   <-- tag: annotag
             ^
             :
             z   <-- tag: zomgtag

If you delete one of these...

Now let's try deleting tag annotag. One interesting thing about Git is that deleting a reference does not actually delete the underlying object. Underlying objects are normally left around until there is too much garbage cluttering up the repository, at which point Git runs git gc --auto for you. The GC (Garbage Collector) finds the unreferenced objects and actually removes them. This GC is thus a sort of Grim Reaper, or perhaps Grim Collector, that recycles the dead objects back into usable disk space.

This is true for branch name references, for instance: deleting a branch name simply abandons the branch-tip commit, rather than actually deleting it. Moreover, if there's some other way to reach that commit, the commit itself won't go away, even when the Grim Collector comes by. If there's still some linkage, GC leaves the object in place. For normal (non-deleted) branches, when you rebase (which copies commit chains to new chains), the original commit-chain tip IDs are stored in the branch's reflog, which keeps the entire chain reachable until the reflog entries expire. (This means you can go back and recover rebased commits for at least 30 days by default, since 30 and 90 days are the default reflog expiration times.)

But these same rules apply to annotated tag objects! So if we delete annotag while leaving in zomgtag, the picture is now:

...--A--B--C     <-- foobranch
         \
          D--E   <-- barbranch
             ^
             :
             t
             ^
             :
             z   <-- tag: zomgtag

There is no longer a name for tag object t, but it is reachable through z, which we reach through refs/tags/zomgtag, so t sticks around in the repository forever. (Well, unless zomgtag is also deleted, so that t becomes unreferenced.)

Now there are two Git objects involved in zomgtag: starting from the external reference, we find the annotated tag object z. From this we find the annotated tag object t, and from t we find commit E.

Git has a special syntax described in the gitrevisions documentation for "peeling" a tag: zomgtag^{}. The description says:

A suffix ^ followed by an empty brace pair means the object could be a tag, and dereference the tag recursively until a non-tag object is found.

If we make more annotated tags, we can have refs/tags/wacky point to a tag object that points to a second tag object that points to yet another tag object, that eventually, after following many tags, points to z which points to t which points to E. The notation wacky^{} means "find the non-tag-object" (in this case, a commit, though as always, the endpoint can also be a tree or blob).

like image 63
torek Avatar answered Dec 11 '22 15:12

torek