Undoing Changes in Git: reset, revert, checkout, and restore
Comprehensive guide to undoing changes in Git: git reset (soft/mixed/hard), git revert, git checkout, and git restore for safe version control operations.
Introduction
Every developer has been there: you committed to the wrong branch, staged the wrong files, or pushed a change that broke production. Git provides several commands for undoing changes, but they work in fundamentally different ways and choosing the wrong one can cause data loss or break your team’s workflow.
The key distinction is between rewriting history (changing commits that exist) and creating new history (adding commits that undo previous ones). Rewriting history is safe for local, unpushed commits but dangerous for shared branches. Creating new history is always safe but adds noise to the log. Understanding this distinction is the foundation of safe Git operations.
This guide covers git reset (with its three modes), git revert, git checkout, and the modern git restore command. You will learn when to use each, how they interact with the three-state model, and how to recover from mistakes. For the conceptual foundation, see The Three States.
When to Use / When Not to Use
Use git reset when:
- Undoing local, unpushed commits
- Unstaging files (
--mixedor--soft) - Discarding local commits you no longer want
- Squashing commits before pushing
Use git revert when:
- Undoing commits that have been pushed to a shared branch
- Creating an auditable undo trail for compliance
- Reverting a specific commit in the middle of history
Use git restore when:
- Discarding uncommitted changes in the working directory
- Unstaging files (modern replacement for
git reset HEAD) - Restoring files from a specific commit
Use git checkout when:
- Switching branches (its primary purpose)
- Restoring individual files from a specific commit (legacy syntax)
Core Concepts
The undo commands operate at different levels of the three-state model:
graph LR
A[Working Directory] -->|git restore / git checkout| B[Discard changes]
A -->|git add| C[Staging Area]
C -->|git restore --staged| A
C -->|git reset --mixed| A
C -->|git reset --soft| D[Repository HEAD]
D -->|git reset --mixed| C
D -->|git reset --hard| A
D -->|git revert| D
Three Modes of git reset
graph TD
A[Commit C3<br/>HEAD points here] -->|git reset --soft HEAD~1| B[HEAD moves to C2<br/>Changes stay STAGED]
A -->|git reset HEAD~1| C[HEAD moves to C2<br/>Changes stay UNSTAGED<br/>in working directory]
A -->|git reset --hard HEAD~1| D[HEAD moves to C2<br/>Changes are DISCARDED<br/>working directory clean]
reset vs revert
graph LR
A[C1] --> B[C2]
B --> C[C3<br/>Bad commit]
C -->|git reset --hard C2| D[C1 --> C2<br/>C3 is gone<br/>History rewritten]
C -->|git revert C3| E[C1 --> C2 --> C3 --> C4<br/>C4 undoes C3<br/>History preserved]
Architecture or Flow Diagram
Decision Tree: Which Undo Command?
graph TD
A[Need to undo something?] --> B{Is the commit pushed/shared?}
B -->|Yes| C[Use git revert]
B -->|No| D{What level?}
D -->|Working directory| E[Use git restore file]
D -->|Staging area| F[Use git restore --staged file]
D -->|Last commit| G{Keep changes?}
G -->|Yes, staged| H[git reset --soft HEAD~1]
G -->|Yes, unstaged| I[git reset HEAD~1]
G -->|No, discard| J[git reset --hard HEAD~1]
D -->|Specific commit| K[git revert <commit>]
D -->|Switch branches| L[git switch branch]
The Safety Spectrum
graph LR
A[Safest] --> B[git revert]
B --> C[git reset --soft]
C --> D[git reset --mixed]
D --> E[git restore --staged]
E --> F[git restore file]
F --> G[git reset --hard]
G --> H[Most Dangerous]
Step-by-Step Guide / Deep Dive
git reset: Three Modes
Soft Reset: Undo Commit, Keep Changes Staged
# Scenario: You committed but want to add more files to the same commit
# Undo the last commit, keep all changes staged
git reset --soft HEAD~1
# Now you can add more files and re-commit
git add additional-file.py
git commit -m "feat: complete feature with all files"
# Result: One clean commit instead of two
The soft reset moves HEAD back one commit but leaves both the staging area and working directory untouched. Your changes are still staged, ready to be committed again.
Mixed Reset: Undo Commit, Keep Changes Unstaged
# Scenario: You committed but want to restage changes selectively
# Undo the last commit, keep changes in working directory (unstaged)
git reset HEAD~1
# Equivalent to: git reset --mixed HEAD~1
# Review what changed
git status
git diff
# Stage only the changes you want
git add -p src/app.py
# Commit selectively
git commit -m "feat: partial feature implementation"
The mixed reset (default mode) moves HEAD back and unstages all changes. Your working directory is untouched — you still have all your changes, just not staged.
Hard Reset: Undo Commit, Discard Everything
# Scenario: You want to completely discard the last commit and all changes
# WARNING: This permanently deletes uncommitted changes
git reset --hard HEAD~1
# Reset to a specific commit
git reset --hard abc1234
# Reset to match the remote branch (discard all local changes)
git reset --hard origin/main
The hard reset moves HEAD back and makes both the staging area and working directory match the target commit. All uncommitted changes are permanently lost.
git revert: Safe Undo for Shared History
# Revert the last commit
git revert HEAD
# Revert a specific commit
git revert abc1234
# Revert a range of commits
git revert abc1234..def5678
# Revert without opening the editor (auto-generate message)
git revert --no-edit abc1234
# Revert a merge commit (requires specifying the parent)
git revert -m 1 abc1234 # -m 1 means revert relative to first parent
git revert creates a new commit that is the inverse of the specified commit. If the original commit added lines, the revert removes them. If it deleted lines, the revert adds them back. The original commit remains in history.
git restore: Modern File-Level Undo
Introduced in Git 2.23, git restore separates the file-restoration functionality from git checkout:
# Discard changes in working directory (uncommitted changes)
git restore src/app.py
# Discard all changes in working directory
git restore .
# Unstage a file (move from staging area back to working directory)
git restore --staged src/app.py
# Restore a file from a specific commit
git restore --source=abc1234 src/app.py
# Restore and stage in one command
git restore --staged --worktree src/app.py
git checkout: Branch Switching and File Restoration
# Switch branches
git checkout feature-branch
# Modern alternative:
git switch feature-branch
# Create and switch to a new branch
git checkout -b new-feature
# Modern alternative:
git switch -c new-feature
# Restore a file from HEAD (legacy syntax)
git checkout -- src/app.py
# Modern alternative:
git restore src/app.py
# Restore a file from a specific commit
git checkout abc1234 -- src/app.py
# Modern alternative:
git restore --source=abc1234 src/app.py
Common Undo Scenarios
Scenario 1: Committed to Wrong Branch
# You committed to main but should have been on feature
# Undo the commit, keep changes staged
git reset --soft HEAD~1
# Switch to the correct branch
git switch feature
# Commit on the correct branch
git commit -m "feat: add feature"
Scenario 2: Forgot to Stage a File
# You committed but forgot a file
# Add the forgotten file
git add forgotten-file.py
# Amend the last commit
git commit --amend --no-edit
Scenario 3: Committed with Wrong Message
# Fix the commit message
git commit --amend -m "Correct commit message"
Scenario 4: Want to Squash Last Two Commits
# Soft reset both commits, keep changes staged
git reset --soft HEAD~2
# Re-commit as one
git commit -m "feat: combined feature implementation"
Scenario 5: Discard All Local Changes
# Reset working directory and staging area to match remote
git fetch origin
git reset --hard origin/main
# Clean untracked files too
git clean -fd
Production Failure Scenarios + Mitigations
| Scenario | Impact | Mitigation |
|---|---|---|
git reset --hard on shared branch | Breaks teammates’ clones, forces everyone to re-sync | Never reset pushed commits; use git revert instead |
git reset --hard loses uncommitted work | Hours of development lost permanently | Commit frequently; use git stash for temporary saves; check git reflog for recovery |
git revert on a merge commit without -m | Error or incorrect revert | Always specify -m 1 or -m 2 when reverting merge commits |
git checkout on uncommitted changes | Changes may be overwritten or lost | Use git stash before switching branches with uncommitted work |
| Reverting a commit that was already reverted | Double-negative restores original changes | Check git log to see if a revert commit already exists |
git reset on a subdirectory | Unexpected behavior, partial reset | Always reset from the repository root |
Trade-offs
| Approach | Advantages | Disadvantages | When to Use |
|---|---|---|---|
git reset --soft | Preserves all changes staged, clean undo | Only works for unpushed commits | Squashing, amending, restaging |
git reset --mixed | Preserves changes, allows selective restaging | Requires restaging | Restaging selectively, undoing commits |
git reset --hard | Complete clean slate | Permanently loses uncommitted changes | Discarding experimental work, syncing with remote |
git revert | Safe for shared history, auditable | Creates extra commits, may cause conflicts | Undoing pushed commits, compliance |
git restore | Clear intent, modern syntax | Not available in Git < 2.23 | Discarding file changes, unstaging |
git checkout -- | Works everywhere | Ambiguous intent (switching vs restoring) | Legacy scripts, older Git versions |
Implementation Snippets
The Safe Undo Workflow
# Before any destructive operation, create a safety branch
git branch backup-before-reset
# Now you can safely reset
git reset --hard HEAD~3
# If something goes wrong, recover
git reset --hard backup-before-reset
git branch -d backup-before-reset
Recovery with reflog
# See all HEAD movements (including resets)
git reflog
# Find the commit you lost
git reflog | head -20
# Recover a lost commit
git reset --hard abc1234
# Or cherry-pick it
git cherry-pick abc1234
Bulk Operations
# Revert all commits since a tag
git log --oneline v1.0.0..HEAD | while read hash msg; do
git revert --no-edit $hash
done
# Reset all modified files to HEAD
git checkout -- $(git diff --name-only)
# Unstage all files
git restore --staged .
Interactive Undo
# Interactive rebase to edit, squash, or drop commits
git rebase -i HEAD~5
# In the editor:
# pick = keep commit
# reword = keep but edit message
# edit = stop for manual changes
# squash = combine with previous
# fixup = combine, discard message
# drop = remove commit
Observability Checklist
- Logs: Use
git reflogas your safety net — it records every HEAD movement - Metrics: Track the frequency of
git reset --hard— high frequency indicates workflow issues - Traces: Use
git log --onelinebefore and after undo operations to verify the result - Alerts: Pre-push hooks should warn about force-push requirements after resets
- Audit: Use
git reflogto audit who performed undo operations and when - Health: Periodically check
git statusto ensure no unintended state changes - Validation: After any undo operation, run tests to verify the codebase is still functional
Security/Compliance Notes
git reset --harddestroys evidence: In regulated environments, discarding commits may violate audit requirements. Usegit revertinstead to maintain a complete audit trail- Reflog is local:
git reflogis not shared with remotes. Each developer has their own reflog. It is not a substitute for proper audit logging - Reverted commits remain visible:
git revertpreserves the original commit in history, which is essential for compliance. Anyone can see what was done and what was undone - Force-push after reset: If you reset a pushed branch, you must force-push. This rewrites shared history and may violate organizational policies
- Sensitive data removal:
git reset --harddoes not remove sensitive data from Git’s object database. Usegit filter-repoor BFG Repo-Cleaner for permanent removal
Common Pitfalls / Anti-Patterns
- Using
git reset --hardas a reflex: This is the most dangerous Git command for data loss. Always considergit reset --mixedfirst, which preserves your changes - Resetting pushed commits: This rewrites shared history and breaks everyone who pulled. Use
git revertfor shared branches - Confusing
git checkoutandgit restore:git checkoutdoes two things (switch branches and restore files), which is confusing. Usegit switchfor branches andgit restorefor files - Not checking reflog after mistakes:
git reflogcan recover almost any lost commit. Before panicking about lost work, check the reflog - Reverting without understanding conflicts: A revert may conflict with subsequent changes. Always review the result of
git revertbefore committing - Using
git reseton a subdirectory:git resetoperates on HEAD, not on paths.git reset -- subdirdoes not do what you might expect - Forgetting that
git revertcreates a new commit: The undo is itself a commit. This means reverting a revert restores the original changes
Quick Recap Checklist
-
git reset --softmoves HEAD back, keeps changes staged -
git reset --mixed(default) moves HEAD back, keeps changes unstaged -
git reset --hardmoves HEAD back, discards all changes permanently -
git revertcreates a new commit that undoes a previous commit (safe for shared history) -
git restore filediscards uncommitted changes in a file -
git restore --staged fileunstages a file -
git checkoutis primarily for switching branches (usegit switchinstead) -
git reflogcan recover lost commits after reset - Never reset or amend commits that have been pushed to shared branches
- Always create a backup branch before destructive operations
- Use
git revertfor compliance and audit trails -
git reset --hard origin/mainsyncs your branch with the remote
Interview Q&A
git reset rewrites history by moving the branch pointer backward. The commits after the reset point become unreachable (though recoverable via reflog). git revert creates new history — it adds a new commit that is the inverse of the specified commit. The original commit remains in history. Reset is safe for local, unpushed commits. Revert is safe for shared, pushed commits. The mnemonic: reset erases, revert reverses.
--soft moves HEAD to the target commit but keeps all changes staged — the staging area and working directory are untouched. --mixed (default) moves HEAD and unstages all changes but keeps them in the working directory. --hard moves HEAD and discards everything — both the staging area and working directory are reset to match the target commit. The progression is: soft keeps everything, mixed keeps working files, hard keeps nothing.
Use git reflog to find the lost commit. The reflog records every HEAD movement, including resets. Run git reflog to see the history of HEAD positions, find the commit hash from before the reset, and then run git reset --hard <hash> to restore it. The reflog typically retains entries for 90 days. This is why you should never panic after a reset — the reflog is your safety net.
git checkout has two unrelated responsibilities: switching branches and restoring files. This dual purpose is confusing — git checkout file.txt restores a file, but git checkout branch switches branches. git restore (introduced in Git 2.23) separates these concerns: use git switch for branches and git restore for files. This makes the intent clear and reduces mistakes. The old syntax still works but is deprecated.
How Reset Moves HEAD, Index, and Working Tree
git reset operates by moving the HEAD reference and optionally updating the index (staging area) and working directory. Understanding exactly what each mode touches is critical to using reset safely:
graph TD
A[HEAD points to C3] --> B{reset mode?}
B -->|--soft| C[HEAD moves to C2<br/>Index = C3 snapshot<br/>Working tree = C3 snapshot]
B -->|--mixed| D[HEAD moves to C2<br/>Index = C2 snapshot<br/>Working tree = C3 snapshot]
B -->|--hard| E[HEAD moves to C2<br/>Index = C2 snapshot<br/>Working tree = C2 snapshot]
C --> F[Changes preserved as STAGED]
D --> G[Changes preserved as UNSTAGED]
E --> H[Changes DISCARDED permanently]
--soft: Only HEAD moves. Index and working tree keep the content of the original commit. Your changes appear as staged.--mixed(default): HEAD and index move. Working tree keeps the original content. Your changes appear as unstaged modifications.--hard: HEAD, index, and working tree all move. Everything matches the target commit. Uncommitted changes are destroyed.
Production Failure: git reset --hard on Shared Branch
A senior developer notices a bad commit on the shared develop branch. Instead of reverting, they run git reset --hard HEAD~2 and force-push. Consequences:
- Five teammates lose work — each had pulled the now-deleted commits and built on top of them
- Divergent histories — every teammate’s local branch now has different commit hashes for the same logical changes
- Hours of manual recovery — each developer must stash, re-fetch, rebase, and resolve conflicts individually
- Lost commits — one teammate’s unpushed WIP commit becomes orphaned and is only recovered via reflog
- Team trust broken — developers become afraid to pull, slowing the entire team
What should have happened:
# Safe undo that preserves history
git revert abc1234
git revert def5678
git push origin develop
Rule: Never reset a branch that others have pulled. If it is shared, revert it.
Trade-offs: reset vs revert vs restore
| Command | What it does | Safety level | History impact | When to use |
|---|---|---|---|---|
git reset --soft | Moves HEAD back, keeps changes staged | Safe (local only) | Rewrites local history | Squashing commits, fixing last commit |
git reset --mixed | Moves HEAD back, unstages changes | Safe (local only) | Rewrites local history | Restaging selectively, undoing commit |
git reset --hard | Moves HEAD back, discards everything | Dangerous | Rewrites local history, destroys work | Discarding experimental work, syncing with remote |
git revert | Creates new commit that undoes old one | Safe (shared OK) | Preserves all history | Undoing pushed commits, compliance |
git restore | Discards file changes or unstages | Safe | No history impact | Throwing away uncommitted changes |
git checkout | Switches branches or restores files | Safe | No history impact | Branch switching (use git switch instead) |
Security and Compliance: Why Revert Over Reset for Shared Branches
In regulated environments (finance, healthcare, government), every change must have an auditable trail. git reset erases commits from the visible history, while git revert preserves them:
- Audit trail:
git revertleaves both the original commit and the revert commit in history. Auditors can see what was done and what was undone.git resetremoves evidence entirely. - Non-repudiation: With signed commits, a revert chain proves who made changes and who authorized their removal. Reset breaks this chain.
- Compliance requirements: SOC 2, HIPAA, and PCI-DSS all require change management records. Revert commits serve as those records; reset commits destroy them.
- Team accountability: When revert is used, everyone can see the undo operation in
git log. When reset is used, the change simply disappears, raising questions about what was removed and why.
Policy recommendation: Configure branch protection rules to reject force-pushes on shared branches. Enforce git revert as the only acceptable undo mechanism for any commit that has been pushed.
Resources
- Git Reset Documentation — Complete reference
- Git Revert Documentation — Safe undo operations
- Git Restore Documentation — Modern file restoration
- Pro Git — Undoing Things — Official guide
- The Three States — Foundational concepts
Category
Related Posts
Centralized vs Distributed VCS: Architecture, Trade-offs, and When to Use Each
Compare centralized (SVN, CVS) vs distributed (Git, Mercurial) version control systems — their architectures, trade-offs, and when to use each approach.
Automated Changelog Generation: From Commit History to Release Notes
Build automated changelog pipelines from git commit history using conventional commits, conventional-changelog, and semantic-release. Learn parsing, templating, and production patterns.
Choosing a Git Team Workflow: Decision Framework for Branching Strategies
Decision framework for selecting the right Git branching strategy based on team size, release cadence, project type, and organizational maturity. Compare Git Flow, GitHub Flow, and more.