Learning Objectives
By the end of this lesson you will be able to:
- Explain why a commit survives after nothing points at it
- Describe what the reflog records and when Git writes to it
- State the three limits of the reflog: local, unpushed, and expiring
- Recognise the situations where the reflog is the answer
Two Facts That Together Save You
Fact one: commits are stored by content, not by branch.
When you commit, Git writes an object into .git/objects named by its hash. Branches are separate — a branch is a 41-byte file containing a commit ID. Deleting a branch deletes the pointer, not the objects. The commits sit there exactly as before, simply with nothing referring to them.
A commit nothing points at is called unreachable. It is not deleted, it is unaddressed.
Fact two: Git writes down every position HEAD has held.
Every time HEAD moves — commit, checkout, switch, merge, rebase step, reset, pull — Git appends a line to .git/logs/HEAD recording where it moved from, where to, and why.
Put the two together: an unreachable commit is still in the database, and the reflog still contains its ID. Which means it is not unreachable at all — you just need to look it up.
What a Reflog Entry Contains
git reflog
d4a91e0 HEAD@{0}: reset: moving to HEAD~2
1a77c60 HEAD@{1}: commit: test: add refund boundary cases
9d3e072 HEAD@{2}: commit: test: extract payment fixture
e2b90d1 HEAD@{3}: checkout: moving from main to test/PAY-4417
Each line is: the commit ID after the move, the HEAD@{n} shorthand, the operation, and a description.
HEAD@{0} is where you are now. HEAD@{1} is where you were one move ago. Counting up goes backwards in time — and note that this is not the same as HEAD~1, which means "one commit back in the current history". The reflog counts moves you made; ~ counts ancestry. Confusing them is the most common reflog mistake.
In the output above, HEAD@{1} is the commit that the reset removed. It is right there, with its ID, ready to be recovered.
Branch Reflogs
Each branch has its own reflog as well:
git reflog show test/PAY-4417
d4a91e0 test/PAY-4417@{0}: reset: moving to HEAD~2
1a77c60 test/PAY-4417@{1}: commit: test: add refund boundary cases
Useful when several branches have moved and the HEAD reflog is crowded. The syntax <branch>@{n} works anywhere a commit ID does:
git diff test/PAY-4417@{1} test/PAY-4417@{0}
You can also index by time, which is occasionally exactly what you need:
git show 'main@{yesterday}'
git log 'main@{2 hours ago}'
The Three Limits
The reflog is not a backup, and being precise about what it is not matters more than being enthusiastic about what it is.
It is local. Your reflog describes your clone. A colleague's mistakes are in their reflog, not yours, and you cannot recover their lost commit from your machine.
It is not pushed. .git/logs/ never travels. Clone a repository and the new clone's reflog starts empty — it has no memory of anything that happened before the clone.
It expires. By default, reachable entries are kept 90 days and unreachable ones 30. After that, git gc may remove both the entry and the object it referred to.
git config gc.reflogExpire # default: 90 days
git config gc.reflogExpireUnreachable # default: 30 days
So: recovery works on the machine where the mistake happened, within roughly a month. That is a generous window and a real boundary.
When the Reflog Is the Answer
Four situations, all covered in the rest of this module:
- A
git reset --hardmoved the branch and took commits with it - A branch was deleted with
git branch -D - A rebase produced something wrong and has already finished
- You committed in detached
HEADand switched away
They share a shape: commits exist, but nothing points at them any more. That is exactly the problem the reflog solves.
And the situation it cannot help with, for the same structural reason: changes that were never committed. git reset --hard over an unsaved afternoon leaves nothing for the reflog to have recorded.
Pro Tip: The instant something goes wrong, run
git reflogbefore anything else. Every subsequent command pushes entries down the list, and a few —git gc --prune=nowespecially — can permanently remove what you were about to recover.
Key Takeaways
- Commits are objects stored by hash; branches are just pointers to them
- Deleting a branch or resetting away commits leaves the objects intact but unreachable
- The reflog records every position
HEADhas held, with the reason for each move HEAD@{n}counts moves you made;HEAD~ncounts ancestry — they are different- Branches have their own reflogs, addressable as
<branch>@{n}and by time - The reflog is local, never pushed, and expires after roughly 30–90 days
- It recovers commits, never uncommitted work