How to bring back commits after a bad reset or rebase
A careless git reset --hard or a botched rebase feels like a permanent loss, but rarely is. The reflog logs every movement of HEAD locally and makes vanished commits recoverable again within minutes in most cases. This article shows how the reflog works, how long entries survive, and how a clean recovery plays out in practice.
Table of Contents
- 1. What the reflog actually is: every HEAD movement in view
- 2. Reflog vs. commit history: local, ephemeral, and indispensable
- 3. Reading the reflog format: HEAD@{n}, ORIG_HEAD, and timestamps
- 4. The classic case: recovering a commit after git reset --hard
- 5. Rebase gone wrong: rescuing lost commits after an interactive rebase
- 6. Finding deleted branches and orphaned commits again
- 7. A practical recovery workflow: reflog, diff check, restoration
- 8. Limits and expiry: configuring reflog expiry and gc.reflogExpire
- 9. Reflog compared: risky vs. safe recovery strategies
- 10. Summary
- 11. FAQ
1. What the reflog actually is: every HEAD movement in view
The reflog is Git's built in movement log for references such as HEAD and branch pointers. Unlike the commit history, which only shows the ancestry line of commits currently reachable from a ref, the reflog logs every single movement of HEAD chronologically, regardless of whether the target commit is still reachable through a branch afterward. Every git commit, git checkout, git merge, git rebase, git cherry-pick, and even every git reset leaves its own entry with a timestamp and a short description of the action.
This unbroken logging is exactly what makes the reflog the most important safety net in Git. While a git reset --hard immediately moves the branch pointer to a different commit and the previous position disappears from the visible history, the entry stays in the reflog. The reflog therefore does not just know a branch's current state, it knows every state the branch has held in the recent past, and the entire recovery strategy in this article is built on exactly that fact.
2. Reflog vs. commit history: local, ephemeral, and indispensable
The decisive difference between git log and git reflog lies in the data source. git log walks the parent pointers of commit objects and therefore shows the ancestry graph that gets shared between repositories on clone, push, and fetch. The reflog, on the other hand, is pure local metadata: it exists only as a file under the .git/logs/ directory, such as .git/logs/HEAD for the HEAD reference and .git/logs/refs/heads/<branch> for individual branches.
This local nature has two important consequences. First, the reflog is never transmitted with git push and never copied with git clone. A colleague who clones your repository does not see your reflog and therefore cannot use it for recovery either. Second, the reflog is not permanent: it is subject to an expiry, explained in detail in section 8. Anyone who wants to rescue lost commits must act locally and within a limited time window, not weeks later on a different machine.
3. Reading the reflog format: HEAD@{n}, ORIG_HEAD, and timestamps
Every line in the reflog follows the same pattern: an abbreviated SHA, followed by the reference and an index in curly braces, such as HEAD@{0} for the most recent entry, HEAD@{1} for the previous state of HEAD, and so on. These indexes are relative and shift with every new action, which is why an entry you want to rescue should be noted down by its SHA immediately, or acted on right away, instead of relying on a number that might shift later.
Besides the numeric index, Git also understands relative time expressions such as HEAD@{2.hours.ago} or HEAD@{yesterday}, which is especially useful when you know roughly when a mistake happened but not the exact number of steps. In addition, Git automatically sets the special reference ORIG_HEAD to the previous position of HEAD before every reset, merge, rebase, and am. ORIG_HEAD is thus a one step safety net that works without reading the reflog at all, though it only ever goes back a single step.
$ git reflog
a1b2c3d (HEAD -> feature/checkout) HEAD@{0}: commit: Add express checkout button
e4f5a6b HEAD@{1}: commit: Refactor payment step validation
9c8d7e6 HEAD@{2}: checkout: moving from main to feature/checkout
3f2e1d0 (main) HEAD@{3}: commit: Fix currency rounding in cart totals
7b6a5c4 HEAD@{4}: reset: moving to HEAD~2
d0e9f8a HEAD@{5}: commit: WIP tax calculation
b3c2d1e HEAD@{6}: commit: Initial tax calculation draft
# Time-based reference instead of an index
$ git show HEAD@{2.hours.ago}
# ORIG_HEAD points to HEAD before the last reset/rebase/merge
$ git log -1 ORIG_HEAD
4. The classic case: recovering a commit after git reset --hard
git reset --hard is the most common trigger for seemingly lost commits. The command moves the branch pointer to the given commit and overwrites both the index and the working tree without asking for confirmation. It's important to understand that reset --hard does not delete any commit objects from the object database. The previous commits merely become unreachable because no branch, tag, or other pointer references them directly anymore, but they remain in the object store as so called dangling commits until they are eventually removed by garbage collection.
This is exactly where recovery comes in: the reflog entry right before the reset points precisely at the branch's old position. A simple git reset --hard HEAD@{1} fully restores the previous state, including all commits, the index, and the working tree as they were at that point. If you are unsure whether the entry you found is really the right one, verify it first with git show or git log HEAD@{1} before another hard reset overwrites data again.
# Working tree before the mistake: 3 commits on top of main
$ git log --oneline -4
c4d5e6f (HEAD -> feature/pricing) Add tiered discount logic
b3c4d5e Fix rounding edge case
a2b3c4d Add unit tests for discount calculator
9f8e7d6 (main) Merge pull request #142
# Accidental hard reset, discarding all three commits
$ git reset --hard HEAD~3
HEAD is now at 9f8e7d6 Merge pull request #142
# The commits are gone from the branch, but not from the object database
$ git reflog
9f8e7d6 (HEAD -> feature/pricing) HEAD@{0}: reset: moving to HEAD~3
c4d5e6f HEAD@{1}: commit: Add tiered discount logic
b3c4d5e HEAD@{2}: commit: Fix rounding edge case
a2b3c4d HEAD@{3}: commit: Add unit tests for discount calculator
# Restore the branch to the state right before the reset
$ git reset --hard HEAD@{1}
HEAD is now at c4d5e6f Add tiered discount logic
5. Rebase gone wrong: rescuing lost commits after an interactive rebase
An interactive rebase with git rebase -i rewrites commits and generates new SHAs even when the content barely changes. If a commit is accidentally dropped, or the wrong order is chosen while squashing or reordering, the original commits look completely gone from the perspective of git log. In reality, every single step of a rebase produces its own reflog entry: rebase (start) marks the beginning, rebase (pick) and rebase (squash) mark each processed commit, and rebase (finish) marks the end.
For recovery, that means the entire rebase sequence can be traced step by step in the reflog, including intermediate states that are no longer visible in the final result at all. A dropped commit can be pulled back onto the current branch with a targeted git cherry-pick <sha>, without undoing the whole rebase. If instead you want to restore the complete state from before the rebase, git reset --hard ORIG_HEAD is enough, as long as no further reset or rebase has happened since then that would have overwritten ORIG_HEAD.
# Interactive rebase squashes and reorders 4 commits into 2
$ git rebase -i HEAD~4
# After the rebase, git log only shows the rewritten history
$ git log --oneline -2
7a8b9c0 (HEAD -> feature/api-client) Add retry logic and tests
5d6e7f8 (main) Implement base API client
# The reflog still holds every intermediate step of the rebase
$ git reflog
7a8b9c0 HEAD@{0}: rebase (finish): returning to refs/heads/feature/api-client
7a8b9c0 HEAD@{1}: rebase (pick): Add retry logic and tests
1e2f3a4 HEAD@{2}: rebase (squash): Add retry logic and tests
9b8c7d6 HEAD@{3}: rebase (start): checkout HEAD~4
c3d4e5f HEAD@{4}: commit: Add retry configuration constants
# Recover the commit that was dropped during the squash
$ git cherry-pick c3d4e5f
6. Finding deleted branches and orphaned commits again
git branch -D only deletes the branch pointer, never the associated commit objects. If the deleted branch was checked out at least once before, its commits show up in HEAD's reflog, specifically in the checkout entries that document switching onto and away from that branch. There is one important caveat, though: deleting a branch also removes its own reflog file under .git/logs/refs/heads/<branch>, which is why git reflog show <branchname> stops working once the branch is gone. The global HEAD reflog, however, remains unaffected.
Recovery happens in two steps: first, git reflog, combinable with grep for the branch name, is used to find the last known SHA of the deleted branch. Then git branch <new-name> <sha> creates a new branch pointer at exactly that spot, without a single commit having to be rewritten. If the branch was never checked out locally, for example because it was only known through git fetch, only the object database itself via git fsck --lost-found can help, which is considerably less reliable than the reflog.
# Accidental branch deletion before merging
$ git branch -D hotfix/payment-timeout
Deleted branch hotfix/payment-timeout (was 4f5e6d7).
# The branch ref is gone, but HEAD's reflog still knows the tip commit
$ git reflog | grep hotfix
4f5e6d7 HEAD@{2}: checkout: moving from hotfix/payment-timeout to main
# Recreate the branch pointing at the last known commit
$ git branch recovery/hotfix-payment-timeout 4f5e6d7
$ git checkout recovery/hotfix-payment-timeout
7. A practical recovery workflow: reflog, diff check, restoration
A reliable recovery workflow always follows the same pattern, regardless of the specific root cause. The first step is git reflog, or the more verbose git reflog show HEAD, to see the chronological list of every HEAD movement. The second step is identifying the right entry by timestamp and action description, such as reset: moving to HEAD~3 or rebase (start), which already spell out in plain text what happened.
Before any destructive action, the commit you found should be checked with git show <sha> or git log -p -1 <sha> to make sure it really is the state you are looking for. Only after that comes the actual restoration, and there are three common ways to do it: git reset --hard HEAD@{n} for a full return to that state, git cherry-pick <sha> to bring a single commit into the current history, or, as the safest option, git branch recovery-branch <sha>, which secures the found state as a new branch without touching the current branch at all.
8. Limits and expiry: configuring reflog expiry and gc.reflogExpire
The reflog is not a permanent archive. Git distinguishes between two expiry periods: reflog entries that are still reachable through a branch or tag expire after 90 days by default, controlled via gc.reflogExpire. Entries that no longer belong to any reachable commit, so called unreachable entries, for example after an overwritten reset or rebase, expire after just 30 days via gc.reflogExpireUnreachable. Once these deadlines pass, git gc, which regularly runs automatically in the background, permanently removes both the reflog entry and the associated object from the repository.
In practice this means a real, but generous, time window: most lost commits can be recovered without trouble as long as no more than 30 days have passed since the mistake. Anyone who wants to deliberately extend this window, for example in a repository with particularly valuable history, can adjust both values in the local or global .git/config. A value of never disables expiry entirely, though that lets the repository size grow noticeably over time.
[gc]
; Reflog entries still reachable from a ref expire after 90 days by default
reflogExpire = 90.days.ago
; Entries no longer reachable from any ref expire after 30 days by default
reflogExpireUnreachable = 30.days.ago
auto = 6700
[core]
; Keep an explicit safety margin before any automatic pruning
logAllRefUpdates = true
9. Reflog compared: risky vs. safe recovery strategies
The table below sets the risky, often panic driven reflex against a safe, reflog based approach for five typical failure scenarios.
| Scenario | Risky reflex | Safe approach |
|---|---|---|
| Commit lost after reset --hard | Searching git log, which only shows current history | git reflog show HEAD, then reset --hard HEAD@{n} |
| Branch accidentally deleted | Writing the branch off as permanently lost | git reflog + git branch <name> <sha> |
| Bad amend overwrites a commit | Trying a force push to "fix" it | git reflog, bring back the old commit via cherry-pick |
| Rebase produces the wrong result | Rewriting commits by hand manually | git reset --hard ORIG_HEAD or HEAD@{n} |
| Force push overwrites a remote branch | Hoping a teammate still has a local copy | Check your own reflog, use push --force-with-lease going forward |
The same basic rule applies in all five cases: check the reflog before taking any further destructive action, identify the correct state, and only act afterward. Anyone who instead reacts immediately with another reset --hard or a force push risks overwriting the very state that could have been rescued before it was ever restored.
Mironsoft
Git workflows, recovery playbooks, and CI/CD safeguards for Magento and Hyvä teams
Want Git workflows your team can trust?
We set up robust Git branching strategies, pre-commit hooks, and CI/CD safeguards for your Magento and Hyvä projects, so a single slip never turns into real data loss.
Git workflow audit
Branching strategy, hook configuration, and recovery playbooks for your team
Onboarding & training
Hands-on Git training on reflog, rebase, and safe force pushing
CI/CD safeguards
Automated backups, protected branches, and force push rules in the pipeline
10. Summary
The reflog solves one core problem in everyday Git work: the difference between a real data loss and a quickly fixable mistake almost always comes down to knowing git reflog. Every movement of HEAD is logged locally, regardless of whether the target commit is still reachable through a branch afterward. A git reset --hard, a failed interactive rebase, or an accidentally deleted branch are therefore rarely final, as long as you act within the 90 day or 30 day expiry window.
The decisive difference between a stress free recovery and a chaotic one lies in the order of operations: read git reflog first, verify the right entry with git show, and only then act with reset --hard, cherry-pick, or a new recovery branch. Anyone who follows that order and knows the reflog expiry periods will, in practice, almost never lose a commit for good.
The Reflog as a Lifesaver, the Essentials at a Glance
Understanding the reflog
Logs every HEAD movement locally, independent of the visible commit history in git log.
After reset --hard
git reflog shows the previous position, git reset --hard HEAD@{1} restores it fully.
After a rebase
Every rebase step leaves an entry. cherry-pick or reset --hard ORIG_HEAD rescue the old state.
Mind the expiry
90 days for reachable entries, 30 days for unreachable ones, configurable via gc.reflogExpire.