The good news is that your commits are still available. You will need to use git reflog
to find them.
The first thing to know is that Git is not about files, and not really about branches either. It's about commits. Each commit holds files, and a collection of commits forms a branch or set of branches, but the central thing in Git is the commit itself.
Every commit has a unique number. This number takes the form of a big ugly random-looking hash ID, such as 51ebf55b9309824346a6589c9f3b130c6f371b8f
. Every commit you make stores a full and complete snapshot of all of your files—well, all of your tracked files anyway—in a read-only, compressed, frozen-for-all-time, Git-only format, that only Git can use. So when you make a commit, Git produces a new random-looking hash ID: a new number, different from every other commit ever. That's why the number has to be so big.
If we did not have a computer to remember them for us, we would have to write down every one of these random-looking hash IDs. But we have a computer, so we have it remember the IDs for us.
Every commit we make remembers the hash ID of some earlier commit. That is, we pick a commit, e.g., by some hash ID, with git checkout
. That's what you did when you got into your detached HEAD state. That commit—the one we have checked out—is the current commit. Then, when you make a new commit, Git makes our new commit and puts into it, along with the snapshot, some metadata: your name and email address, the date-and-time of when you made the commit, your log message, and—very important for Git itself—the hash ID of the commit you had checked out, that was the current commit.
This process of creating the new commit results in a new unique hash ID, and now that commit becomes the current commit. That is, Git writes the new commit's hash ID into HEAD
. Since the current commit contains the ID of the previous commit, Git can find its way, backwards, from here to the previous commit:
... <--previous <--current <--HEAD
A detached HEAD is not the normal way to work, though. Normally, we git checkout branch-name
, e.g., git checkout master
. In this case, the name HEAD
is attached to a branch name. If we use single uppercase letters to stand in for actual hash IDs, and the current end of the master
branch is commit H
, we can draw that like this:
... <-F <-G <-H <--master(HEAD)
That is, Git has saved the hash ID of the last commit in our branch named master
under our name master
. Then, it attached the special name HEAD
to the name master
. From HEAD
, Git can find the name master
, and from there, Git can find the hash ID of commit H
.
Commit H
, meanwhile, contains the hash ID of earlier commit G
, so from H
Git can work backwards to G
. Of course, G
contains the hash ID of earlier commit F
, so Git can work backwards there too, to F
... and F
contains the hash ID of another earlier commit, and so on.
If you git checkout master
, do some work, and then create a new commit, Git creates that new commit with some random-looking hash ID, but we'll just call it I
. Commit I
will point back to commit H
:
...--F--G--H <-- ???
\
I <-- ???
The name master
pointed to H
a moment ago, but because HEAD
is attached to the name master
, Git now writes I
's commit hash ID into the name master
, making master
point to I
:
...--F--G--H
\
I <-- master (HEAD)
How will we find commit H
? Well, the name master
has I
's hash ID written down in it, so we can easily find I
. And commit I
has commit H
's hash ID written inside it—so that's how we'll find H
, by starting with HEAD
to get master
to get I
to find H
.
We don't need the kink in the drawing any more as we don't need a pointer directly pointing to H
. But in fact, we do have several. One is in the reflog for HEAD
, and one is in the reflog for master
. Git keeps a history of every commit that master
has ever named, and every commit that HEAD
has ever named, including through branch names like master
, so at this point we have:
...--F--G--H <-- master@{1}, HEAD@{1}
\
I <-- master (HEAD)
If the name master
pointed to G
earlier, you'll have master@{2}
as well.
When you detach HEAD
—e.g., by git checkout <hash-of-F>
—you get HEAD
pointing directly to the commit, because instead of containing a branch name, HEAD
now just contains a raw commit hash ID. So you have this—I'll draw in HEAD@{1}
but not any of the others:
...--F <-- HEAD
\
G--H--I <-- master, HEAD@{1}
If you now create a new commit, it gets a new unique hash ID which we'll call J
:
J <-- HEAD
/
...--F <-- HEAD@{1}
\
G--H--I <-- master, HEAD@{2}
Note how what was HEAD@{1}
is now HEAD@{2}
; what was HEAD
is now HEAD@{1}
; and HEAD
refers to new commit J
.
Every time you move HEAD
, or attach it to a different branch, all the numbers bump up one to make room for the previous value. Git stores the previous hash ID from HEAD
into the reflog for HEAD
, and writes the appropriate branch name or hash ID into the name HEAD
.
So: run git reflog HEAD
. Note that it spills out both abbreviated hash IDs, and HEAD@{number}
. The hash IDs are the true names of the commits. The commits are what Git is all about; that's what it is keeping. The names with @{1}
, @{2}
, etc., all get renumbered frequently. Note that Git only keeps these old values around for a while—30 to 90 days by default.1 After that, Git figures you probably don't care any more, and starts erasing old reflog entries.
Once you've seen which reflog entry or entries are the commits you want back, give them some name. For instance, you can create a new branch name to refer to the last commit in a chain. If you have done this:
J--K--L <-- HEAD
/
...--F
\
G--H--I <-- master
by making three detached-HEAD commits, and then this:
J--K--L <-- HEAD@{1}
/
...--F
\
G--H--I <-- master (HEAD)
by re-attaching HEAD
to the name master
, you could do this:
git branch lazarus HEAD@{1}
and then you would have:
J--K--L <-- lazarus, HEAD@{1}
/
...--F
\
G--H--I <-- master (HEAD)
The new name lazarus
now points to the resurrected commit. (Perhaps I should have made four of them?)
See more at GIT restore last detached HEAD.
1The 30 day limit is for unreachable commits and the 90 day limit is for reachable commits. The term reachable here is from graph theory, and reachability implies a starting point. The starting point in this case is the current commit hash ID in the ref. If the reflog's saved hash ID is reachable from the ref-name value, the entry has a longer lease on life.
Once the reflog entry is gone, the commit itself has one fewer ways to reach it. Commits must be reachable from some name—some branch name, or tag name, or any other kind of name—or otherwise Git's garbage collector, git gc
, will eventually delete them. So these reflog entries serve to keep the commits themselves alive. While all commits are frozen for all time, commits themselves will be reaped (discarded) if they are unreachable, so once the reflog entry is gone, the commit needs to be reachable from some other reachable commit, or named directly by some other name.