Fix: Undo git reset --hard and Recover Lost Commits
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to undo git reset --hard and recover lost commits using git reflog — step-by-step recovery for accidentally reset branches, lost work, and dropped stashes.
The Error
You run git reset --hard to undo changes and realize you reset too far — commits you needed are gone:
git reset --hard HEAD~3
# You meant to go back 1 commit, not 3Or you reset to the wrong target:
git reset --hard origin/main
# Wiped out 5 local commits that were not pushed yetOr after a bad merge:
git reset --hard HEAD
# Discarded uncommitted changes you neededThe commits appear lost — git log no longer shows them.
Why Recovery Is Usually Possible
git reset --hard moves the branch pointer — it does not immediately delete commits. The commit objects remain in Git’s object database until garbage collection runs (typically after 30 days or on explicit git gc). Git’s reflog records every movement of HEAD and branch pointers, making it possible to find and restore those commits.
What can be recovered:
- Commits that existed before the reset (they are in the reflog).
- Commits from deleted branches (also in the reflog).
What cannot be recovered:
- Uncommitted changes that were never staged or committed — these are gone permanently after
git reset --hard. - Changes discarded with
git checkout -- fileorgit restore filewithout prior staging.
Two distinct mechanisms make recovery possible. First, the reflog (.git/logs/HEAD and .git/logs/refs/heads/*) records every change to where a ref points, including resets, rebases, checkouts, and amends. Second, every commit, tree, and blob object in .git/objects/ survives until it is unreachable from any ref AND old enough that git gc removes it. Until both conditions are met, the data is sitting on disk waiting to be found. This is why even a panicked git reset --hard origin/main rarely costs you actual data — it costs you the convenience of seeing the commits in git log. Recovery is a matter of locating the right hash and pointing a ref at it.
A few caveats are worth internalizing before you start fixing anything. The reflog is a per-clone, per-machine log; it does not travel with git push. A fresh clone has no reflog entries from your original repo, so if you clone the remote intending to recover lost local commits, you start from zero. Untracked files (never git added) are not Git objects and cannot be recovered through Git. And if you ran a worktree-aware operation, you may need git reflog --all to see entries from all worktrees.
Platform and Environment Differences
Git is portable, but recovery workflows differ enough between operating systems, file systems, and GUI clients that knowing your environment can speed up triage:
- Git for Windows reflog retention. On Windows, Git ships in the Git for Windows distribution and uses MinGW. Reflog files live under
.git/logs/exactly as on Linux. Defaultgc.reflogExpireis 90 days, but Windows file timestamps under NTFS handle locking differently — if a process holds.git/HEADopen, reflog writes can be delayed, and a hard crash can truncate the tail entry. Always rungit reflogfrom a clean shell before assuming an entry is missing. - macOS APFS snapshots. APFS on macOS takes hourly local snapshots when Time Machine is enabled, and the system also keeps periodic “local snapshots” even without an external drive. If your repo lives on an APFS volume and you reset minutes ago, run
tmutil listlocalsnapshots /to find a snapshot, mount it (sudo mount_apfs -s com.apple.TimeMachine.<id>.local /mnt), and copy the pre-reset.gitdirectory out. This is a last-resort path, but it has saved repositories whose reflog was wiped by a follow-upgit gc --prune=now. - WSL2 cross-filesystem quirks. WSL2 stores files on a virtual ext4 disk and mounts the Windows drive via
/mnt/c. Runninggitagainst a repository on/mnt/cis slow and exposes you to NTFS case-insensitivity and permission-bit drift. Reflog writes still succeed butgit fsck --lost-foundcan produce stale results if antivirus on the Windows side scans.git/objectsmid-operation. Recover from the WSL2-native filesystem when possible. - GitHub Desktop client vs CLI. GitHub Desktop logs every action it performs through Git, so its history view (“File → View Repository History”) often shows reset operations alongside the destination commit hash. The hash is exactly what you need to pass to
git reset --hard. Sourcetree exposes a similar “Recent Activity” panel; GitKraken keeps a graphical “Undo” stack under Ctrl+Z that wraps agit resetto the previous ref position. - Repository-scope reflog config. Reflog expiry is configurable per-repo, not just globally. A team that runs
git config --global gc.reflogExpire neveron shared machines preserves reflog entries indefinitely — useful for a recovery-friendly setup. Conversely, CI runners that aggressively rungit gc --prune=nowcan vaporize unreachable commits within seconds. Checkgit config --get gc.reflogExpireinside the repo, not just globally. - Detached HEAD and worktrees. Linked worktrees (
git worktree add) each have their ownHEADreflog under.git/worktrees/<name>/logs/HEAD. If you reset in a worktree,git reflog HEADin the main repo may not show that entry. Usegit reflog --allorcdinto the affected worktree. - Editor and IDE local history. VS Code maintains a per-file Local History (and the Timeline view), IntelliJ has Local History under VCS, and Eclipse keeps a “Local History” snapshot. These survive
git reset --hardbecause they are not Git data. If recovery via reflog fails, check the editor’s local history before giving up.
Fix 1: Use git reflog to Find the Lost Commits
The reflog is a local log of where HEAD has pointed. It is your primary recovery tool:
git reflogOutput looks like:
abc1234 HEAD@{0}: reset: moving to HEAD~3
def5678 HEAD@{1}: commit: Add user authentication
ghi9012 HEAD@{2}: commit: Fix login form validation
jkl3456 HEAD@{3}: commit: Update dashboard layout
mno7890 HEAD@{4}: commit: Initial dashboard setupThe commit hashes on the left are the actual commit objects — still in the repository. HEAD@{1} is where HEAD was before your reset, HEAD@{2} is the commit before that, and so on.
Find the commit you want to restore:
# See the commit message and diff for a reflog entry
git show HEAD@{1}
git show def5678Fix 2: Restore the Branch to a Reflog Entry
Once you find the commit hash or reflog reference you want to restore to:
# Reset the branch back to the commit before the mistake
git reset --hard HEAD@{1}
# Or use the commit hash directly
git reset --hard def5678This moves the branch pointer back to that commit — all commits between that point and your current position are restored in git log.
Verify the recovery:
git log --oneline -10
# Should show the recovered commitsPro Tip: After recovering commits, push them to the remote immediately if they were already pushed before — or push for the first time if they are new. This protects them from future accidental resets and garbage collection.
Fix 3: Recover Commits Without Changing the Branch
If you only need one or a few commits back (not the entire branch), cherry-pick specific commits:
# Find the commit hash in reflog
git reflog
# Cherry-pick specific commits onto the current branch
git cherry-pick def5678 # Restore one commit
git cherry-pick ghi9012 # Restore another
# Cherry-pick a range
git cherry-pick ghi9012^..def5678Cherry-pick applies the changes from those commits as new commits — useful when you want to bring back specific changes without fully resetting the branch.
Create a new branch from a reflog entry:
# Keep current branch as-is, create a recovery branch
git branch recovery-branch HEAD@{1}
# Or create and switch to it
git checkout -b recovery-branch HEAD@{1}This is the safest recovery approach — you get the old commits on a new branch and can selectively merge what you need.
Fix 4: Find Lost Commits with git fsck
If the reflog does not show what you need (e.g., after a long time), use git fsck to find dangling commits:
# Find commits not reachable from any branch or tag
git fsck --lost-found
# Output:
# dangling commit def5678abc...
# dangling commit ghi9012def...Inspect dangling commits:
git show def5678abc
git log --oneline def5678abc
# Or browse all lost-found objects
ls .git/lost-found/commit/Git writes dangling objects to .git/lost-found/commit/. Inspect each one to find your lost work, then create a branch from it:
git branch recovered-work def5678abcFix 5: Recover a Dropped git stash
git stash drop or git stash clear removes stash entries. These are also recoverable via reflog:
# List recent stash reflog entries
git reflog stash
# Or find dangling blobs (stash contents)
git fsck --lost-found 2>/dev/null | grep "dangling commit"
# Inspect each one — stash commits have a specific format
git show <hash>
# Restore a lost stash as a branch
git branch recovered-stash <hash>Stash entries in reflog look like:
abc1234 refs/stash@{0}: WIP on main: def5678 Add featureAfter git stash drop, the entry disappears from git stash list but remains findable with git fsck until garbage collection.
Fix 6: Recover a Deleted Branch
When a branch is deleted with git branch -D, its commits are not immediately lost:
# Find the deleted branch's last commit in reflog
git reflog | grep "branch-name"
# Output:
# abc1234 HEAD@{5}: checkout: moving from branch-name to main
# Recreate the branch at that commit
git checkout -b branch-name abc1234If the branch was pushed to remote and deleted locally:
# The remote still has it
git checkout -b branch-name origin/branch-nameIf the branch was deleted on the remote too:
Use the reflog approach above — remote deletions do not affect your local reflog until you fetch.
Fix 7: Prevent Future Accidents
Use git reset --soft or git reset --mixed instead of --hard:
# --soft: move branch pointer, keep changes staged
git reset --soft HEAD~1
# --mixed (default): move branch pointer, unstage changes, keep files
git reset HEAD~1
# --hard: move branch pointer, DISCARD all changes — use with caution
git reset --hard HEAD~1For most “undo last commit” use cases, --soft or --mixed is safer — they preserve your work.
Create a backup tag before risky operations:
# Tag the current state before a dangerous operation
git tag backup-before-reset
# Do the risky thing
git reset --hard HEAD~5
# Oops — restore from tag
git reset --hard backup-before-reset
# Clean up the tag when done
git tag -d backup-before-resetEnable Git’s safety features:
# Warn before force-pushing
git config --global push.default simple
# Require --force-with-lease instead of --force (safer)
git config --global alias.pushf "push --force-with-lease"Common Mistake: Running git reset --hard when you meant git checkout -- file (to undo changes to a single file). git reset --hard discards ALL uncommitted changes. For single-file undo, use:
# Undo changes to a specific file only
git checkout -- path/to/file.js
# or (modern syntax):
git restore path/to/file.jsStill Not Working?
Check if garbage collection already ran. If significant time has passed or you ran git gc --prune=now, dangling commits may be permanently deleted. Run git fsck — if it shows no dangling commits for your time range, they are gone.
Check the reflog expiry settings. By default, reflog entries expire after 90 days (30 days for unreachable entries). If the reset happened months ago, the reflog entry may be expired:
git config gc.reflogExpire # Default: 90 days
git config gc.reflogExpireUnreachable # Default: 30 daysCheck for a remote copy. If you had pushed the commits before resetting, the remote branch still has them:
git fetch origin
git log origin/your-branch --oneline
# If they appear here, you can restore them
git reset --hard origin/your-branchTry git reflog --all to scan every ref’s history. Plain git reflog shows only HEAD. Branch refs, the stash, and linked worktrees keep their own logs. The commit you want may be in a worktree-scoped reflog that the default command does not surface.
Use the GUI client’s “recent activity” view. GitHub Desktop, Sourcetree, GitKraken, and Tower all show the operations they performed and the commit hashes involved. If you used a GUI to perform the reset, the client may remember the hash even if your reflog is short.
Search the editor’s Local History. VS Code, IntelliJ, and Eclipse all maintain a per-file local-history feature that survives Git resets. This is your fallback when uncommitted edits vanish.
Look in CI build artifacts and code review caches. A recently pushed branch may live on in CI logs, GitHub Actions caches, or pull-request review pages — sometimes including the full diff. Pull the diff and reapply it as a new commit if the canonical objects are gone.
For other accidental git operations, see Fix: git stash pop conflict and Fix: git merge conflict. For push errors that block recovery, see Fix: git push rejected non-fast-forward. For accidentally rebased history, see Fix: git rebase conflict.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: .gitignore Not Working (Files Still Being Tracked)
How to fix .gitignore not working — files still showing in git status after being added to .gitignore, caused by already-tracked files, wrong syntax, nested gitignore rules, and cache issues.
Fix: git fatal: A branch named 'x' already exists
How to fix 'git fatal: A branch named already exists' when creating or renaming branches — including local conflicts, remote tracking branches, and worktree issues.
Fix: Git Worktree Not Working — Branch Already Checked Out, Prune, Submodules, and Locked Worktrees
How to fix git worktree errors — fatal: 'branch' is already checked out at, worktree prune removing valid trees, detached HEAD on add, submodules not initialized, moving/locking worktrees, and ignoring per-worktree files.
Fix: DVC Not Working — Remote Push Errors, Pipeline DAG Issues, and Git Integration
How to fix DVC errors — dvc push authentication failed, dvc pull file missing, pipeline stage not reproducing, cache out of disk space, dvc add vs dvc stage, conflict with git LFS, and S3/GCS remote setup.