Git Undo: Reset, Revert & Restore — The Complete Guide for 2026
Every developer reaches the moment: you committed the wrong files, pushed a broken build, or ran a rebase that scrambled your history. Git has powerful undo capabilities, but choosing the right command depends on exactly what you need to undo and whether the commit has been shared. The wrong choice can lose work or break your teammates' history.
This guide covers every undo scenario you will encounter: git reset for rewriting local history, git revert for safely undoing pushed commits, git restore for discarding file changes, and git reflog for recovering commits you thought were lost. Each section includes the exact commands to run and explains when to use each approach.
Quick Reference: "I Want to Undo X"
Find your scenario and jump to the right command:
| I want to undo… | Command |
|---|---|
| Last commit (keep changes staged) | git reset --soft HEAD~1 |
| Last commit (keep changes unstaged) | git reset HEAD~1 |
| Last commit (discard everything) | git reset --hard HEAD~1 |
| A pushed commit (safe for shared branches) | git revert <commit-hash> |
| Changes to a file (not yet staged) | git restore <file> |
| Staged changes (unstage a file) | git restore --staged <file> |
| A merge (not yet pushed) | git reset --hard ORIG_HEAD |
| A merge (already pushed) | git revert -m 1 <merge-hash> |
| A lost commit (after reset --hard) | git reflog then git reset --hard <hash> |
git reset: Rewrite Local History
git reset moves the branch pointer (HEAD) to a different commit. It is the primary tool for undoing local, unpushed commits. The three modes control what happens to your working directory and staging area.
--soft: Keep Everything Staged
Moves HEAD back but keeps all changes staged. Your files are completely untouched. Use this when the commit message was wrong or you want to combine commits:
# Undo the last commit, keep changes staged
git reset --soft HEAD~1
# Undo the last 3 commits, squash them into one
git reset --soft HEAD~3
git commit -m "Combined feature implementation"
After --soft, running git status shows all the undone changes as "Changes to be committed".
--mixed (default): Keep Changes, Unstage Them
Moves HEAD back, unstages changes, but keeps file modifications. This is the default when you run git reset without a flag:
# These are equivalent
git reset HEAD~1
git reset --mixed HEAD~1
# Unstage all files without moving HEAD
git reset HEAD
# Unstage a specific file
git reset HEAD -- path/to/file.js
After --mixed, changes appear as "Changes not staged for commit". You can re-stage selectively before recommitting.
--hard: Discard Everything
Moves HEAD back and deletes all changes — staged, unstaged, and file modifications. This is destructive:
# Discard the last commit and all its changes
git reset --hard HEAD~1
# Discard all local changes, match remote exactly
git reset --hard origin/main
# Discard all uncommitted changes (keep commits)
git reset --hard HEAD
Warning: Uncommitted changes destroyed by --hard cannot be recovered. Always check git stash or git diff before running this.
Visual: How the Three Modes Compare
Before reset (HEAD at commit C):
A --- B --- C (HEAD)
|
Staged: file.js (modified)
Working: file.js (modified)
After git reset --soft HEAD~1:
A --- B (HEAD)
|
Staged: changes from C + file.js (all staged)
Working: unchanged
After git reset --mixed HEAD~1:
A --- B (HEAD)
|
Staged: (empty)
Working: changes from C + file.js (all unstaged)
After git reset --hard HEAD~1:
A --- B (HEAD)
|
Staged: (empty)
Working: (clean — everything deleted)
git revert: Safe Undo for Pushed Commits
git revert creates a new commit that reverses the changes from a previous commit. It does not rewrite history, making it safe for shared branches:
# Revert the most recent commit
git revert HEAD
# Revert a specific commit by hash
git revert a1b2c3d
# Revert without auto-committing (inspect changes first)
git revert --no-commit a1b2c3d
git diff --staged # review what will be undone
git commit -m "Revert: remove broken feature flag"
# Revert multiple commits (oldest to newest)
git revert a1b2c3d..f4e5d6c
When to use revert: Always use git revert instead of git reset when the commit exists on a shared branch (main, develop, or any branch others have pulled from). Reset rewrites history; revert preserves it.
Handling Revert Conflicts
If the revert conflicts with later changes, Git pauses and asks you to resolve:
git revert a1b2c3d
# CONFLICT in src/config.js
# Fix the conflict manually, then:
git add src/config.js
git revert --continue
# Or abort the revert entirely:
git revert --abort
git restore: The Modern File-Level Undo
Introduced in Git 2.23 (2019), git restore replaces the confusing git checkout -- <file> syntax. It operates on files, not commits:
Discard Unstaged Changes
# Restore a single file to its last committed state
git restore src/app.js
# Restore all files in a directory
git restore src/
# Restore everything (discard all unstaged changes)
git restore .
Unstage Files
# Unstage a file (keep changes in working directory)
git restore --staged src/app.js
# Unstage everything
git restore --staged .
# Unstage AND discard changes (both flags)
git restore --staged --worktree src/app.js
Restore from a Specific Commit
# Restore a file from 3 commits ago
git restore --source HEAD~3 src/config.js
# Restore a file from a specific commit
git restore --source a1b2c3d src/config.js
# Restore a deleted file
git restore --source HEAD~1 src/deleted-file.js
git reflog: Recovering "Lost" Commits
The reflog records every time HEAD moves — commits, resets, rebases, checkouts. Even after git reset --hard, the old commits still exist in Git's object database for at least 30 days:
# View the reflog
git reflog
# Output looks like:
a1b2c3d HEAD@{0}: reset: moving to HEAD~3
f4e5d6c HEAD@{1}: commit: Add user authentication
b7c8d9e HEAD@{2}: commit: Update dashboard layout
e1f2g3h HEAD@{3}: commit: Fix navigation bug
# Recover: reset back to the commit before the destructive reset
git reset --hard f4e5d6c
# Or create a new branch from the lost commit
git branch recovery-branch f4e5d6c
Reflog for Specific Branches
# View reflog for a specific branch
git reflog show feature/auth
# View reflog with timestamps
git reflog --date=relative
# Output:
a1b2c3d HEAD@{2 hours ago}: commit: Add login form
f4e5d6c HEAD@{5 hours ago}: commit: Set up auth routes
Key limitation: The reflog only tracks commits that Git recorded. If you modified a file but never committed or stashed it, git reflog cannot help. Commit early and often.
Undo Scenarios: Step-by-Step
Scenario 1: Undo the Last Commit (Not Pushed)
# Keep changes staged (fix the commit message or add more files)
git reset --soft HEAD~1
git commit -m "Corrected commit message"
# Keep changes unstaged (re-edit before committing)
git reset HEAD~1
# edit files...
git add -p # stage selectively
git commit -m "Clean implementation"
Scenario 2: Undo the Last Commit (Already Pushed)
# Create a new commit that reverses the changes
git revert HEAD
git push origin main
# Never do this on shared branches:
# git reset --hard HEAD~1 && git push --force # DANGEROUS
Scenario 3: Undo Staged Changes
# Accidentally staged the wrong file
git restore --staged secrets.env
# File is now unstaged but still modified in working directory
# Accidentally ran git add .
git restore --staged .
Scenario 4: Undo Unstaged Changes
# Discard modifications to a single file
git restore src/app.js
# Discard all modifications (keep untracked files)
git restore .
# Remove untracked files too
git clean -fd # -f = force, -d = directories
git clean -fdn # dry run first (show what would be deleted)
Scenario 5: Undo a Merge (Not Pushed)
# Git saves the pre-merge HEAD in ORIG_HEAD
git reset --hard ORIG_HEAD
# Or use reflog to find the exact commit
git reflog
git reset --hard HEAD@{1}
Scenario 6: Undo a Merge (Already Pushed)
# -m 1 means "keep parent 1 (our branch), undo parent 2 (merged branch)"
git revert -m 1 <merge-commit-hash>
git push origin main
# Warning: if you later want to re-merge the same branch,
# you must first revert the revert:
git revert <revert-commit-hash>
git merge feature/that-branch
Scenario 7: Undo a Rebase
# Find where you were before the rebase
git reflog
# Look for the entry before "rebase (start)"
# e.g., HEAD@{5}: checkout: moving from feature to feature
git reset --hard HEAD@{5}
# Or use ORIG_HEAD (set before rebase starts)
git reset --hard ORIG_HEAD
Scenario 8: Recover After a Force Push
# Someone (maybe you) force-pushed and overwrote commits
# If you still have the local repo with the original commits:
git reflog
git reset --hard <original-commit-hash>
git push --force-with-lease origin main
# If you do not have the commits locally, check if a teammate does
# They can push the correct history back
Common Mistakes and How to Fix Them
Mistake: Used --hard When You Meant --soft
# You ran: git reset --hard HEAD~1 (lost your changes!)
# Fix: recover the commit from reflog
git reflog
# Find the lost commit hash
git reset --hard <lost-commit-hash>
Mistake: Committed to the Wrong Branch
# You committed to main instead of feature/login
# Step 1: save the commit hash
git log --oneline -1 # note the hash
# Step 2: remove from main
git reset --soft HEAD~1
# Step 3: switch and recommit
git stash
git checkout feature/login
git stash pop
git commit -m "Add login form"
Mistake: Amended a Commit and Lost the Original
# git commit --amend overwrites the previous commit
# The original commit still exists in reflog
git reflog
# Find the pre-amend commit (the one before "commit (amend)")
git reset --soft <pre-amend-hash>
git commit -m "Original message restored"
Mistake: Deleted an Untracked File
# If Git never tracked the file, Git cannot recover it
# Prevention: always run git clean with --dry-run first
git clean -fdn # shows what WOULD be deleted
git clean -fd # actually deletes
Decision Flowchart: Which Undo Command?
Is the change committed?
|
+-- NO: Is the change staged?
| |
| +-- YES --> git restore --staged <file> (unstage)
| |
| +-- NO --> git restore <file> (discard modification)
|
+-- YES: Has the commit been pushed?
|
+-- NO: Do you want to keep the changes?
| |
| +-- YES, staged --> git reset --soft HEAD~1
| |
| +-- YES, unstaged --> git reset HEAD~1
| |
| +-- NO --> git reset --hard HEAD~1
|
+-- YES: Is it on a shared branch?
|
+-- YES --> git revert <hash> (safe, creates new commit)
|
+-- NO (your own branch) --> git reset --hard HEAD~1
git push --force-with-lease
Best Practices for Safe Undoing
- Commit frequently. Small commits give you more granular undo points. You can always squash later.
- Use
--force-with-leaseinstead of--force. It refuses to push if someone else has pushed commits you do not have, preventing data loss. - Check
git statusandgit diffbefore any destructive command. Verify what will be affected. - Use
git stashbefore risky operations. Stash your work-in-progress, perform the undo, then pop the stash. - Never reset on shared branches. If others have pulled the commit, use
git revertinstead. - Know your reflog. It is your safety net. Commits linger for 30+ days even after
reset --hard. - Prefer
git restoreovergit checkout --. The newer syntax is explicit about whether you are acting on the staging area or working directory.
Frequently Asked Questions
How do I undo the last git commit without losing changes?
Run git reset --soft HEAD~1. This moves HEAD back one commit but keeps all your changes staged and ready to commit again. Your files are untouched. If you also want to unstage the changes, use git reset HEAD~1 (the default --mixed mode).
What is the difference between git reset --soft, --mixed, and --hard?
All three move the HEAD pointer to a different commit. --soft keeps everything staged and files unchanged. --mixed (the default) unstages changes but keeps files unchanged. --hard discards everything — staged changes, unstaged changes, and file modifications are all deleted. Use --soft to recommit differently, --mixed to re-stage selectively, and --hard only when you want to throw away all work.
How do I undo a commit that has already been pushed?
Use git revert <commit-hash>. This creates a new commit that reverses the specified commit's changes without rewriting history. Run git revert HEAD to undo the most recent commit, then git push. Never use git reset followed by force push on shared branches.
Can I recover a commit after git reset --hard?
Yes. Git's reflog records every HEAD position change for at least 30 days. Run git reflog, find the commit hash from before the reset, and run git reset --hard <hash> to restore it. However, uncommitted changes (never committed or stashed) cannot be recovered.
When should I use git revert vs git reset?
Use git revert when the commit has been pushed to a shared branch — it creates a new commit that preserves history. Use git reset when the commit is local only, since it cleanly removes the commit from history. Rule of thumb: if anyone else might have pulled the commit, use revert.
How do I undo a git merge?
If the merge is not pushed, run git reset --hard ORIG_HEAD. If the merge has been pushed, use git revert -m 1 <merge-commit-hash>. The -m 1 flag keeps the first parent and reverses the merged branch's changes. Note that re-merging the same branch later requires reverting the revert first.