Reputation: 11279
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
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.
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
?
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.
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.
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 HEADIf 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.
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
).
git checkout
command can do most of this tooWhen 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
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