Back to Blog

Git: How to Undo Almost Anything

Git: How to Undo Almost Anything cover image

A developer messaged me at 11pm: "I ran git reset --hard and lost a day of work." He had committed at lunchtime, then worked all afternoon, then reset to undo one bad change.

The afternoon's committed work was fine. It took about forty seconds to get back with git reflog. What he had actually lost was only the uncommitted changes from the last twenty minutes.

Almost everything in git is recoverable if it was ever committed, and almost nothing is if it was not. That single sentence is the mental model behind every command below.

Reflog Is the Undo Button

Start here, because it is the one that saves you and the one most people do not know exists.

Git keeps a local log of everywhere HEAD has pointed — every commit, checkout, merge, rebase and reset — for about 90 days. Even "deleted" commits are still there.

git reflog
# 8a3f1c2 HEAD@{0}: reset: moving to HEAD~3
# 4d9e7b1 HEAD@{1}: commit: add invoice export     <-- the "lost" work
# 2c1a8f0 HEAD@{2}: commit: wire up the endpoint

git reset --hard 4d9e7b1      # back exactly as it was

Bad rebase, deleted branch, reset too far — reflog covers all of it. Before you panic about anything in git, run it.

Reset, Revert and Restore Do Different Jobs

People use these interchangeably and that is where the accidents come from.

git reset moves the branch pointer. It rewrites history, so it is only safe on commits you have not pushed. The three modes matter:

git reset --soft HEAD~1    # undo the commit, keep changes staged
git reset HEAD~1           # undo the commit, keep changes unstaged (default: --mixed)
git reset --hard HEAD~1    # undo the commit AND throw the changes away

--soft is the one to reach for when you just committed too early or with a bad message. --hard is the only genuinely dangerous command in daily git use, because uncommitted changes it destroys are not in reflog.

git revert creates a new commit that undoes an old one. History is preserved, nothing is rewritten, and it is safe on shared branches. This is what you use when something bad is already on main.

git revert a1b2c3d           # one commit
git revert -m 1 a1b2c3d      # a merge commit: -m 1 keeps the mainline

git restore works on files rather than commits, and it is the modern, clearer replacement for the overloaded git checkout --:

git restore src/app.ts              # discard uncommitted changes to a file
git restore --staged src/app.ts     # unstage, keep the changes
git restore --source=HEAD~2 src/    # bring a directory back from two commits ago

The Situations You Will Actually Hit

Committed to main instead of a branch, not pushed:

git branch feature/thing      # bookmark the commits where they are
git reset --hard origin/main  # put main back
git switch feature/thing      # your work, on the right branch

Wrong commit message, not pushed: git commit --amend. If already pushed, leave it — rewriting shared history to fix a typo is not worth it.

Committed a secret: amending only helps if you have not pushed. If you have, the credential is compromised regardless of what you do to the history — rotate it first, then clean up with git filter-repo and coordinate the force-push. Cleaning history without rotating is theatre.

Need to switch branches with work in progress:

git stash push -m "half-done export"
git switch main
# ...
git switch -                  # back to the previous branch
git stash pop

Stashes do not carry across clones and are easy to forget. For anything longer than an hour, a throwaway commit on a branch is safer.

One commit from another branch: git cherry-pick a1b2c3d. Useful for hotfixes, and it creates a duplicate commit — do not use it as a substitute for merging.

Which commit broke it: git bisect is underused and genuinely excellent. It binary-searches your history, so twenty commits takes about five tests.

git bisect start
git bisect bad                # current state is broken
git bisect good v1.4.0        # this tag was fine
# git checks out a midpoint; test it, then say bisect good / bisect bad
git bisect reset

Rebase Versus Merge

The argument that consumes the most energy for the least benefit. Both are fine. What matters is being consistent and knowing the one rule.

Rebase replays your commits on top of the target branch. Linear history, easy to read, and it rewrites commit hashes.

Merge creates a merge commit joining the two lines. Preserves exactly what happened, and produces a busier graph.

The rule: never rebase a branch other people have pulled. Rewriting shared history forces everyone else into a painful recovery. On your own feature branch before opening a PR, rebase freely.

What I do on teams: rebase my feature branch onto main to stay current while working, then merge the PR with a squash. Main gets one clean commit per feature, and the messy work-in-progress history stays out of it. git pull --rebase as the default avoids the pointless "Merge branch 'main' into main" commits.

Interactive rebase is worth learning for cleanup before review:

git rebase -i HEAD~5
# pick   a1b2c3d  add endpoint
# squash 4d5e6f7  fix typo          <-- fold into the one above
# reword 8g9h0i1  wip               <-- give it a real message
# drop   2j3k4l5  debug logging     <-- remove entirely

Force-Push Without Breaking Things

After rebasing a pushed branch you have to force. Never use plain --force:

git push --force-with-lease

This refuses if someone else has pushed to the branch since you last fetched. Plain --force overwrites their work silently. The extra characters have saved me at least twice.

Two Habits Worth More Than Any Command

Commit far more often than feels necessary. Small commits are cheap to make and cheap to undo, and everything is recoverable once it is committed. You can always squash before review. The developer who lost a day had actually only lost twenty minutes, because he had committed at lunch — and had he committed every half hour, he would have lost nothing.

Check before you destroy. git status and git diff before any --hard. That is the two-second habit that separates a recoverable mistake from a real one.

Everything else you can look up. Reflog and frequent commits are what actually keep you safe.

Related Posts