Skip to main content

Undoing Changes in Git: reset, revert, restore, and When to Use Each

·5 mins
Table of Contents
Git has four main verbs for undoing things: restore, reset, revert, and reflog. Picking the wrong one either rewrites history that others have already pulled, or throws away work you wanted to keep. Here is the mental model for choosing correctly.

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.go
Warning

git 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.

FlagHEAD movesStaging areaWorking tree
--softYesUnchanged (changes staged)Unchanged
--mixed (default)YesCleared (changes unstaged)Unchanged
--hardYesClearedCleared (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~1

HEAD~1 means “one commit before HEAD”. You can also use a specific SHA: git reset --soft abc1234.

Important

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 abc1234

This 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.

Tip

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 def5678

The 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.

Note

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.

**Using `git reset --hard` on a shared branch.** This rewrites history and will cause merge conflicts or force-push requirements for everyone who has already pulled. Always use `git revert` on branches other people are tracking. **Not knowing about `git reflog` after a `--hard` reset.** Many developers believe the work is gone forever. Run `git reflog`, find the SHA of the commit you were on before the reset, and recover it with `git reset --hard `. You have 30 days. **Confusing `git restore` with `git reset`.** `git restore` only touches the working tree and staging area -- it never moves HEAD or changes commit history. `git reset` always moves HEAD. They are complementary tools, not interchangeable. **Reverting a merge commit without the `-m` flag.** Git does not know which side of the merge to revert to and will error out. Always specify `-m 1` to revert to the mainline parent.

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.

Related