The “how do I undo this?” question comes up constantly, and the answer depends on one critical variable: has anyone else already pulled the commit you want to undo? If yes, rewriting history causes pain for your teammates. If no, you have more options. The map below will get you to the right command quickly, but understanding why each command works the way it does is what makes the difference in an incident at 2am.
git restore: Discarding Working Tree Changes#
git restore is the modern replacement for the old git checkout -- file pattern. It discards changes in the working tree or the staging area without touching commits at all.
# Discard unstaged changes to a file (working tree only)
git restore path/to/file.go
# Unstage a file (move it from staging area back to working tree)
git restore --staged path/to/file.go
# Unstage AND discard working tree changes
git restore --staged --worktree path/to/file.gogit restore on the working tree is destructive and irreversible. There is no reflog for unstaged changes. Use it only when you are certain you do not need those edits.
git reset: Moving HEAD#
git reset moves the branch pointer (HEAD) to a different commit. What happens to the changes from the commits you “skipped over” depends on the flag.
| Flag | HEAD moves | Staging area | Working tree |
|---|---|---|---|
--soft | Yes | Unchanged (changes staged) | Unchanged |
--mixed (default) | Yes | Cleared (changes unstaged) | Unchanged |
--hard | Yes | Cleared | Cleared (changes lost) |
# Undo last commit, keep changes staged
git reset --soft HEAD~1
# Undo last commit, keep changes in working tree (unstaged)
git reset --mixed HEAD~1
# Undo last commit, discard all changes entirely
git reset --hard HEAD~1HEAD~1 means “one commit before HEAD”. You can also use a specific SHA: git reset --soft abc1234.
Never use git reset on commits that have already been pushed to a shared branch. It rewrites history. If a teammate has pulled those commits, their repo will diverge from yours and the resulting merge will be confusing and painful. Use git revert instead (see below).
git revert: Creating an Undo Commit#
git revert does not move HEAD backwards. Instead it creates a new commit whose changes are the inverse of the commit you specify. The full history is preserved.
# Create a revert commit for the most recent commit
git revert HEAD
# Revert a specific commit by SHA
git revert abc1234
# Revert without immediately committing (stage the changes first, then commit manually)
git revert --no-commit abc1234This is the correct approach for undoing changes on main, release, or any other branch that other engineers have already pulled. The history shows exactly what happened: the original commit followed by the revert commit.
When reverting a merge commit, you need to specify which parent to revert to with the -m flag: git revert -m 1 <merge-commit-sha>. Parent 1 is the branch that was merged into; parent 2 is the merged branch.
Decision Diagram#
flowchart TD
A[I need to undo something] --> B{Has it been pushed\nto a shared branch?}
B -->|Yes| C[git revert\nCreates a new undo commit\nHistory is preserved]
B -->|No| D{Where are the\nchanges?}
D -->|Unstaged working tree| E[git restore file\nDiscard file changes]
D -->|Staged but not committed| F[git restore --staged file\nUnstage the changes]
D -->|Committed locally| G{What do I want\nto do with the changes?}
G -->|Keep them staged| H[git reset --soft HEAD~1]
G -->|Keep them unstaged| I[git reset --mixed HEAD~1]
G -->|Discard them entirely| J[git reset --hard HEAD~1]
git reflog: The Safety Net#
The reflog is a local log of everywhere HEAD has pointed in the last 30 days. Even if you run git reset --hard and “lose” commits, they are still in the object store and reachable via the reflog.
# Show the reflog
git reflog
# Output looks like:
# abc1234 HEAD@{0}: reset: moving to HEAD~1
# def5678 HEAD@{1}: commit: add login feature
# ...
# Recover the "lost" commit
git checkout def5678
# Or reset back to it
git reset --hard def5678The reflog is purely local. It is not pushed to the remote. If you clone a repo fresh and lose commits with --hard, they are gone. But in your normal day-to-day workflow on your own machine, the reflog is an almost reliable safety net.
Reflog entries expire after 90 days by default (30 days for unreachable objects). You can adjust this with gc.reflogExpire and gc.reflogExpireUnreachable in your git config, but the defaults are generous enough for most recovery scenarios.
If you want to go deeper on any of this, I offer 1:1 coaching sessions for engineers working on AI integration, cloud architecture, and platform engineering. Book a session (50 EUR / 60 min) or reach out at manuel.fedele+website@gmail.com.