Search code examples
gitgithubgit-checkoutgit-log

Detached HEAD on same commit as main?


I'm not really sure how I got my repository in this situation, and it's not even a problem anymore, but I'd like to learn about what happened so I don't feel so lost next time.

When doing a pull, I was warned I was in detached HEAD state. git status showed the following (screenshot):

HEAD detached at 44422b7

So I ran git log to find out how far back from main I was, and it showed (screenshot):

commit 44422b74b6826291479ee7a17fe18bb4acca6355 (HEAD, main)

It didn't make much sense to me, as I seemed to be in the same commit as the main branch but, eager to solve the problem, I ran git checkout main, and then the next git log showed, instead (screenshot),

commit 44422b74b6826291479ee7a17fe18bb4acca6355 (HEAD -> main)

and all started working as I initially expected.

So I'm left trying to understand the difference between what git log showed on the first and second time I ran it (the one with the arrow vs. the one with just a comma), and maybe understand how I got to that situation in the first place.

Thanks for any help I can get.


Solution

  • These are really two separate questions:

    1. trying to understand the difference between what git log showed on the first and second time I ran it (the one with the arrow vs. the one with just a comma),

    2. understand how I got to that situation in the first place.

    The answer to question 2 is in Why did my Git repo enter a detached HEAD state?

    The answer to question 1 is simpler: the first one shows the detached-HEAD state and the second, with the arrow in it, shows the attached-HEAD state. But we can expound on this a bit: in particular, the whole concept of "detached HEAD" in the first place is a bit peculiar. Before we can describe it, though, we have to know what an attached HEAD is! In one way it's obvious—it's the opposite of a detached HEAD—but that's just circular reasoning. We need something more basic, and this lies in how Git finds commits.

    First, remember that every Git commit has a unique hash ID:

    commit 44422b74b6826291479ee7a17fe18bb4acca6355
    

    That big ugly string of letters-and-digits is a number, expressed in hexadecimal. (In decimal it's 389687007142770230305517186390235637298437448533, which isn't really any better. 😀 In fact, Git has to have it in hexadecimal, or in binary, because of the way it looks these up by prefix in its database, which in part uses tries of various power-of-two sizes.) This hash ID is used as a key in a simple key-value database; the value is the internal Git representation of the commit.

    So Git needs this commit hash ID to find this commit. You must give Git the hash ID: that's the only way Git has, for looking up the commit. But who wants to type in 44422b74b6826291479ee7a17fe18bb4acca6355 over and over again? Would you even get it right? What if you could just say main and Git could look up main—or, technically, refs/heads/main—in a secondary key-value store and find 44422b74b6826291479ee7a17fe18bb4acca6355? Wouldn't that be nicer?

    Yes, it is in fact nicer, and that's what we do: we type in main or v1.2 or origin/develop, and Git looks that up in this secondary database to get the raw hash ID. From there, Git finds the commit we want.

    When you're "on a branch", as git status says about it, what this means is that every new commit you make will update the current branch name. So if you're on branch main and you make a new commit and its hash ID is a123456... or whatever, Git will store the new commit's hash ID, a123456... or whatever, under the name main. (The old commit-hash-ID—44422b74b6826291479ee7a17fe18bb4acca6355—goes into the new commit, so that it's now the second-to-last commit.)

    What all this means, in turn, is that Git needs to know, at all times, which branch name is the name it should update whenever you make a new commit. To remember that branch name, Git "attaches" the special name HEAD to the branch name.

    (The current implementation of this is that Git writes ref: refs/heads/main into .git/HEAD. However, this implementation could change in the future. The current implementation is already different from the previous one, and has already changed somewhat to handle added working trees, as now it's sometimes not .git/HEAD at all: that's just for the main working tree.)

    So that's an attached HEAD, and therefore ...

    An attached HEAD is when HEAD contains the name of a branch. The branch name contained in HEAD is the current branch. The branch name itself, in the database full of name-to-hash-ID mappings, contains the hash ID of the current commit. So HEAD provides two pieces of information:

    • the current branch name, and
    • the current commit hash ID (indirectly).

    To detach HEAD, Git simply writes the raw hash ID of some commit—any existing commit—into HEAD. Now that .git/HEAD contains a raw hash ID, there is no current branch name, but there's still a current commit hash ID. It's just that HEAD now holds it directly. There's no need to look up the name in the names database.

    Everything else you do still works the same as before: in particular, if you create a new commit, the new commit connects (backwards, as commits do) to the commit that was current when you made the new one; and now the new commit is the current commit, i.e., Git has stored its hash ID in HEAD.

    If you do make such a commit and run git log, you'll see:

    commit a123456... (HEAD)
    [your new commit]
    
    commit 44422b74b6826291479ee7a17fe18bb4acca6355 (main)
    [your old commit]
    

    since git log works by starting at the current commit and working backwards, one commit at a time. But since you didn't make a new one, both the name HEAD and the name main mean "commit 44422b74b6826291479ee7a17fe18bb4acca6355", so you get:

    commit 44422b74b6826291479ee7a17fe18bb4acca6355 (HEAD, main)
    

    When HEAD is attached, Git prints it with that forward-arrow notation (since Git 2.10: prior to 2.10.0, Git printed HEAD, main for both cases).