On branch foo
I started a rebase like this: git rebase --interactive HEAD~1
, where I wanted to add changes for last commit in file A
.
I made my changes, git add
them, and then git commit --amend
them. (Please note that I have not issued git rebase --continue
command yet)
Then I switched to branch bar
via git checkout bar
; did nothing there and switched back to foo
via git checkout foo
. When checking file A
, I found that all changes I did during rebase were gone, even though git status
says:
Last command done (1 command done):
e deadbee Nice commit message
Is it possible to get those changes back?
When you start an interactive rebase, Git puts you in "detached HEAD" mode. When you check out a branch name by name, Git puts you in "attached HEAD" mode, i.e., back on a branch. This rather badly disrupts an ongoing rebase, because any new commits you made are now difficult to find.
Lemuel Nabong's answer contains the key (but is wrong): you must re-check-out the appropriate detached-HEAD commit, which you can find using git reflog
. Don't do this with git reset
, do it with git checkout hash
or git checkout HEAD@{number}
, after finding the correct commit in the reflog. You should then be able to continue your rebase.
What detached HEAD means here is that the special file .git/HEAD
(which always exists) no longer contains the name of a branch. Normally HEAD
, or .git/HEAD
, contains a string like ref: refs/heads/master
to indicate that the current branch is the one named master
. The current branch then determines the current commit.
For doing certain kinds of work, though—including interactive rebase—Git changes .git/HEAD
so that it contains instead a raw commit hash ID. The interesting thing about this mode is that you can make new commits, which get new hash IDs that are different from every existing commit. When you do this, those new commits' IDs can only by found by reading .git/HEAD
itself.
A picture, I think, makes this a lot clearer. If we start with a tiny repository with just three commits in it, we can draw them like this, using single uppercase letters to stand in for those horrible hash ID strings like ccdcbd54c4475c2238b310f7113ab3075b5abc9c
. We'll call our first commit A
, our second B
, and our third C
:
A <-B <-C <--master
Commit C
, our latest commit, has its hash ID stored under the name master
. We say that the name master
points to C
. Commit C
itself stores the hash ID of commit B
as its parent, so we say that C
points to B
. Commit B
stores A
's hash ID in turn, so B
points to A
. Commit A
is the very first commit ever made, so it has no parent at all. Git calls this a root commit, and it's where the action stops if we run git log
, for instance, because there's no earlier commits to look at.
Hence, Git always works backwards: a branch name points to the last commit on the branch. The commit itself remembers the previous commit, and so on. If we go to add a new commit to master
, we run:
git checkout master # if needed
... do things to modify files ...
git add file1 file2 ...
git commit
The commit step packages up the latest snapshot (from the index aka staging area, where git add
copied them, but we'll leave that for another topic), then writes out a new commit D
whose parent is the current commit C
:
A <-B <-C <--master
\
D
Finally, having written out the new commit, git commit
writes the new commit's hash ID—whatever it turns out to be; it's not easily predicted—into the name master
so that master
now points to D
:
A <-B <-C
\
D <--master
and the commit is done.
The way that Git knows which branch name to update, if you have more than one branch name, is by attaching HEAD
to it. Suppose that instead of committing D
on master
, we do this:
git checkout master
git checkout -b develop # create new develop branch
Now the drawing looks like this (I'm dropping the internal arrows, we know they always point backwards and they get hard to draw):
A--B--C <-- master, develop (HEAD)
We do our work, git add
, and git commit
, and since HEAD
is attached to develop
rather than master
, Git writes new commit D
's hash ID into develop
rather than master
, giving:
A--B--C <-- master
\
D <-- develop (HEAD)
A detached HEAD just means that instead of having HEAD
attached to some branch name, HEAD
points directly to some commit. If we detached HEAD
now and had it point to commit D
, we could draw this as:
A--B--C <-- master
\
D <-- develop, HEAD
If we now make a new commit E
, we'll get this:
A--B--C <-- master
\
D <-- develop
\
E <-- HEAD
If we now say git checkout master
, this is what happens:
A--B--C <-- master (HEAD)
\
D <-- develop
\
E <-- ???
The way to get back to where we were is to find some name for commit E
(remember, its real name is some big ugly hash ID).
Both rebase and git commit --amend
work by making new commits. The special thing that --amend
does is to make the new commit with its parent being the current commit's parent. If we start with:
A--B--C <-- master
\
D <-- develop (HEAD)
and run git commit --amend
, Git makes a new commit E
whose parent is D
's parent C
, rather than D
itself. Git then writes that into the appropriate name—develop
in this case—giving:
E <-- develop (HEAD)
/
A--B--C <-- master
\
D <-- ??? [abandoned?]
Each branch name has a reflog, recording the commit IDs that the branch name used to point-to. That is, if master
pointed to A
at one time—which it must have—then the reflog for master
includes the hash ID for commit A
. This reflog also includes the hash ID for commit B
. Once master
no longer points directly to C
, the master
reflog will contain the hash id of C
as well, and so on.
There's also a reflog for HEAD
itself, recording the hash IDs that HEAD
has pointed-to, either directly (detached) or indirectly (by being attached to a branch name). So git reflog HEAD
shows you those reflog entries, which allows you to find the actual hash ID for the commit you're looking for.
One downside with reflog entries is that they eventually expire: after 30 to 90 days, Git assumes you don't care any more. That particular down-side won't apply here since the commit you are looking for is fresh. The other (an other?) down-side is that the commits found in the reflog tend to all look alike, and there may be an awful lot of them, so it can be hard to find them in the noise. One thing that helps is to note that they're kept in order: the @{1}
entry is the old value from a moment ago, the @{2}
entry is the one from before that, and so on. So if you only recently switched, the one you want will be in the top few.