How to Safely Undo Changes in Git: reset, revert, restore, and reflog
Which Git command to use to undo changes in each situation — reset, revert, restore, stash, and reflog — without losing your work.
You edited the wrong file, committed something half-finished, or ran git add on a file you didn't mean to — and now your stomach tightens before typing the next command. The fear is fair: some Git "undo" commands silently and permanently delete work, while others are perfectly safe. The difference between git checkout, git restore, git reset, and git revert isn't cosmetic — it's the difference between regaining your composure and losing an afternoon of code. This guide shows which command to use in each situation, always favoring the paths that destroy nothing.
Before anything else: find out what state you're in
Almost every undo mistake comes from applying the right command in the wrong state. Git has three areas, and the correct command depends on where your change lives:
- Working directory: you edited the file but haven't run
git add. - Staging (index): you ran
git addbut haven't committed. - History (commits): you already ran
git commit.
The command that tells you the truth here is git status. Run it before undoing anything:
git status
It literally tells you which command to use ("use git restore to discard changes", "use git restore --staged to unstage"). If you're just getting started and these three states still feel abstract, it's worth going through our Git for beginners guide first, which walks through the add → commit → push cycle calmly. Here the focus is the opposite: how to step back.
Undoing changes that haven't been committed yet
Unstage a file (after git add)
You staged a file by mistake but want to keep the edits. This is safe — nothing is deleted:
git restore --staged file.js # Git 2.23+
git reset HEAD file.js # classic form, same effect
The file goes back to "modified but not staged". Your work stays intact.
Discard edits in a file (danger: this deletes)
Here lives the first real trap. If you want to throw away a file's changes and return to the last commit:
git restore file.js # Git 2.23+
git checkout -- file.js # old form
This command permanently discards anything uncommitted. There's no reflog for uncommitted edits. Before running it, ask yourself: "am I sure I don't want any of this?". If doubt creeps in, git stash instead of discarding — it tucks everything into a recoverable drawer:
git stash # stashes your changes and cleans the working directory
git stash pop # brings them back whenever you want
git stash is the most underrated "undo with a safety net" in Git.
Undoing commits: reset vs. revert
Here's the most important decision in this guide. The golden rule:
Commit you have NOT pushed yet →
git reset. Commit you HAVE pushed and shared →git revert.
git reset — for rewriting local history
git reset moves the branch pointer to an earlier commit. It has three modes, and understanding the difference is what prevents lost work:
git reset --soft HEAD~1 # undoes the commit, KEEPS everything staged
git reset --mixed HEAD~1 # (default) undoes the commit, keeps edits in the working dir
git reset --hard HEAD~1 # undoes the commit AND DELETES the changes
--soft: the most common case. "I committed too early / bad message." The commit disappears, but your files stay ready for a new commit. Safe.--mixed: the commit disappears and the changes return to the working directory, unstaged. Good for reorganizing what goes into each commit. Safe.--hard: destructive. It deletes the working directory changes. Only use it when you're 100% sure you want none of it.
To fix just the last commit's message, you don't even need a reset:
git commit --amend -m "correct message"
git revert — to undo without rewriting history
git revert deletes nothing: it creates a new commit that undoes the changes of an earlier commit. It's the mandatory choice when the commit is already on the remote and other people may have pulled it.
git revert abc1234 # creates a commit that inverts abc1234
Why not use reset on a shared branch? Because reset rewrites the timeline. If you do reset + push --force on a branch a teammate already pulled, their history and yours diverge, and the next pull becomes a minefield. revert keeps the full history — anyone auditing sees both the mistake and the fix, which is usually what you want.
The safety net almost nobody uses: git reflog
If you ran a reset --hard and your blood ran cold, breathe. Git keeps a log of where HEAD pointed across your recent actions: the reflog. Commits "lost" to a reset still exist for weeks before the garbage collector clears them.
git reflog # lists the history of HEAD movements
git reset --hard HEAD@{1} # go back to where you were before the mistake
The reflog recovers commits that vanished from git log. It does not, however, recover edits that were never committed or stashed — which is why git restore and reset --hard on uncommitted files are the most dangerous commands in the set.
Quick decision table
| Situation | Safe command |
|---|---|
| Unstage a file | git restore --staged file |
| Discard an uncommitted edit | git stash (recoverable) or git restore file (deletes) |
| Fix the last commit's message | git commit --amend |
| Undo the last commit, keep the code | git reset --soft HEAD~1 |
| Undo a commit already on the remote | git revert <hash> |
| Recover from a reset --hard | git reflog + git reset --hard HEAD@{n} |
Frequently asked questions
How do I undo the last commit without losing my changes?
Use git reset --soft HEAD~1. The commit is removed from history, but all your changes stay staged, ready for a new commit. If you want the changes back in the working directory (unstaged), use git reset --mixed HEAD~1. In neither case is your code deleted — only --hard does that.
What's the difference between git reset and git revert?
git reset rewrites history: it moves the branch pointer backward and the undone commits vanish from the timeline. It's ideal for local mistakes nobody has seen yet. git revert rewrites nothing — it creates a new commit that cancels an earlier one, preserving the entire history. Use revert whenever the commit has already been pushed to a shared branch.
Do git checkout and git restore do the same thing?
Partly. The old git checkout was overloaded: it both switched branches and discarded file changes, which confused a lot of people. As of Git 2.23, that second job became git restore (discard/restore files) and branch switching became git switch. Functionally git restore file and git checkout -- file are equivalent, but git restore is more explicit and less risky.
Can I recover changes after a git reset --hard?
It depends on the state they were in. If the changes had become a commit, yes: git reflog shows the previous point and you return with git reset --hard HEAD@{1}. If they were edits never committed or stashed, no — they leave no recoverable trace. That's why the recommendation is to stash before any destructive command.
What to take from this guide
Safety in Git doesn't come from memorizing commands — it comes from two questions before you type: "is my change in the working dir, staged, or already committed?" and "has this commit already been pushed?". The first picks between restore and reset; the second, between reset and revert. Reserve --hard and push --force for when you're absolutely certain, use git stash as a net whenever there's doubt, and remember that git reflog is your insurance against lost commits. Undoing in Git stops being scary once you know that, in the vast majority of cases, nothing is truly irreversible.
- 01 UTF-8 and encoding problems: why accents break and how to fix them What UTF-8 is, code points, mojibake, and why accented text turns to garbage — the mechanism explained end to end.
- 02 MIT, Apache, and GPL Licenses: Practical Differences for Developers MIT, Apache 2.0, and GPL are not the same thing. Understand permissive vs copyleft, patent protection, and compatibility before shipping code to production.