After git reset --hard discarded your last three commits, how do you get them back?
answer
- Commits are not deleted, pointers move
- HEAD remembers everywhere it has been
- One local log per repository, newest first
- git reflog, then git branch <name> <hash>
basics
~20 sNothing was deleted: git reset --hard only moved the branch pointer. Find the old tip's hash with git reflog, verify it with git show, then re-anchor it using git branch rescue <hash> or git reset --hard <hash>.
solid answer
~40 s`git reset --hard` rewrites a ref and forces the index and working tree to match it; the commit objects themselves are untouched and simply become unreachable. So the first move is `git reflog`, which lists every position HEAD has held, most recent first, with the reason (`commit`, `reset`, `rebase`, `checkout`). I find the entry from just before the reset, confirm it with `git show <hash>` or `git log --oneline <hash> -3`, then re-anchor it — usually `git branch rescue <hash>`, because that creates a real ref without disturbing my current state, and I can merge or cherry-pick from there. `git reset --hard ORIG_HEAD` is the quick version immediately after the mistake. The one thing that does not come back is uncommitted, never-staged work: no object was ever written for it.
code
console · 9 lines$ git reflog
8f1c2a9 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
4b7de10 HEAD@{1}: commit: add retry backoff
0c9a331 HEAD@{2}: commit: extract client factory
77ab512 HEAD@{3}: commit: parse timeout config
$ git show 4b7de10 --stat
$ git branch rescue 4b7de10
$ git log --oneline rescue -3go deeper
Memorise the two-step reflex: run git reflog, then git branch <name> <hash>. Being able to say out loud that reset --hard moves a pointer rather than deleting commits is most of the credit here.
Explain the mechanics: commits are content-addressed objects, a branch is a file holding one hash, and the reflog is a local append-only log of HEAD movements. Note that uncommitted changes are the only real casualty.
Show the incident habits: verify the hash with git show before acting, prefer an additive rescue branch over a second destructive command, and know that the recovery window is bounded by reflog expiry and garbage collection.
Frame it as blast radius. Argue for practices that make this a non-event — frequent WIP commits, pushing topic branches, and treating any command that rewrites refs on shared branches as requiring a recorded pre-image.
## What `git reset --hard` actually does Git keeps three pieces of state: the working tree (files on disk), the index (the staging area), and HEAD (the commit your current branch points at). `git reset --hard <target>` moves the current branch to `<target>` and then forces both the index and the working tree to match that commit. The destructive-sounding part is only the working-tree overwrite. The command does not delete a single commit object. ## Why the commits still exist A commit is an immutable object stored under `.git/objects`, addressed by the hash of its own content. A branch is nothing more than a small file under `.git/refs/heads/` (or a line in `.git/packed-refs`) containing one hash. Moving a branch backwards leaves the old commits with no ref pointing at them — they become *unreachable*, which is not the same as deleted. Unreachable objects sit in the object database until garbage collection eventually prunes them, which normally takes weeks. ## Step 1 — find the hash in the reflog Every time HEAD moves, Git appends a line to `.git/logs/HEAD`. `git reflog` prints that log newest-first, each line tagged with the operation that caused the move: `commit`, `commit (amend)`, `reset`, `rebase (finish)`, `checkout`, `merge`, `pull`. The entry immediately below your `reset:` line is the tip you had before the reset. `git reflog show <branch>` does the same for one branch's own log, and `git log -g --oneline` (`--walk-reflogs`) walks reflog entries with full log formatting. The reflog is strictly local and per-repository. It is not fetched, not pushed, and a fresh clone starts with an empty one — so this recipe only works in the clone where the mistake happened. ## Step 2 — verify before you act Abbreviated hashes look alike after a bad day. `git show <hash>` prints the commit and its diff; `git log --oneline <hash> -5` shows it plus its ancestry. Confirm you have the right tip *before* moving anything, since a second wrong `--hard` just adds another layer to unwind. ## Step 3 — re-anchor the commits Recovery means making the commits reachable again from some ref: - `git branch rescue <hash>` — safest. It creates a ref without touching your current branch or working tree, so the commits are protected from garbage collection and you can inspect, merge, or cherry-pick at leisure. - `git reset --hard <hash>` — puts the current branch back exactly where it was. Fine when your working tree is clean and you are certain. - `git cherry-pick <hash>` — when you only want some of the lost commits on top of where you are now. There is also a shortcut: `git reset` records the pre-reset HEAD in `ORIG_HEAD`, so `git reset --hard ORIG_HEAD` undoes the reset directly. It holds only the most recent such operation, so it is a first-minute tool, not a general one. ## What genuinely does not come back `git reset --hard` also blows away modifications to tracked files that were never committed. Those never became objects, so there is nothing to recover — this is the real loss, and it is why "commit early, even a WIP commit" is good advice. One partial exception: content you had run `git add` on was hashed into a blob object at that moment, so it survives as a dangling blob and can be dug out with `git fsck --lost-found`, though you must work out which file each blob was. Untracked files are not removed by `reset --hard` at all; that is `git clean`. ## The clock is running Unreachable commits are recoverable until garbage collection prunes them. Reflog entries expire on their own schedule and unreachable loose objects are pruned after a grace period, so if you discover the loss weeks later, or someone has run an explicit prune, the window may already be closed. While you are recovering, avoid commands that trigger housekeeping and do not run an aggressive prune. ## The habit interviewers are listening for The answer they want is "reflog first, panic never", plus the reflex of creating a rescue branch rather than issuing a second destructive command. Candidates who say the work is gone, or that they would re-clone from the remote, are telling you they do not understand that commits are content-addressed objects and branches are just pointers.
- What is genuinely unrecoverable after a hard reset?Modifications to tracked files that were never committed. No object was ever written for them, so there is nothing in the object database to find. Content you had staged with git add is a partial exception: it exists as a dangling blob and can be dug out with git fsck --lost-found, though you have to work out which file it belonged to.
- Why create a rescue branch instead of running another reset --hard?A branch is additive: it makes the lost commits reachable again without moving your current branch or overwriting your working tree, so a misidentified hash costs nothing. A second hard reset is another destructive operation performed while you are already unsure of the state, and it can bury changes you have made since.
- The commits were made in a teammate's clone. Does your reflog help?No. Reflogs are local files under .git/logs and are never transferred by fetch, push, or clone. Recovery has to happen in the repository where the commits were created, or from a remote-tracking ref if they were pushed before being lost. In a fresh clone there is no prior HEAD history to consult.
Resetting a branch is like moving a bookmark, not shredding the pages. The pages are still in the book; the reflog is the list of every page the bookmark has sat on.
saying these in an interview costs you the question
- Says reset --hard permanently deletes the commits
- Proposes re-cloning from the remote to recover local commits
- Believes the reflog lives on the server and is fetched
- Thinks git revert undoes a local hard reset
- Expects uncommitted working-tree edits to return with the commits