7

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?

user2221343
  • 614
  • 5
  • 16
  • +1 I wish all questions had a reproducible example like yours does. – jub0bs Sep 17 '14 at 19:03
  • I don't have a satisfactory answer (yet), but note that the entries missing from `git reflog`'s output are still there, in `.git/logs/HEAD`. I'm guessing that `git reflog` only prints entries ranging from the last entry in `.git/logs/HEAD` that starts with `0000000000000000000000000000000000000000` onwards. – jub0bs Sep 17 '14 at 19:10
  • Interesting, reflog must have some logic in addition to simply showing the log then... – user2221343 Sep 17 '14 at 19:12
  • 2
    I just ran a simple test: I replaced all (except the first one) occurences of `0000000000000000000000000000000000000000` in `.git/logs/HEAD` by some other SHA. Then `git reflog` prints all the entries in `.git/logs/HEAD`. It's as I guessed earlier: `git reflog` only prints from the first entry starting with a "zero SHA" onwards. I still need to look into that, but I think that, whenever you're in a state of `bad default revision 'HEAD'`, the corresponding reflog entry starts by a "zero SHA". And you definitely land in `bad default revision 'HEAD'` territory if you saw the branch you're on... – jub0bs Sep 17 '14 at 19:17
  • 2
    On linux it appears you can simulate the behavior by replacing `git branch -D NEW-branch` with `git update-ref -d refs/heads/new-branch` – Andrew C Sep 17 '14 at 19:37

2 Answers2

8

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.

Richard Hansen
  • 51,690
  • 20
  • 90
  • 97
  • Very good explanation. I wonder why when the branch is deleted git doesn't simply put you in a 'detached HEAD' state, but rather seems to delete HEAD entirely... I suspect when it sees the HEAD ref pointing to a branch that doesn't exist, it removes it instead of updating to point to the commit. – user2221343 Sep 18 '14 at 11:46
  • Ah, you're right, `.git/HEAD` is still there pointing to `ref: refs/heads/new-branch`. Of course, there is no `.git/refs/heads/new-branch` – user2221343 Sep 18 '14 at 18:37
1

Deleted current branch and lost reflog

Two years later, this issue should be alleviated in Git 2.13 (Q2 2017).

See commit 39ee4c6, commit 893dbf5, commit de92266, commit 755b49a (21 Feb 2017) by Kyle Meyer (kyleam).
(Merged by Junio C Hamano -- gitster -- in commit c13c783, 27 Feb 2017)

branch: record creation of renamed branch in HEAD's log

Renaming the current branch adds an event to the current branch's log and to HEAD's log.
However, the logged entries differ.
The entry in the branch's log represents the entire renaming operation (the old and new hash are identical), whereas the entry in HEAD's log represents the deletion only (the new sha1 is null).

Extend replace_each_worktree_head_symref(), whose only caller is branch_rename(), to take a reflog message argument.
This allows the creation of the new ref to be recorded in HEAD's log.
As a result, the renaming event is represented by two entries (a deletion and a creation entry) in HEAD's log.

It's a bit unfortunate that the branch's log and HEAD's log now represent the renaming event in different ways.
Given that the renaming operation is not atomic, the two-entry form is a more accurate representation of the operation and is more useful for debugging purposes if a failure occurs between the deletion and creation events.

It would make sense to move the branch's log to the two-entry form, but this would involve changes to how the rename is carried out and to how the update flags and reflogs are processed for deletions, so it may not be worth the effort.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250