H2ONaCl
H2ONaCl

Reputation: 11279

what happens eventually to what Git calls a detached head

I wanted to revert to an earlier commit via SmartGit. Via SmartGit I picked an earlier commit and did a check out. I was prompted for a branch name. I opted not to create a branch because it seemed unnecessary, even silly, to create a branch when all that I wanted to do was to "move back in time" on an existing branch. This resulted in a detached head. It seemed a bad idea to continue development on a detached head so I did not continue.

I switched to the command line and did a git log to identify a the hash code for the earlier commit I am interested in. I did a git reset --hard 0dfc994b23ea.

Now git log appears to be from an earlier time, which is what I want. Likewise SmartGit looks okay. In neither git log nor SmartGit (at least superficially) does there appear to be any sign of my mistake (the detached head). What will happen to the detached head?

Upvotes: 4

Views: 593

Answers (2)

torek
torek

Reputation: 489293

It's not really a mistake, and everything is still OK here. Your HEAD is still detached, it's just detached at a different commit.

It's generally wiser to use git checkout to switch to the commit you want, though. To see why, read on.

Long description

The key to understand this is multi-part. First, Git is mostly concerned with commits. Commits, as you have seen, have big ugly hash IDs, 0dfc... and the like, which are hard for humans to use, but work fine for Git. So these hash IDs are the "true names" for each commit.

Moreover, each commit records a parent commit, by its hash ID. This parent commit is the commit that came before it. Some commits—merges—record more than one parent, and at least one commit in the repository has no parent because it was the very first commit ever made, so it can't possibly record any earlier commit's ID: there weren't any earlier commits.

What all this means is that we can draw a graph of the commits, using their hash IDs—or using single uppercase letters to stand in for the big ugly hash IDs, as long as we don't mind running out after just 26 commits:

A <-B <-C

This represents a tiny repository with just three commits. The last commit, C, has some big ugly hash ID. Commit C records the hash ID of its parent B, and B records the hash ID of A. A is the first commit, so it has no parent—Git calls it a root commit—and it ends the chain. We say that C is the child of B and points to B, and B points to A. Git starts with C and works backwards, following these pointers. But how does Git know to start with C?

Branch names point to commits

Git needs some way to find the hash ID of commit C, and this is where branch names come in. We pick out a human-readable name like master and use it to store the actual hash ID of commit C, giving:

A--B--C   <--master

We can stop drawing the internal arrows because (1) no commit, once made, can ever change and (2) they're all necessarily backwards since the child commit doesn't exist when the parent gets made, but the parent does exist when the child gets made. We still need the master arrow because now we can see how Git adds a new commit to the repository.

Adding commits changes branch names

If we check out master and do some work and run git add and then git commit, Git will build a new commit—let's call it D—and give D the hash ID of C as D's parent:

A--B--C   <-- master
       \
        D

Now that D exists and has acquired its new, unique hash ID, Git simply writes D's ID into the name master, so that master now points to D instead of C:

A--B--C
       \
        D   <-- master

and now we have a new commit. Let's straighten out the chain, and add another branch name pointing to commit D too:

A--B--C--D   <-- develop, master

Now let's make a new commit E. This works just like before:

A--B--C--D   <-- develop, master
          \
           E

Git now needs to write the hash ID of E into one of the two branch names, to update it. But which one? This is where HEAD comes in.

HEAD is normally attached to a branch name

Let's draw this again, but attach HEAD to develop:

A--B--C--D   <-- develop (HEAD), master
          \
           E

Now Git knows which name to update: it's the one that HEAD is attached to, i.e., develop. So Git writes the new ID into develop, giving us:

A--B--C--D   <-- master
          \
           E   <-- develop (HEAD)

and now Git knows that to work with develop it should use commit E, but to work with master it should use commit D.

Note that with this kind of attached HEAD, HEAD really just contains the name of the branch. We'll get to the detached HEAD case later.

git reset moves your HEAD

If we're still on develop and run git reset <hash-of-C>, this is what happens:

A--B--C   <-- develop (HEAD)
       \
        D   <-- master
         \
          E   <-- ???

That is, after the git reset, the current branch name develop now refers to commit C, not commit E. The name master (which does not have HEAD attached to it) does not move. Commit E is now rather lost, as we have no name for it, and finding it may be difficult.

The git reset command can do more than just move your HEAD, and has modes of operation in which it doesn't move your HEAD, so this can be pretty confusing, but when used as git reset --hard it always moves HEAD, even if the place to which it moves it is the current commit. For instance, if we put develop back to pointing to commit E:

A--B--C--D   <-- master
          \
           E   <-- develop (HEAD)

and run git reset --hard HEAD, we move our HEAD—i.e., the name develop—from E to E, which leaves it in place. The other effects of git reset apply, and presumably that's why we did this git reset, since the movement of our HEAD was movement-free.

You are now ready to understand a detached HEAD

A detached HEAD simply means that Git has changed HEAD so that instead of containing the name of a branch, it contains a raw commit hash ID directly. We can draw this like so:

A--B--C--D   <-- master
          \
           E   <-- develop, HEAD

Now we can git reset --hard to move to commit C (along with doing the other things that git reset can do), which moves HEAD without moving master or develop, to give us this:

A--B--C   <-- HEAD
       \
        D   <-- master
         \
          E   <-- develop

Any other git reset --hard we do while HEAD is detached will simply move HEAD (plus do the rest of what we want with git reset --hard).

The git checkout command can do most of this too

When you run git checkout master or git checkout develop, what you are asking Git to do is two-fold:

  • Switch commits: use the name, master or develop, to locate the commit to check out, and extract that commit so that we can look at it and/or work on it.

  • Change the name to which our HEAD is attached: use the name, master or develop, to select the current branch. Our HEAD is now attached to that branch, so that additional commits move the branch automatically, as we saw above.

When you give git checkout something that's not a branch name, but that does identify a commit, you are asking Git to:

  • Switch to the given commit. This works the same as for a named branch.

  • Detach HEAD, if it was attached. Write the target commit's hash ID directly into the name HEAD. If your HEAD is already detached, it remains detached, and you simply switch commits.

Besides the fact that git reset will change a name if HEAD is attached, there are some other differences between using git checkout and git reset to move from one commit to another. In particular, git checkout attempts to make sure that no in-progress work is ever destroyed, but git reset --hard tells Git: any in-progress work is worthless; if switching commits requires destroying it, go ahead and destroy that work.

In this sense, then, git checkout is much safer than git reset --hard.

Upvotes: 4

eftshift0
eftshift0

Reputation: 30277

Let me explain briefly. detached HEAD means that your working tree is "pointing" to the revision you asked git to checkout, and from here you can work "as usual". You can commit stuff, merge stuff, whatever. The only thing is that git won't move any "branch" (a.k.a. revision pointer) because you didn't checkout a "branch" but a revision (same thing could be achieved if you asked to checkout a branch with --detach). Detached HEAD is extremely useful if you want to do something quickly and don't want a real pointer to it (say.... do a quick test and the go back to where you were working before. What's the point of creating a branch for just going back in time, then go back to where you were working before and then delete the branch you created "temporarily" just to be able to checkout?). Long story short: You can move to anywhere else you like from your "detached HEAD" position and it won't make a difference.

As a side comment... what does git reset --hard do when you provide a range of revisions? Because I have only used it providing a single revision. Just checked git help reset and I don't see it.

Upvotes: 0

Related Questions