Search code examples
macosgitversion-controlgit-reflog

Git: Deleted current branch and lost reflog


I ran into an unusual git issue earlier that I've since resolved, but I'm still curious as to why it happened.

The problem occurred when I accidentally deleted the branch I was currently working on. Normally git wouldn't allow this, but due to case-insensitivity on OSX I got myself into a situation where I thought I had two branches, one named feature/ONE and another named feature/one. Thinking these were two separate branches (coming from a mostly linux/case-sensitive background) and that I was working on feature/ONE I attempted to delete feature/one using git branch -D.

I quickly noticed what I had done, tried to retrieve my lost work from git reflog, which gave me the error fatal: bad default revision 'HEAD'. I attempted to get back into a normal state using git checkout -f develop, which worked. However, when I looked at git reflog after this unfortunately it only had one entry stating checkout: moving from feature/ONE to develop. No previous actions appeared in the log.

I've compiled some steps to replicate this kind of scenario (presumably this is only possible on case-insensitive filesystems):

mkdir test
cd test
git init
echo 'hi' > file1
git add file1
git commit -m 'test commit 1'
git checkout -b new-branch
echo 'test2' > file2
git add file2
git commit -m 'test commit 2'
git branch -D NEW-branch
git checkout -f master
git reflog

I've since been able to find my lost commits by checking git-fsck, but my question is this:

Why did this sequence of actions break reflog? Shouldn't reflog still know the history of the HEAD ref, even though the branch was deleted?


Solution

  • In normal circumstances, HEAD either points to a SHA1 (in which case it's called detached) or it points to an existing branch reference (in which case the named branch is considered to be checked out).

    When you check out new-branch (HEAD points to refs/heads/new-branch) and then somehow manage to delete the new-branch branch, Git simply deletes the branch's ref file (.git/refs/heads/new-branch) and the branch's reflog file (.git/logs/refs/heads/new-branch). Git does not delete HEAD, nor does it update it to point somewhere else (such as the SHA1 that new-branch used to point to), because there shouldn't be a need—you're not supposed to be able to delete the current branch. So HEAD still references the now-deleted branch, which means that HEAD no longer points to a valid commit.

    If you then do git checkout -f master, Git updates HEAD to point to refs/heads/master, a new entry is added to HEAD's reflog file (.git/logs/HEAD), the files are checked out, and the index is updated. All of this is normal—this is what Git always does when you check out another branch.

    The issue you encountered arises from how the reflog file is updated and how git reflog processes the updated reflog file. Each reflog entry contains a "from" and "to" SHA1. When you switch from the non-existent new-branch branch to master, Git doesn't know what the "from" SHA1 is. Rather than error out, it uses the all-zeros SHA1 (0000000000000000000000000000000000000000). The all-zeros SHA1 is also used when a ref is created, so this most recent reflog entry makes it look like HEAD was just created, when in fact it was never deleted. Apparently the git reflog porcelain command stops walking the reflog when it encounters the all-zeros SHA1 even if there are more entries, which is why git reflog only prints one entry.

    The following illustrates this:

    $ git init test
    Initialized empty Git repository in /home/example/test/.git/
    $ cd test
    $ echo hi >file1
    $ git add file1
    $ git commit -m "test commit 1"
    [master (root-commit) 3c79ff8] test commit 1
     1 file changed, 1 insertion(+)
     create mode 100644 file1
    $ git checkout -b new-branch
    Switched to a new branch 'new-branch'
    $ echo test2 >file2
    $ git add file2
    $ git commit -m "test commit 2"
    [new-branch f828d50] test commit 2
     1 file changed, 1 insertion(+)
     create mode 100644 file2
    $ cat .git/HEAD
    ref: refs/heads/new-branch
    $ cat .git/refs/heads/new-branch
    f828d50ce633918f2fcaaaad5a52ac1ffa1c81b1
    $ git update-ref -d refs/heads/new-branch
    $ cat .git/HEAD
    ref: refs/heads/new-branch
    $ cat .git/refs/heads/new-branch
    cat: .git/refs/heads/new-branch: No such file or directory
    $ cat .git/logs/HEAD
    0000000000000000000000000000000000000000 3c79ff8fc5a55d7c143765b7f749db4dd8526266 Your Name <email@example.com> 1411018898 -0400        commit (initial): test commit 1
    3c79ff8fc5a55d7c143765b7f749db4dd8526266 3c79ff8fc5a55d7c143765b7f749db4dd8526266 Your Name <email@example.com> 1411018898 -0400        checkout: moving from master to new-branch
    3c79ff8fc5a55d7c143765b7f749db4dd8526266 f828d50ce633918f2fcaaaad5a52ac1ffa1c81b1 Your Name <email@example.com> 1411018898 -0400        commit: test commit 2
    $ git checkout -f master
    Switched to branch 'master'
    $ cat .git/logs/HEAD
    0000000000000000000000000000000000000000 3c79ff8fc5a55d7c143765b7f749db4dd8526266 Your Name <email@example.com> 1411018898 -0400        commit (initial): test commit 1
    3c79ff8fc5a55d7c143765b7f749db4dd8526266 3c79ff8fc5a55d7c143765b7f749db4dd8526266 Your Name <email@example.com> 1411018898 -0400        checkout: moving from master to new-branch
    3c79ff8fc5a55d7c143765b7f749db4dd8526266 f828d50ce633918f2fcaaaad5a52ac1ffa1c81b1 Your Name <email@example.com> 1411018898 -0400        commit: test commit 2
    0000000000000000000000000000000000000000 3c79ff8fc5a55d7c143765b7f749db4dd8526266 Your Name <email@example.com> 1411018898 -0400        checkout: moving from new-branch to master
    $ git reflog
    3c79ff8 HEAD@{0}: checkout: moving from new-branch to master
    

    As you can see, HEAD's reflog still has all of the old entries—they're just not shown by git reflog. I consider this to be a bug in Git.

    Side note: When you delete a ref, the corresponding log is also deleted. I consider this to be a bug, as there's no way to completely undo an accidental ref deletion unless you have a backup of the log.