Readplace

Git: How to Undo a Commit Without Undoing Your Career

fagnerbrack.com 3 min read
View original
Summary (TL;DR)
The author recounts accidentally running `git reset --hard HEAD~3` on the main branch instead of a feature branch, losing three commits. Recovery used `git reflog` to find commit hashes and `git reset --hard` to restore them. The article covers reflog (90-day default retention for reachable commits), `git fsck --lost-found` for unreferenced objects, and commands that lose work (`reset --hard`, `clean -fd`, `push --force`) or recover it (reflog, fsck, cherry-pick). The key lesson: run `git status` and `git branch` before destructive operations to avoid mistakes.

Git: How to Undo a Commit Without Undoing Your Career

Chronological Representation of a git tree with an arrow pointing backwards from a branch

It's a beautiful summer on January 2014. I ran git reset --hard HEAD~3 on a Friday afternoon. The branch was main. I thought it was feature/auth-refactor.

Three commits and two days of work disappeared. I had no backup branch and no stash. Just a terminal cursor blinking at me like nothing happened.

git reset --hard does exactly what you tell it to do. It resets the branch pointer, the index, and the working directory to the target commit.

The whole operation finishes in milliseconds.

I sat there for about ten seconds. I scrolled up in my terminal. Wrong branch.

What I tried first

My first instinct was git log. That was useless. The log only shows reachable commits.

I had just moved the branch pointer backward. Those three commits were no longer reachable from main.

My second instinct was git stash list. Nothing there. I had committed, not stashed.

The commits existed somewhere in the object store. No ref pointed to them. Then I remembered git reflog.

What actually worked

git reflog records ref changes: checkouts, resets, commits, rebases. It's a local history of where HEAD has been.

I ran git reflog. The output looked like this:

a1b2c3d HEAD@{0}: reset: moving to HEAD~3
f4e5d6c HEAD@{1}: commit: add token refresh logic
b7a8c9d HEAD@{2}: commit: extract auth middleware
e0f1a2b HEAD@{3}: commit: wire up session store

There they were. Three commits, still alive in the object database, just disconnected from any branch.

I ran git reset --hard f4e5d6c. The work came back. The working directory, the index, and the branch pointer returned to their prior state.

Recovery took about four minutes. The panic lasted about ten seconds. Those ten seconds felt much longer 😅.

When reflog isn’t enough

Reflog entries expire. The default is 90 days for reachable commits and 30 days for unreachable ones.

An aggressive git gc schedule deletes those entries early. Reflog won't help in that case.

For those situations, there’s git fsck --lost-found. This command walks the entire object database. Unreferenced objects get listed in .git/lost-found/.

You can inspect each one with git show <hash>. From there, git cherry-pick brings needed commits back onto a branch.

I’ve used fsck once in 20 years for production recovery. It's slower and messier than reflog. But it gets the job done.

The three commands that lose work

Three Git commands can destroy uncommitted or unreferenced work:

git reset --hard resets the branch pointer to a new target. Your working directory changes to match. Unreachable commits disappear from git log.

git clean -fd deletes untracked files and directories. No confirmation by default. Uncommitted files are gone.

git push --force rewrites remote history. Other developers with copies of the old commits will hit conflicts. git push --force-with-lease is the safer alternative. It rejects the push if the remote branch has changed since your last fetch (to avoid overwriting your colleague's work).

The three commands that recover work

git reflog shows you where HEAD has been. It retains hashes for 30 to 90 days.

git fsck --lost-found lists every unreferenced object in the database. It works as a last resort.

git cherry-pick <hash> replays a single commit by hash on your current branch. Find the lost commit via reflog or fsck. Then cherry-pick brings it back.

What I actually learned

Destructive Git commands run faster than you can read their output.

git reset --hard finishes in milliseconds. The command assumes you meant it.

The real fix is a habit and knowledge, not a set of recovery commands. Run git status and git branch before any destructive operation. Read the output. Five seconds.

I’ve been writing code for over a decade. My mistakes go beyond Git: wrecked databases, broken deploys, dropped tables.

Every one of those mistakes had the same root cause. I ran the command without reading the context.

Pausing is worth more than any recovery technique. I’d rather be slow for five seconds than clever for four minutes.

If you liked this, you might like readplace.com, built for exactly this kind of reading.

Thanks for reading. If you have some feedback, reach out to me on LinkedIn, Github, Reddit, or by replying to this post.