Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 632
user2221343 Avatar asked Sep 17 '14 18:09

user2221343


2 Answers

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 protected]> 1411018898 -0400        commit (initial): test commit 1
3c79ff8fc5a55d7c143765b7f749db4dd8526266 3c79ff8fc5a55d7c143765b7f749db4dd8526266 Your Name <[email protected]> 1411018898 -0400        checkout: moving from master to new-branch
3c79ff8fc5a55d7c143765b7f749db4dd8526266 f828d50ce633918f2fcaaaad5a52ac1ffa1c81b1 Your Name <[email protected]> 1411018898 -0400        commit: test commit 2
$ git checkout -f master
Switched to branch 'master'
$ cat .git/logs/HEAD
0000000000000000000000000000000000000000 3c79ff8fc5a55d7c143765b7f749db4dd8526266 Your Name <[email protected]> 1411018898 -0400        commit (initial): test commit 1
3c79ff8fc5a55d7c143765b7f749db4dd8526266 3c79ff8fc5a55d7c143765b7f749db4dd8526266 Your Name <[email protected]> 1411018898 -0400        checkout: moving from master to new-branch
3c79ff8fc5a55d7c143765b7f749db4dd8526266 f828d50ce633918f2fcaaaad5a52ac1ffa1c81b1 Your Name <[email protected]> 1411018898 -0400        commit: test commit 2
0000000000000000000000000000000000000000 3c79ff8fc5a55d7c143765b7f749db4dd8526266 Your Name <[email protected]> 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.

like image 161
Richard Hansen Avatar answered Sep 28 '22 06:09

Richard Hansen


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.

like image 21
VonC Avatar answered Sep 28 '22 05:09

VonC