Home > Blockchain >  Git failed to create a branch on a tag
Git failed to create a branch on a tag

Time:10-22

I have below branches:

xxx@box:~/src$ git branch
  jira_6500
* main
xxx@box:~/src$ git rev-parse main
bfd271932228f8ce33b68b82ffee5ee3b2386a17
xxx@box:~/src$ git rev-parse jira_6500
bfd271932228f8ce33b68b82ffee5ee3b2386a17
xxx@box:~/src$

I try to create a new branch from a tag v2.6.0-rc3 as below:

xxx@box:~/src$ git rev-parse v2.6.0-rc3
ff8db8992102ca7ce76f55169d06173c888c9447

xxx@box:~/src$ git checkout -b test001 v2.6.0-rc3
Switched to a new branch 'test001'
xxx@box:~/src$ git branch
  jira_6500
  main
* test001

Then I check the rev hash of the new branch. I expected to be the same as the tag v2.6.0-rc3. But it is not. It is the same as the jira_6500 branch.

xxx@box:~/src$ git rev-parse test001
bfd271932228f8ce33b68b82ffee5ee3b2386a17

I did the same as below thread. And I remember I did this before.

How could the rev hash be wrong?

How to create a new branch from a tag?

CodePudding user response:

Your branch creation is working the way you will want it to. The reason for what you're seeing has to do with the internals of Git's tags, which are a little bit peculiar.

Git is, in its little gitty heart, all about commits, which are numbered by hash IDs, normally expressed in hexadecimal: bfd271932228f8ce33b68b82ffee5ee3b2386a17, for instance.

To make commits work, Git needs two more internal supporting objects, which Git calls trees and blobs. These also have hash IDs. You don't normally see these hash IDs: they don't "leak out" very much. (Blob hash IDs do show up in index: lines in git diff output, though, and you can find tree hashes if you look for them: none of these are hidden. They just don't get all up in your face the way commit hash IDs do.)

Tags, in Git, tag a commit—but you have a choice here: a lightweight tag holds a commit hash ID directly, so if you have commit bfd27..., you can create a lightweight tag that stores that hash ID. If you'd like to store more information, though, Git has a supporting object called a tag object or annotated tag object. We have Git create one of these objects, storing the extra data—such as a PGP signature or whatever—and that object gets its own unique hash ID, such as ff8db8992102ca7ce76f55169d06173c888c9447.

The tag object itself stores, along with the annotation data, the commit hash ID, bfd271932228f8ce33b68b82ffee5ee3b2386a17. Since these hash IDs each uniquely identify the corresponding object, Git can use the tag ID ff8db... to find the commit object, by reading the tag object and finding the stored commit hash ID. (It's not possible to go the other way: commit bfd27... is set in stone before we create any tags that point to it, and as a result we can't add those tag IDs to the commit later. So, as usual with Git, we have to work backwards, from newer objects to older ones.)

Using git rev-parse v2.6.0-rc3, you get the hash ID of the annotated tag object. From here Git can find the commit. Tag names may point directly to a commit—again, this makes it a lightweight tag—or to a tag object, making the tag name an annotated tag. Git can find the commit either way.

Branch names, unlike tag names, are constrained: they may only contain the hash ID of some (existing) commit. So when creating a new branch name, if you give Git the hash ID of an annotated tag object, or a name whose resolution is an annotated tag object, Git goes on to follow the annotated tag object to its target, which needs to be a commit.1

So that's precisely what you're seeing here. Creating the branch name follows the tag to the tagged commit. Other branch names also point to this same commit—that's fine and normal. When you get "on" one of these branch names, using git checkout or git switch, and make a new commit, Git will make the new commit as usual, and as the last step of git commit, will write the new commit's hash ID into the current branch name, causing the branch to advance.

Checking out the tag, with git checkout v2.6.0-rc3 or git switch --detach v2.6.0-rc3, will put Git into detached HEAD mode, where HEAD contains the raw hash ID of the commit. In this case, making a new commit stores the new commit's hash ID directly in the special name HEAD, rather than in any branch name. This means that re-attaching HEAD—which overwrites the HEAD storage slot with a branch name instead of a commit hash ID—"loses" the new commit(s), which is why you normally don't do new work in detached-HEAD mode.2

There's one last thing to mention here, which is that git rev-parse has a bunch of syntactic tricks for dealing with this. They are all covered in the gitrevisions documentation, but a quick overview of the relevant ones is useful here:

  • git rev-parse v2.6.0-rc3 just gets you the ID of whatever v2.6.0-rc3 resolves to: in this case, refs/tags/v2.6.0-rc3 resolves to an annotated tag.

  • git rev-parse v2.6.0-rc3^{commit} finds the commit associated with v2.6.0-rc3: that is, if this is a tag, it peels the tag and demands that the result be a commit.

  • git rev-parse v2.6.0-rc3^{tree} finds the tree associated with v2.6.0-rc3: that is, if this is a tag, it peels the tag; if this is now a commit, it finds the top-level tree stored in that commit; it demands that the final result be the hash ID of a tree.

  • git rev-parse v2.6.0-rc3^{} finds the hash ID associated with v2.6.0-rc3, and if it is a tag, peels the tag (and then stops successfully and produces the hash ID, regardless of the type of the object found).

In this case, git branch test001 v2.6.0-rc3 or git checkout -b test001 v2.6.0-rc3 has the same effect internally as you'd get using v2.6.0-rc3^{commit} with git rev-parse.

These syntax tricks work with most Git commands: wherever some hash ID might be required, you can use a name, and whatever name you supply goes through the same process git rev-parse uses to turn it into a hash ID.


1Annotated tags can be made to point directly tree or blob objects. If you do this, you cannot use them to create a new branch name. Annotated tag objects may also contain, as their target hash ID, the hash ID of another annotated tag object; in this case, Git continues indirecting until it finds the final object. This repeated indirection is called peeling tags, with the concept taken from the idea of peeling an onion, layer by layer, until you find out what is inside. Of course, in the case of an onion, when you've peeled away all the layers, there is nothing left but the smell.

  • Related