Reflog and Recovery
The reflog is a per-ref local journal of every position HEAD and each branch has held. Because Git records where a ref pointed before and after each move, a commit can become completely unreachable from any branch or tag and still be recoverable — by default for 90 days. It is the undo buffer behind almost every "I just destroyed my work" situation.
The reflog's defining limitation is the flip side of its power: it tracks refs, not files. It can bring back any commit a ref ever pointed at, but it knows nothing about edits that were only ever in your working tree or staged in the index and never committed. Understanding that boundary is what tells you, in a panic, whether the reflog can help.
What the Reflog Records
Every move of HEAD and every branch tip is logged: commits, checkouts, resets, rebases, merges, amends. Each entry stores the before and after SHA plus a short description of the action. The data lives under .git/logs/, is strictly local, and is never pushed or fetched. That locality is why a fresh clone starts with an empty reflog and why a teammate's reflog can never help you recover your own loss.
Because the reflog captures the SHA a ref pointed at before each move, it preserves the trail to commits that a reset or rebase made unreachable. The commit objects themselves survive as long as the reflog references them.
Reading It
Plain git reflog shows HEAD's history, the most useful default. git reflog show <branch> shows a specific branch's history. To address an entry, use the HEAD@{n} syntax — HEAD@{1} is where HEAD pointed one move ago — or the time-based form like HEAD@{2.days.ago} or main@{yesterday}. Note that HEAD@{1} is fundamentally different from HEAD~1: the former is the previous position of the ref in time, the latter is the first parent in the commit graph.
Recovering Lost Commits
The textbook recovery is a botched reset: git reset --hard HEAD@{1} moves the branch back to where it pointed immediately before the bad reset. For an orphaned commit you found by reading the reflog — say, work done on a detached HEAD — re-anchor it with git branch rescue <sha> or git checkout -b rescue <sha>, which gives it a ref again so it stops being a GC candidate.
The reliable workflow is always the same: run git reflog first, read the entries to find the SHA you want, then point a ref at it. The commit was never actually gone; it was only unnamed.
Expiry and Limits
The reflog is not forever. gc.reflogExpire governs reachable entries and defaults to 90 days; gc.reflogExpireUnreachable governs entries whose commits are no longer reachable from any ref and defaults to 30 days. After the relevant window passes and garbage collection runs, the objects are genuinely gone. Running git reflog expire --expire=now --all followed by a gc destroys the recovery trail immediately and on purpose — which is exactly what a history-scrubbing rewrite needs, and exactly what you never want to run by accident.
- Assuming a
git reset --hardis unrecoverable and re-doing hours of work, whenHEAD@{1}held the discarded commit the whole time. - Expecting the reflog to recover work that was only edited or staged but never committed — it tracks ref movements, not index or working-tree state, so that work is gone.
- Waiting for a teammate's reflog to bail you out — the reflog is strictly local and is never pushed, fetched, or shared.
- Running
git reflog expire --expire=now --alland thengit gcon a whim, permanently destroying the recovery trail you might have needed. - Cloning a repo and being surprised the reflog is empty, then concluding history is broken — clones always start with a fresh, empty reflog.
- Make
git reflogthe very first command after any "I lost it" moment, before running anything else that could move refs. - Recover a botched reset with
git reset --hard HEAD@{1}once you have confirmed that entry is the state you want back. - Rescue a detached-HEAD orphan by pointing a ref at its SHA with
git branch <name> <sha>straight from the reflog. - Raise
gc.reflogExpireUnreachableon a critical local repo when you want a longer recovery window than the 30-day default. - Reach for
git fsck --no-reflogs --lost-foundto find dangling objects only after confirming the reflog has already expired.
hg journal plus the evolve extension's backups are the closest analogsSubversion no client-side reflog — recovery means digging through the server repositoryPerforce history is server-side; local recovery is not a conceptFossil no reflog, because history is immutableKnowledge Check
How can the reflog recover a commit that is unreachable from every branch and tag?
- The reflog records the SHA a ref pointed at before each move, keeping the commit referenced and alive until the reflog entry expires
- Git silently stores a hidden backup branch under refs for every commit that becomes unreachable from a ref
- Unreachable commits are pushed to a special area on the remote for safekeeping, where the reflog can fetch them back
- The reflog reconstructs the missing commit object from the current working tree and the index on demand, the moment you ask Git for its SHA
Why does reflog recovery fail for work that was edited but never committed?
- The reflog tracks movements of refs, not the contents of the index or working tree, so uncommitted work was never recorded
- The reflog keeps committed work for only 24 hours before scheduled garbage collection prunes those entries permanently from the entry list
- Uncommitted work is staged onto the remote first, and the reflog has no permission to read that remote-side area
- The reflog requires a non-empty commit message before it will index any entry, and uncommitted edits have none
What is the difference between gc.reflogExpire and gc.reflogExpireUnreachable?
reflogExpire(90 days) governs reachable entries;reflogExpireUnreachable(30 days) governs entries whose commits are no longer reachable from any ref- One applies only to the HEAD reflog and the other only to the per-branch reflogs under refs/heads, and both expire their entries on exactly the same schedule
- They are interchangeable aliases for one setting and both fall back to the same 90-day default expiry window
- One controls the local reflog while the other controls the reflog kept on the remote for fetched refs
How does HEAD@{1} differ from HEAD~1?
HEAD@{1}is where HEAD pointed one move ago in time;HEAD~1is the first parent of the current commit in the graph- They always resolve to the same commit by definition, since both step exactly one position back from HEAD regardless of any intervening checkout or reset
HEAD@{1}selects the second parent of a merge commit, whileHEAD~1selects the first parent of that same mergeHEAD~1indexes the reflog by time, whileHEAD@{1}walks the commit graph by first-parent
You got correct