Undoing Mistakes: git reset, git revert, and git restore
Every Developer Breaks Things
It's not a question of if — it's a question of when. You'll accidentally commit the wrong file. You'll stage something you didn't mean to. You'll make a change that completely breaks the project. You'll write a commit message with a typo and hit Enter too fast.
This is normal. It happens to beginners and senior engineers alike. What separates confident developers from panicked ones is knowing how to undo things cleanly.
Git gives you three primary tools for undoing mistakes: git restore, git reset, and git revert. Each one operates at a different level and is designed for a different situation. By the end of this article, you'll know exactly which one to reach for — and when.
Setting Up a Practice Project
Let's build a small project to experiment with. It's much easier to learn undo commands when you can break things freely without consequence.
mkdir git-undo-practice
cd git-undo-practice
git init
Create a few files and build up some history:
echo "# My Notes App" > README.md
echo "Buy groceries" > todo.txt
git add .
git commit -m "Initial commit: add README and todo list"
echo "Clean the house" >> todo.txt
git add .
git commit -m "Add second task to todo list"
echo "function loadNotes() {}" > app.js
git add .
git commit -m "Add app.js with loadNotes function"
echo "Call the dentist" >> todo.txt
git add .
git commit -m "Add third task to todo list"
Let's verify our history:
git log --oneline
You should see four commits. Now let's start breaking — and fixing — things.
git restore — Undoing Changes to Files
git restore is the simplest undo tool. It works at the file level and handles two common situations: discarding changes you haven't staged yet, and unstaging changes you've already added. It was introduced in Git 2.23 specifically to make these operations clearer and safer.
Scenario 1: Discard Unstaged Changes
You're editing a file and you realize the changes are wrong. You want to throw them away and go back to the last committed version.
Let's simulate this. Make a bad edit to the todo list:
echo "ASDFJKL GARBAGE TEXT" >> todo.txt
Check the status:
git status
Output:
Changes not staged for commit:
modified: todo.txt
You can see the damage with git diff:
git diff todo.txt
There's the garbage text at the bottom. Let's undo it:
git restore todo.txt
That's it. Check the status again:
git status
Output:
nothing to commit, working tree clean
The file is back to its last committed state. The garbage text is gone. It's as if the edit never happened.
Warning: This is one of the few Git operations that permanently destroys work. Once you restore a file, the discarded changes are gone forever — they were never committed, so Git has no record of them. Only use this when you're certain you don't need those changes.
Scenario 2: Unstage a File
You staged a file by mistake and want to move it back to the working directory without losing the changes.
Let's set it up:
echo "Do laundry" >> todo.txt
echo "function saveNotes() {}" >> app.js
git add .
Check the status:
git status
Output:
Changes to be committed:
modified: app.js
modified: todo.txt
Both files are staged. But let's say you only wanted to commit the todo change, not the app.js change. You need to unstage app.js:
git restore --staged app.js
Check the status:
git status
Output:
Changes to be committed:
modified: todo.txt
Changes not staged for commit:
modified: app.js
Perfect. app.js is back in the working directory (your changes are still there — nothing was lost), while todo.txt remains staged and ready to commit.
The key difference between the two forms:
git restore <file>— Discards changes in the working directory. Changes are lost.git restore --staged <file>— Moves a file from staged back to unstaged. Changes are preserved.
Restoring a File from a Specific Commit
You can also restore a file to the state it was in at any previous commit. Let's say you want to see what todo.txt looked like in the very first commit:
git log --oneline -- todo.txt
Find the commit ID of the initial commit (let's say it's a1b2c3d), then:
git restore --source a1b2c3d todo.txt
This replaces the current todo.txt with the version from that commit. The change shows up as a modification in your working directory — it's not committed automatically. You can review it, keep it, or discard it.
Before git restore Existed
If you're reading older tutorials or Stack Overflow answers, you'll see git checkout -- <file> used to discard changes. This still works, but git restore replaced it because git checkout did too many different things (switching branches and restoring files), which confused people. Use git restore — it's clearer and safer.
git reset — Rewinding Commits
While git restore works on individual files, git reset works on commits. It moves your branch pointer backward in history, effectively "un-committing" one or more commits. It's more powerful than git restore, and it comes in three modes that control what happens to your changes.
The Three Modes of git reset
When you reset to a previous commit, Git needs to know what to do with the changes that were in the commits you're "removing." The three modes give you three different answers:
--soft: Keep Everything Staged
git reset --soft HEAD~1
This undoes the last commit but keeps all the changes in the staging area. It's as if you went back in time to just before you typed git commit. Your files are exactly the same, and your changes are still staged — you can modify the commit message, add more changes, or commit again immediately.
When to use it: You committed too early, or you want to change the commit message, or you want to combine the last commit with additional changes before re-committing.
Let's try it. First, let's commit the staged todo change from earlier:
git add todo.txt
git commit -m "Add fourth task with typo in messge"
Oops — there's a typo in the commit message ("messge" instead of "message"). Let's undo the commit but keep everything staged:
git reset --soft HEAD~1
Check the status:
git status
Output:
Changes to be committed:
modified: todo.txt
The commit is gone, but the change is still staged. Now recommit with the correct message:
git commit -m "Add fourth task to todo list"
Fixed. Your history is clean.
--mixed: Keep Changes but Unstage Them (Default)
git reset HEAD~1
This is the default mode (you don't need to type --mixed). It undoes the last commit and unstages the changes, but keeps them in your working directory. Your files still have the changes — they're just not staged anymore.
When to use it: You want to undo a commit and rework the changes before committing again. Maybe you want to split one big commit into two smaller ones, or you realized you need to edit the code before re-committing.
Let's try it:
git reset HEAD~1
Check the status:
git status
Output:
Changes not staged for commit:
modified: todo.txt
The commit is gone. The change is still in your file but not staged. You can now edit the file further, stage only the parts you want, and make new, better commits.
Let's recommit for the next example:
git add todo.txt
git commit -m "Add fourth task to todo list"
--hard: Discard Everything
git reset --hard HEAD~1
This is the nuclear option. It undoes the last commit, removes the changes from the staging area, and deletes the changes from your working directory. It's as if those changes never existed.
When to use it: You committed something completely wrong and you want it erased entirely. You don't want to keep any of the changes.
Let's try it:
git reset --hard HEAD~1
Check the status:
git status
Output:
nothing to commit, working tree clean
Check the file:
cat todo.txt
The fourth task is gone. The file looks exactly like it did after the third commit.
Warning: Like git restore, a hard reset permanently destroys uncommitted changes. The commits themselves can still be recovered using git reflog (we'll cover that in a later article), but any uncommitted edits in your working directory are gone for good. Use with caution.
Comparing the Three Modes
| Mode | Commit | Staging Area | Working Directory | Use When |
|---|---|---|---|---|
--soft |
Undone | Changes kept (staged) | Unchanged | Fix a commit message or add more changes |
--mixed |
Undone | Changes unstaged | Unchanged | Rework changes before re-committing |
--hard |
Undone | Changes removed | Changes removed | Completely erase the commit and its changes |
Resetting More Than One Commit
You're not limited to undoing just the last commit. HEAD~2 goes back two commits, HEAD~3 goes back three, and so on. You can also use a specific commit ID:
git reset --soft a1b2c3d
This resets your branch pointer to that specific commit. All the commits that came after it are "removed" from the branch history (though they still exist in Git's internal storage and can be recovered with git reflog).
Important: Don't Reset Pushed Commits
This is critical: never use git reset on commits that have already been pushed to a shared remote repository. When you reset, you're rewriting history — removing commits from the timeline. If your teammates have already pulled those commits, their history and yours will diverge, creating a messy conflict.
For undoing commits that have been shared with others, use git revert instead.
git revert — Safely Undoing Shared Commits
Where git reset removes a commit from history, git revert takes a different approach: it creates a new commit that does the exact opposite of the original. If the original commit added a line, the revert removes it. If the original deleted a file, the revert brings it back.
The key difference: the original commit stays in the history. Nothing is erased. You're not rewriting history — you're adding to it. This makes git revert safe to use on commits that have been pushed to a shared repository.
Reverting the Last Commit
Let's rebuild our fourth commit for this example:
echo "Call the dentist" >> todo.txt
git add .
git commit -m "Add third task to todo list"
Now let's say this commit introduced a problem and we need to undo it without erasing history:
git revert HEAD
Git will open your default text editor with a pre-filled commit message like:
Revert "Add third task to todo list"
This reverts commit d4e5f6a.
Save and close the editor (in Vim: type :wq and press Enter. In Nano: press Ctrl+O to save, then Ctrl+X to exit). If you want to skip the editor and accept the default message, use:
git revert HEAD --no-edit
Now check the log:
git log --oneline
Output:
f5g6h7i Revert "Add third task to todo list"
d4e5f6a Add third task to todo list
c3d4e5f Add app.js with loadNotes function
b2c3d4e Add second task to todo list
a1b2c3d Initial commit: add README and todo list
Both the original commit and the revert commit are in the history. The third task has been removed from the file, but the full story of what happened is preserved. Anyone looking at the log can see: "A task was added, then it was reverted." Complete transparency.
Reverting an Older Commit
You can revert any commit, not just the most recent one. Just specify the commit ID:
git revert b2c3d4e
This creates a new commit that undoes the changes from that specific older commit. Be aware that reverting an old commit can sometimes cause conflicts — if later commits modified the same lines, Git won't know how to automatically undo just the old change. In that case, you'll need to resolve the conflict manually (we'll cover conflict resolution later in the series).
Reverting Without Committing Immediately
Sometimes you want to undo a commit's changes but review them before creating the revert commit. Use the --no-commit flag (or -n for short):
git revert --no-commit HEAD
This applies the reverse changes to your working directory and staging area but doesn't create a commit. You can then inspect the changes with git diff --staged, make additional edits, and commit when you're ready.
git reset vs. git revert — When to Use Which
This is the most common question beginners ask, and the answer is straightforward:
| Situation | Use | Why |
|---|---|---|
| Undo a commit that only exists locally (not pushed) | git reset |
No one else has seen it, so rewriting history is safe and keeps your log clean |
| Undo a commit that's been pushed to a shared repo | git revert |
Preserves history so teammates aren't affected |
| Fix a bad commit message on an unpushed commit | git reset --soft |
Lets you recommit with a new message |
| Split a big commit into smaller ones | git reset --mixed |
Unstages everything so you can re-add and commit in smaller pieces |
| Completely erase a local mistake | git reset --hard |
Removes all traces of the commit and its changes |
| Undo a specific older commit while keeping everything after it | git revert |
Surgically undoes one commit without touching others |
The golden rule: if the commit has been shared with others, use revert. If it's only on your machine, use reset.
Bonus: git commit --amend
There's one more undo trick that deserves a mention here because you'll use it constantly. git commit --amend lets you modify the most recent commit without resetting anything.
Fixing a Commit Message
You just committed but the message has a typo:
git commit -m "Add lgoin page"
Fix it immediately:
git commit --amend -m "Add login page"
The commit is replaced with a new one that has the corrected message. The changes inside the commit are identical — only the message changed.
Adding Forgotten Files to the Last Commit
You committed but forgot to include a file:
git add forgotten-file.js
git commit --amend --no-edit
The --no-edit flag keeps the original commit message. The forgotten file is now included in the commit as if it was always there.
Important: Like git reset, --amend rewrites history. Don't amend commits that have been pushed to a shared repository unless you're the only one working on that branch.
A Decision Flowchart
When something goes wrong, walk through these questions in order:
- Did you edit a file but haven't staged it yet?
Usegit restore <file>to discard the changes, or just keep editing. - Did you stage a file but haven't committed yet?
Usegit restore --staged <file>to unstage it. Your changes stay in the working directory. - Did you commit but haven't pushed yet?
Usegit reset --soft HEAD~1to undo the commit and keep changes staged.
Usegit reset HEAD~1to undo and unstage.
Usegit reset --hard HEAD~1to undo and erase everything.
Usegit commit --amendto fix the message or add files. - Did you commit and already pushed it?
Usegit revert HEADto create a new commit that undoes the changes safely. - Do you need to undo a specific older commit?
Usegit revert <commit-id>to surgically undo just that commit.
Common Mistakes When Undoing Things
Mistake 1: Using reset --hard Carelessly
Hard resets permanently destroy uncommitted changes in your working directory. Always check git status and git diff before running a hard reset. Make sure there's nothing you want to keep.
Mistake 2: Resetting Pushed Commits
If you reset a commit that's been pushed and then force-push (git push --force), every teammate who has pulled that commit will have a broken history. Use git revert for shared commits. Always.
Mistake 3: Panicking and Making It Worse
When something goes wrong, the worst thing you can do is start running commands frantically. Stop. Run git status. Run git log --oneline. Understand the current state before trying to fix it. Git almost always has a way to recover, but only if you don't dig the hole deeper first.
Mistake 4: Not Knowing About Reflog
Even after a git reset --hard, the "lost" commits aren't actually deleted — they're just unreachable from your branch. Git keeps a hidden log of every change to your branch pointer called the reflog. You can recover almost anything with git reflog. We'll cover this in detail later in the series, but just knowing it exists should give you confidence: Git is very hard to break permanently.
Quick Reference
| Command | What It Does |
|---|---|
git restore <file> |
Discards unstaged changes in a file (destructive) |
git restore --staged <file> |
Unstages a file, keeping changes in working directory |
git restore --source <commit> <file> |
Restores a file to the version from a specific commit |
git reset --soft HEAD~1 |
Undoes last commit, keeps changes staged |
git reset HEAD~1 |
Undoes last commit, keeps changes unstaged (default) |
git reset --hard HEAD~1 |
Undoes last commit and erases all changes (destructive) |
git revert HEAD |
Creates a new commit that reverses the last commit |
git revert <commit-id> |
Creates a new commit that reverses a specific commit |
git revert --no-commit HEAD |
Applies reverse changes without auto-committing |
git commit --amend -m "new message" |
Replaces the last commit with a new message |
git commit --amend --no-edit |
Adds staged changes to the last commit silently |