Pregunta

I just used check out head after making a commit. I figured that doing this checkout would not actually do anything, but I seem to have been wrong. It put me into a 'detached head' state. I ignored this note, and continued to make a few additional commits. The message change to 'Head detached from ...' Feeling a bit annoyed by this, I looked for a way to fix it. The answer I found was git checkout master. I did this and now my last few commits disappeared. What happened here?

¿Fue útil?

Solución

TL;DR

Use git reflog to find the lost commits. Give them a name—e.g., a branch name—and then consider whether to copy them to new-and-improved commits, perhaps using git cherry-pick or git rebase.

Long

It's not clear to me what you did to get into "detached HEAD" state. (Anything that checks out a commit by any identifier other than a branch-name will do that. For instance, if you give git checkout a tag name, or a remote-trackng name like origin/master, it will detach. Likewise, if you check out a specific commit by its hash-ID, you'll be in that mode. You can also deliberately do it even when checking out a commit by branch-name, using --detach, although I imagine that was not it.)

Note: HEAD is, at least currently, implemented as a plain-text file inside the .git directory. This file normally contains the name of a branch. When it does, Git says that you are "on" that branch: git status will say on branch master or on branch branch, for instance. In detached HEAD mode, the file doesn't have a name in it (see below for more). In this case git status will say HEAD detached at ... or HEAD detached from ... or similar (the exact phrasing varies depending on several items, including your particular Git version).

Here's how I like to draw commit graphs:

...--D--E--F   <-- master
      \
       G     <-- branch (HEAD)

This represents a master branch with a bunch of commits, and a branch named branch with one commit on it (commit G) that is not on master. Here master has two commits that are not on branch (commits E and F): The branch name master "points to" commit F (has commit F's hash-ID in it), commit F points to commit E (by listing the raw hash ID for E as its parent), commit E points to D, and so on. The branch name branch points to commit G, and G points to D. The (HEAD) part says that HEAD is attached, to the name branch.

In this state, if you make a new commit, it's added as usual. If we label the new commit H, commit H's parent-commit is commit G, and branch is updated to point to commit H, giving:

...--D--E--F   <-- master
      \
       G--H   <-- branch (HEAD)

When you are in this "detached HEAD" mode, on the other hand, HEAD does not contain the name of a branch. Instead, it contains a raw hash-ID, identifying the "current commit". Let's say you decide to take a look at commit E above:

$ git checkout master^      # some windows users may have to write ^^

master^ identifies commit E, and is not a branch name because it has that ^ character in it, so this gets you on to commit E (and I have to use another row to raise F so that I can draw in the arrow for HEAD):

          F   <-- master
         /
...--D--E     <-- HEAD
      \
       G--H   <-- branch

Now, if you are in this state and add new commits, they are added in the same way as always—but there is no branch to update, so Git writes each new commit's ID directly into HEAD. Let's say we now create commit I:

          F   <-- master
         /
...--D--E--I   <-- HEAD
      \
       G--H   <-- branch

Commit I has commit E as its parent, and HEAD now points to commit I. However, if you decide you want to go look at branch again:

$ git checkout branch

how will you now locate commit I? The only name it had was HEAD, and after the above checkout, the picture now looks like this:

          F   <-- master
         /
...--D--E--I
      \
       G--H   <-- branch (HEAD)

(Note: if you made more than one commit while in "detached HEAD" state, they might be commits I-J-K for instance. This doesn't change any of what you need to do below, except to make it a bit more work to be sure you have the last commit, K. I'll just use the one commit I below.)

There's no label letting you find commit I. You can't find it from commit E: E's parent is still only D, and E does not list its children. You cannot find it from commits F, G, or H either; none of them are even related. Commit I is "abandoned", and in about a month it will truly go away.

However, as Nikhil Gupta noted, there is still a way to find commit I (until it is collected in about a month): it's stored in what git calls the "reflog". (Or more precisely, "a" reflog: there's one reflog for each branch, plus one big one for HEAD.) In fact, it's the reflog itself that keeps commit I around for about-a-month. Abandoned repository objects are still name-able through reflog references, but those reflog entries expire. Once the reflog entry for commit I expires, if you have not attached a more-permanent link to it, git's garbage-collection process will truly delete it.

So, to get your commits back, use git reflog to view the HEAD reflog. This will show you things like 54ce513 HEAD@{3}: commit: foo the bar. You can supply either HEAD@{3} or the abbreviated hash-ID, 54ce513,1 to various git commands, including git log and git show.

Once you have a desired hash-ID (or name like HEAD@{3}), you can attach a name—a tag or branch name—to it:

$ git tag get-it-back 54ce513  # or git tag oops HEAD@{3}

or:

$ git branch experiment 54ce513

and now you can refer to commit I with the name get-it-back or experiment. If you make it a branch name, you have a regular ordinary branch now:

          F   <-- master
         /
...--D--E--I   <-- experiment
      \
       G--H   <-- branch (HEAD)

Once it has a convenient name, you can do whatever you want with it. Or you can (for the ~30 days it sticks around) just refer to it by reflog-name or raw hash-ID. For instance, you could copy the changes in commit I onto a new commit on branch branch (where HEAD is still pointing):

$ git cherry-pick 54ce513

(cherry-pick basically means "find out what I did in that commit, and do it again on the current branch"). Assuming you did not attach a name to commit I, this would give you:

          F   <-- master
         /
...--D--E--I
      \
       G--H--J <-- branch (HEAD)

where the difference in moving from commit H to commit J is the same2 as the diff from E to I.


1The string HEAD@{3} is a "relative reference": "where was HEAD 3 changes ago". A hash-ID like 54ce513 is "absolute": it never changes, and is (with the rest of the full ID—this one is abbreviated) the "true name" of the commit. Since HEAD changes every time you do a commit or checkout, if you do git checkout HEAD@{3}, it becomes HEAD@{4}—although of course HEAD@{0} now also contains 54ce513. If you git checkout 54ce513, that always works—assuming, of course, that 54ce513 is the actual hash-ID (I made this particular one up).

2More specifically, git diff between those commits shows the same changes, with one exception: because cherry-pick uses Git's merge machinery, Git can sometimes tell that you already have some of the changes, and avoid duplicating them.

Otros consejos

A detached head is perfectly normal and means that the copy you have point directly to the commit instead of a symbolic-ref in the branch. You can see more details on this here.

How do you come out of your situation? Well, use git reflog to see the stuff you did before git checkout master and then use git merge <sha1> for the commit that you want to bring back

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top