Search code examples
gitazure-devops.git-folder

Tagging a commit produces different hash in .git folder


As I was working on a project in Azure Repos, I noticed that the commit hash value inside .git/refs/tags/my-tag compared to the hash of the commit shown in the UI (which is the 'real' one) differed. To reproduce, I tag a commit, run 'git fetch' locally, and look for the tag hash in the .git folder. The hash in .git/refs/tags/my-tag is different from the commit hash I see in the Azure Repos UI (under Repos -> Tags). How do I get the commit hash that the tag points to (the 'real' one)?

Thanks in advance.

Getting the exact commit hash that the tag points to.


Solution

  • I think you've formulated two questions and mixed them together, plus perhaps a third question:

    1. Why do I get a different (annotated) tag hash than some existing (annotated) tag hash?
    2. Why do annotated tags have their own hash IDs, separate from the hash IDs of the commits they tag?
    3. Given a tag that may or may not be annotated, how do I get the hash ID of the target commit?

    The answer to question 3 is simple enough: use git rev-parse with the right syntax:

    git rev-parse refs/tags/v1.2^{commit}
    

    for instance will find the hash ID of the commit target of tag v1.2. You can leave out the refs/tags/ part in most cases. Note that ^ must be quoted from DOS-style CMD.EXE (e.g., by doubling it), but in most Unix-style shells, no quoting is required. (The main exception here is tcsh where it's the opening brace that requires quoting.)

    The answers to questions 1 and 2 are intertwined: an annotated tag, which is any tag with annotations or text messages or signatures, is an actual internal Git object, stored in Git's all-objects database. Creating a new annotated tag creates a new object, and a new object must always get a new, never-before-used hash ID.1 To help ensure that this occurs, Git adds your name and email address and the current date-and-time to each such tag object.

    The actual annotated tag object contains the hash ID of the target object, plus the type of the target object. Since annotated tag objects can point to any of the four object types—including other annotated tag objects—Git must resolve the ^{commit} suffix by repeatedly following through tag objects until it arrives at some final ending point: a commit, tree, or blob object. If the final object is not a commit, the ^{commit} suffix causes git rev-parse to produce an error message.

    A simpler syntax:

    <tag-name>^{}
    

    (e.g., v1.2^{}) tells Git to do the same repeated resolving of annotated tags—which Git documentation calls peeling a tag, referring to the idea of peeling an onion one layer at a time—but when it reaches the final target object, it just stops and produces that hash ID, regardless of the object type.


    1By the pigeonhole principle, we know that if the input space is big enough, this is mathematically impossible. Still, it works fine in practice.