TL;DR
You have a post-checkout hook that is causing git rebase
to fail (or stop without finishing, anyway). You need to fix that, or use git rebase --continue
or git rebase --abort
if rebase itself recorded the in-progress rebase.
(It's clearly a Git bug that a post-checkout hook causes this problem at all, but precisely what the bug is—i.e., what Git should be doing: whether it should be using the post-checkout hook at all—is still not settled on the Git mailing list.)
Long (and mostly tangential as it turns out)
Start with this: the git pull
command is meant to be a convenience. It runs git fetch
, which obtains new commits from some other Git repository. Then it runs a second Git command, because just obtaining those new commits doesn't do anything with the new commits. You have them, but none of them are in any of your branches. Your Git repository has its own branch names, quite independent of all other Git repositories, even the one you cloned to make your Git repository, and git fetch
did not touch any of your branches.
You get to choose which second command git pull
will run. The default is to run git merge
.1 You can choose to have git pull
run git rebase
instead. How do you know which one to use? That's up to you,2 but if you choose git rebase
, you now know that you need to know what git rebase
will do.
1There is a rare case where git pull
just runs git checkout
, but in this rare case it doesn't matter which second command you chose.
2Often, my answer is I don't know which one, if either, to use and therefore I tend not to use git pull
at all. I like to run git fetch
, see what I got, and only then decide whether to run a second Git command at all, and if so, which one to use. But everyone has a different way of working; Git is a tool-set, not a solution.
When git rebase
fails
When the git rebase
works, all is good. But when git rebase
fails, you are left in this "detached HEAD" mode.
The word fails might be too strong here. Rebase, in Git, is the equivalent of doing a series of git cherry-pick
operations, one for each commit that rebase copies, and—just like git merge
—a cherry-pick can have a merge conflict. So each of these copies can result in a merge conflict, just like if you have git pull
run git merge
, or if you run git merge
yourself.
What you have to do in this situation is resolve the conflict, then resume the operation (merge or rebase or cherry-pick—whatever it was that stopped). That still doesn't explain the detached-HEAD though. What does explain the detached HEAD is how rebase itself works.
How rebase works, in a nutshell
Git is all about commits. It's not about branches, though branch names help you (and Git) find commits, and it's not about files, though commits contain files. It's all about the commits.
Each commit has a snapshot of all the files that Git knew about, at the time you (or whoever) made the commit. Each commit has a hash ID: that's how Git actually finds the commits. The branch name, if there is one, is just there to find the hash ID; the hash ID finds the commit. And, each commit stores the hash ID of some previous commit.3 In our normal cases, that's the hash ID of the previous commit, which is how Git stores history: as a series of commits, linked up to each other, but backwards.
This trick where each commit stores the hash ID of its parent commit means that we can draw the commits like this:
... <-F <-G <-H
Here H
stands in for the actual hash ID of the last commit in the chain. Commit H
holds a snapshot, and also holds the hash ID of its parent commit G
. That lets Git find commit G
. Commit G
holds a snapshot, and the hash ID of its parent F
. That lets Git find F
. Commit F
holds ... well, you should get the idea by this point. 😀
We still have to know the hash ID of the last commit, though. Would you like to memorize the big ugly hash ID of commit H
? I wouldn't. But we can just have the computer remember it for us, using a branch name:
...--F--G--H <-- branch
The name branch
now remembers the hash ID of H
for us.
When you have multiple branch names, each branch na
3Technically, each commit stores a list of hash IDs. This list can be completely empty, or can be more than one hash ID, but the most common case is a one-entry list. That's a normal everyday commit. A two-entry list is a merge commit. A three-or-more-entry list is also a merge commit, but it's mainly for showing off. 😀 You don't normally find these in most repositories, except for with git stash
, which makes weird merge commits that aren't regular merge commits. And the very first commit anyone makes in a new, empty repository has that empty list: it's the root commit.
When you have more than one name, there may be more than one "last commit"
Look at what happens if you have two names. They could point to the same commit:
...--F--G--H <-- branch1, branch2
In this situation, it doesn't matter which name we pick: we'll get commit H
either way. We still have to pick one, though. The one we pick, that's where we'll attach the special name HEAD
. Git will now know that that name is the name to use:
...--F--G--H <-- branch1 (HEAD), branch2
This is the normal attached HEAD situation.
In this situation, when we make a new commit, we get a new commit whose parent in the current commit:
I
/
...--F--G--H
What Git does right after it makes this new commit, though, is to write the hash ID of the new commit—it's a new unique hash ID—into the current branch name. So branch2
still points to H
, but now branch1
points to I
:
I <-- branch1 (HEAD)
/
...--F--G--H <-- branch2
Note that HEAD
is still attached to the name branch1
.
If we now git checkout branch2
, Git switches back to commit H
. The files we saved in commit I
are all safe there in commit I
; but now we have the files that were saved earlier in commit H
, and our HEAD
is attached to the name branch2
:
I <-- branch1
/
...--F--G--H <-- branch2 (HEAD)
Let's make a new commit now, and call it J
, to get this:
I <-- branch1
/
...--F--G--H
\
J <-- branch2 (HEAD)
Now let's go back to branch1
so that we're working with commit I
:
I <-- branch1 (HEAD)
/
...--F--G--H
\
J <-- branch2
Note that no part of any commit has ever changed, in all of this. The existing commits don't change at all. We have Git copy the files out of the commit, so that we can see them and work on them. We make new commits. But none of the existing commits change. They literally can't change.
"Moving" a commit
But what if we decide that it would be nice if commit I
came after commit J
? We'd like commit I
to change. It can't, but we'd like it to.
So, instead of changing I
, let's copy I
to a new and improved commit, that's like I
, but comes after J
. To do this, we'll use git cherry-pick
, which copies one commit. We'll start with git checkout branch2
to get back on branch2
. Then we'll create a new branch name, temp
for temporary:
I <-- branch1
/
...--F--G--H
\
J <-- branch2, temp (HEAD)
Now we run git cherry-pick
. It needs the hash ID of commit I
, for which we can find the hash ID, or just use the branch name—Git will find the commit hash ID from the name. Either way, git cherry-pick
does its magic, which involves using Git's merge code, and makes a copy of commit I
. Let's call the new commit I'
.
Like any new commit, writing I'
involves updating the branch name. The branch name that gets updated is our temporary name, temp
, because that's where HEAD
is attached. Here it is:
I <-- branch1
/
...--F--G--H
\
J <-- branch2
\
I' <-- temp (HEAD)
Now, we've already seen that while commits can't change, branch name can and do move all the time. So let's yank the name branch1
off commit I
and make it point to I'
:
I ???
/
...--F--G--H
\
J <-- branch2
\
I' <-- branch1, temp (HEAD)
Now let's attach HEAD
to the name branch1
, using git checkout branch1
or similar, and then delete the temporary name entirely:
I ???
/
...--F--G--H
\
J <-- branch2
\
I' <-- branch1 (HEAD)
What happened to commit I
? Nothing: it's still in there. You just can't see it any more. Commit I'
is the one at the end of branch1
now.
It looks like we changed I
, but we didn't. We copied it.
Rebase is this, automated
This same process works even if there are multiple commits:
A--B--C <-- feature (HEAD)
/
...--o--*--o--o <-- main
can become:
A--B--C ???
/
...--o--*--o--o <-- main
\
A'-B'-C' <-- feature (HEAD)
We just do a git checkout feature
followed by a git rebase main
. The rebase command automatically:
- checks out the target commit, in this case, the tip of
main
;
- copies the commits that need copying, one at a time, using or as if by using
git cherry-pick
; and then
- moves the branch name at the end of the copying.
Rebase internally uses "detached HEAD" mode
When we did our manual one-commit-copy process, we made a temporary branch name, temp
. We did a git checkout temp
to attach HEAD
there. Rebase doesn't bother with a temporary branch name, though.
Git has this detached HEAD mode in which HEAD
just points directly to some commit. This looks like this:
A--B--C <-- feature
/
...--o--*--o--o <-- main
\
A' <-- HEAD
Here, we've successfully copied A
to A'
. Then we stopped, for some reason.
The git cherry-pick
command can stop in the middle for some reason: because of a merge conflict. A merge conflict can happen with any merge, including the internal weird merge for a cherry-pick. Since git rebase
uses git cherry-pick
, rebase can also stop in the middle.
If it does stop, your job is to fix the conflict and run git rebase --continue
. Or, if you decide the rebase is a bad idea, you can run git rebase --abort
; then Git will give up on the rebase and just go back to an attached HEAD
, abandoning any effort so far. In this case that would produce:
A--B--C <-- feature (HEAD)
/
...--o--*--o--o <-- main
\
A' ???
and it would look like you never started the rebase.
Since git pull
runs git rebase
...
Since git pull
actually runs git rebase
, it can stop just like this, with an in-progress rebase that has suspended in the middle of a commit copy. Your job is to finish the merge and resume the rebase.
git status