The Problem
You pushed a bad commit. Maybe it broke the build, introduced a bug, or included files that should not be in the repository. You need to undo it, but Git offers three different approaches — reset, revert, and checkout — and using the wrong one on a shared branch can corrupt history for your entire team.
This guide explains when to use each command and the consequences of each approach.
Understanding the Three Approaches
| Command | What it does | Rewrites history? | Safe for shared branches? |
|---|---|---|---|
| <code class="inline-code">git revert</code> | Creates a new commit that undoes changes | No | Yes |
| <code class="inline-code">git reset</code> | Moves branch pointer backward | Yes | No (unless force push) |
| <code class="inline-code">git checkout</code> | Restores file content from a commit | No | Yes (for file-level) |
The golden rule: If others have pulled the commit, use git revert. If the commit is only local, use git reset.
git revert — The Safe Undo
git revert creates a new commit that is the exact inverse of the specified commit. History is preserved — anyone who already pulled the original commit will not have conflicts.
Revert the most recent commit
# Revert the last commit (creates a new commit automatically)
git revert HEAD
# Revert without auto-committing (lets you edit or combine with other changes)
git revert --no-commit HEAD
Revert a specific commit
# Find the commit hash you want to undo
git log --oneline -10
# Revert that specific commit
git revert abc123f
# The revert commit message will say:
# Revert "Original commit message"
Revert multiple commits
# Revert a range of commits (oldest..newest)
git revert --no-commit abc123f..def456a
git commit -m "Revert commits abc123f through def456a: broke payment processing"
# Revert multiple non-consecutive commits
git revert --no-commit abc123f
git revert --no-commit ghi789b
git commit -m "Revert deployment config changes"
Revert a merge commit
Merge commits have two parents. You must specify which parent to keep:
# Parent 1 is usually the branch you merged INTO (main)
# Parent 2 is the branch that was merged (feature)
git revert -m 1 abc123f # Keeps main's state, undoes the merged branch
# To see which parent is which:
git log --oneline --graph abc123f
Warning: Reverting a merge makes Git think those changes have already been applied. If you later try to re-merge the same branch, Git will skip the reverted commits. You would need to revert the revert first.
# Later, when you want to re-merge the reverted branch:
git revert <hash-of-revert-commit> # Revert the revert
git merge feature-branch # Now the changes come back
git reset — Rewrite Local History
git reset moves the branch pointer backward, effectively removing commits. There are three modes that differ in what happens to the changes from those commits.
Three reset modes
# --soft: Remove commit but keep changes staged
git reset --soft HEAD~1
# Changes are in staging area, ready to re-commit differently
# --mixed (default): Remove commit, unstage changes, keep in working directory
git reset HEAD~1
# Changes are in working directory as modified files
# --hard: Remove commit AND discard all changes permanently
git reset --hard HEAD~1
# Changes are gone. Unrecoverable (unless you use reflog quickly).
Practical scenarios
# Undo the last commit but keep the changes (rework and recommit)
git reset --soft HEAD~1
# Undo the last 3 commits, keep changes as unstaged
git reset HEAD~3
# Completely discard the last commit (dangerous)
git reset --hard HEAD~1
# Reset to a specific commit
git reset --hard abc123f
# Reset only specific files to their state in a commit
git reset HEAD~1 -- path/to/file.js
Reset on a shared branch (with force push)
If you absolutely must reset a shared branch (e.g., an accidental commit to main with secrets):
# DANGEROUS: Only do this if you have communicated with your team
git reset --hard HEAD~1
git push --force-with-lease origin main
# --force-with-lease is safer than --force
# It fails if someone else pushed since your last fetch
After force pushing, everyone on the team needs to:
# Team members must run:
git fetch origin
git reset --hard origin/main
git checkout — Restore Individual Files
git checkout (or the newer git restore) retrieves file content from a specific commit without affecting other files or history:
# Restore a single file to its state in a previous commit
git checkout abc123f -- path/to/file.js
# Restore a file to how it was 3 commits ago
git checkout HEAD~3 -- src/config.ts
# Restore all files in a directory
git checkout abc123f -- src/components/
# Modern equivalent using git restore
git restore --source=abc123f path/to/file.js
git restore --source=HEAD~3 src/config.ts
This modifies your working directory and stages the changes. You need to commit afterward:
git checkout abc123f -- src/broken-module.js
git commit -m "Restore broken-module.js to pre-refactor state"
Undoing Uncommitted Changes
When you have not committed yet:
# Discard all unstaged changes in working directory
git checkout -- .
# Or modern equivalent:
git restore .
# Discard changes in a specific file
git checkout -- src/app.js
git restore src/app.js
# Unstage files (keep changes in working directory)
git reset HEAD src/app.js
# Or modern equivalent:
git restore --staged src/app.js
# Discard both staged and unstaged changes
git reset --hard HEAD
# Remove untracked files and directories
git clean -fd
# Preview what would be removed:
git clean -fdn
Recovery: When You Reset Too Far
If you accidentally git reset --hard and lost commits:
# Reflog shows everywhere HEAD has been in the last 30 days
git reflog
# Output:
# abc123f HEAD@{0}: reset: moving to HEAD~3
# def456a HEAD@{1}: commit: Add payment processing
# ghi789b HEAD@{2}: commit: Update API endpoint
# Recover by resetting to the reflog entry
git reset --hard def456a
# Or create a new branch at that point (safer)
git branch recovery-branch def456a
Reflog entries expire after 30 days by default. Act quickly if you need to recover.
Decision Flowchart
Use this to decide which command to use:
Has the commit been pushed to a shared branch?
├── YES → Use git revert
└── NO → Has the commit been pushed to YOUR remote branch only?
├── YES → git reset + git push --force-with-lease is OK
└── NO (local only) → git reset is simplest
├── Want to keep changes? → git reset --soft HEAD~1
├── Want to rework changes? → git reset HEAD~1
└── Want to discard everything? → git reset --hard HEAD~1
CI/CD Integration Patterns
Automated revert on failed deployment
# In your deployment script:
DEPLOY_COMMIT=$(git rev-parse HEAD)
if ! deploy_to_production; then
echo "Deployment failed, reverting..."
git revert --no-commit $DEPLOY_COMMIT
git commit -m "Auto-revert: deployment of $DEPLOY_COMMIT failed"
git push origin main
deploy_to_production # Redeploy the reverted state
fi
Revert with a linked issue
git revert abc123f -m "Revert: fixes #342 regression introduced in abc123f
This revert undoes the database migration that caused timeout errors
in the checkout flow. The fix will be re-applied after addressing
connection pooling limits.
Reverts: abc123f
Related: #342, #345"
Common Mistakes
git reset on shared branches — This rewrites history that others have already built upon. Their next push or pull will create conflicts or duplicate commits.-m 1 when reverting merges — Without specifying the parent number, Git does not know which side of the merge to keep and errors out.--force instead of --force-with-lease — Plain --force overwrites the remote regardless of other people's pushes. --force-with-lease fails safely if someone else pushed first.git reset --hard without checking git stash or git reflog — Hard reset is permanent for uncommitted changes. Always stash first or verify reflog can save you.HEAD~1 with HEAD^ — For non-merge commits they are identical. For merge commits, ~ goes to the first parent, ^2 goes to the second parent.Quick Reference
| Task | Command |
|---|---|
| Undo last commit (keep changes staged) | <code class="inline-code">git reset --soft HEAD~1</code> |
| Undo last commit (keep changes unstaged) | <code class="inline-code">git reset HEAD~1</code> |
| Undo last commit (discard changes) | <code class="inline-code">git reset --hard HEAD~1</code> |
| Undo a pushed commit safely | <code class="inline-code">git revert abc123f</code> |
| Undo a merge commit | <code class="inline-code">git revert -m 1 abc123f</code> |
| Restore one file from history | <code class="inline-code">git checkout abc123f -- file.js</code> |
| Discard all local changes | <code class="inline-code">git checkout -- .</code> |
| Unstage a file | <code class="inline-code">git restore --staged file.js</code> |
| Recover after bad reset | <code class="inline-code">git reflog</code> then <code class="inline-code">git reset --hard <hash></code> |
| Force push safely | <code class="inline-code">git push --force-with-lease</code> |
Summary
Use git revert for shared branches — it is always safe. Use git reset for local-only commits when you want a cleaner history. Use git checkout/git restore to restore individual files from any point in history. When in doubt, git revert is the conservative choice that never breaks your team's workflow.
---
Frequently Asked Questions
What is the difference between git revert and git reset?
git revert creates a new commit that undoes the changes from a specific commit, preserving history — safe for shared branches. git reset moves the branch pointer backward, potentially erasing commits from history — only safe for local, unpushed branches. Always use git revert on branches that others have pulled from.
How do I revert a merge commit in Git?
Use git revert -m 1 <merge-commit-hash> where -m 1 tells Git to revert to the first parent (the branch you merged into). This undoes all changes introduced by the merge. Be aware that reverting a merge makes it tricky to re-merge the same branch later — you'll need to revert the revert first.
How do I undo the last commit but keep the changes?
Use git reset --soft HEAD~1 to undo the last commit while keeping all changes staged, or git reset --mixed HEAD~1 to undo the commit and unstage the changes but keep them in your working directory. Only do this if the commit hasn't been pushed yet.
Why did git revert create a conflict?
Revert conflicts occur when later commits have modified the same lines that the revert is trying to undo. Git cannot automatically determine how to remove the original changes without affecting the subsequent modifications. Resolve conflicts manually by deciding which version of the code should remain, then complete with git revert --continue.
---
Related Resources
- Git Cheatsheet — 106 git commands by workflow
- GitHub Actions CI/CD Complete Guide — CI/CD pipeline guide
- Zero Downtime Deployment Strategies — Rollback strategies for failed deployments