Reputation: 328
I was going through some git internals and came across tags and how they're stored internally. In the Git SCM site I found the following:
Checking out Tags
You can’t really checkout a tag in Git, since they can’t be moved around. If you want to put a version of your repository in your working directory that looks like a specific tag, you can create a new branch at a specific tag with git checkout -b [branchname] [tagname]:
git checkout -b version2 v2.0.0
Switched to a new branch 'version2'
Of course if you do this and do a commit, your version2 branch will be slightly different than your v2.0.0 tag since it will move forward with your new changes, so do be careful.
I have the following doubts:
HEAD
point to the commit pointed to by the tag? Thank you.
EDIT:
I'm referring to cases where HEAD and the tag point to different commits.
Upvotes: 4
Views: 2338
Reputation: 488183
Why can't we check out tags?
We can!
Can't we traverse the Git TAG to make HEAD point to the commit pointed to by the tag?
Yes, we can do just that (though, strictly speaking, we don't even need much if any traversal: commits are snapshots, so we're done once we reach the right one, and tags generally require either zero or one intermediate steps to reach the commit).
The more typical state for the HEAD
file—which is an actual file in .git
named HEAD
—is that it contains the name of a branch, rather than the ID of a commit. This mode does not have much of a formal name, but I call it "being on a branch", to contrast it with the other mode.
The mode we get when .git/HEAD
points directly to some commit (by containing its hash ID) is, as CodeWizard mentioned in a comment, called a "detached HEAD". Since .git/HEAD
no longer contains the name of a branch, we are now on no branch—or, equivalently, on some sort of unnamed branch, where we can use the @
symbol (since Git version 1.8.5) to name it, or by spelling out the word HEAD
. Of course HEAD
is a fine name; it's just that Git will overwrite the contents of the .git/HEAD
file as soon as we run git checkout master
again, for instance, and then we will no longer have the hash ID stored anywhere.
Can we create a new commit which will then point to the commit pointed to by tag as the parent?
English is a bit ambiguous here, but I think a diagram makes it clearer that the answer is "yes":
initially: on branch `master` with commit `H` as the current commit
...--F--G <-- tag: v2.0.0
\
H <-- master (HEAD)
Here, commit H
points back to commit G
; commit G
points to commit F
; and so on. The branch named master
points to commit H
, and the tag name v2.0.0
points to commit G
(either directly, if it's a lightweight tag, or through one annotated tag object, if it's an annotated tag).
After `git checkout v2.0.0`:
...--F--G <-- HEAD, tag: v2.0.0
\
H <-- master
The .git/HEAD
file now contains the hash ID of commit G
.
If we make a new commit now:
I <-- HEAD
/
...--F--G <-- tag: v2.0.0
\
H <-- master
New commit I
has commit G
as its parent, as you suggested. The .git/HEAD
file now contains the hash ID of commit I
.
It is said that if we create a new branch from the tag and commit, it'll be slightly different. What difference are they talking about? Won't the new commit (assuming nothing has changed) refer to the tagged commit as its parent with everything else (trees & blobs) remaining the same?
The commit contents would be identical, but the drawing of the commit graph would change a bit. Here's the new graph drawings, starting from the same starting point, but using git checkout -b version2 v2.0.0
as the middle step.
initially: on branch `master` with commit `H` as the current commit
...--F--G <-- tag: v2.0.0
\
H <-- master (HEAD)
After `git checkout -b version2 v2.0.0`:
...--F--G <-- version2 (HEAD), tag: v2.0.0
\
H <-- master
If we make a new commit now:
I <-- version2 (HEAD)
/
...--F--G <-- tag: v2.0.0
\
H <-- master
Note that in this case—the one where we are "on a branch" named version2
—the branch name moves along with us as we make new commits. When we are not on any branch, in the "detached HEAD" mode, new commits just update (the commit hash ID stored in) .git/HEAD
. But when .git/HEAD
contains the name of a branch, those new commits update that branch. The .git/HEAD
file itself just continues to contain the name of the branch.
This, in fact, is what it means to be "on a branch": whenever .git/HEAD
contains the name of the branch, various operations—specifically including making new commits and using git reset
—change the hash ID the branch name remembers, rather than changing .git/HEAD
itself. Whenever .git/HEAD
contains the hash ID of a commit, these same operations just update .git/HEAD
itself.
The git checkout
command usually updates the contents of .git/HEAD
, and is one of the few commands that can change the branch name stored in .git/HEAD
. (I say "usually" here because git checkout
is kind of a kitchen-sink command, having various modes that do oddball things just because they're closely related to the main thing that it usually does.) Specifically, if you are on a branch, git checkout
can put you on a different branch; if you're not on any branch, git checkout
can get you onto one; if you use --detach
or a tag name or similar, git checkout
can put you into detached HEAD mode; and if you're not on any branch, git checkout
can change which commit HEAD
points to.
(The git reset
command can do some of these things, but it won't take you on or off any branch. Like git checkout
, it's a bit "kitchen sinky": it has many things it can do and it is hard to describe these well without many words and several diagrams!)
Upvotes: 4