Git Reset — Hard Reset on Main Lost 8 Developers 2 Hours
- --soft keeps changes staged, --mixed (default) unstages them, --hard discards them. Only --hard risks losing work.
- git reset is for local branch cleanup. git revert is for undoing changes on shared branches — it adds a new commit rather than rewriting history.
- git reflog saves you after a bad --hard reset — commits you reset past are still recoverable for 30 days via reflog. Uncommitted working directory changes are not.
- --soft: HEAD moves back, changes stay staged — ready to recommit
- --mixed (default): HEAD moves back, changes unstaged — in working directory only
- --hard: HEAD moves back, changes discarded — working directory matches target commit
Production Incident
git reset --hard HEAD~5 to undo the last 5 commits (including the bad one and 4 good ones).
3. They force-pushed: git push --force origin main.
4. Eight teammates had already pulled those 5 commits and had feature branches based on them.
5. On their next fetch, origin/main pointed to the reset state (before the 5 commits).
6. Their feature branches were based on commits that no longer existed on the remote.
7. Git saw the branches as diverged — old commits vs new (reset) commits had different histories.
8. One developer merged the diverged branches, creating a merge commit with duplicate changes.
9. The CI pipeline built from the force-pushed main without the 5 commits.git revert on the specific bad commit: git revert <bad-commit-hash>.
3. Pushed the revert commit normally: git push origin main. No force-push needed.
4. All 8 developers ran git pull to get the revert commit — clean fast-forward, no divergence.
5. Team rule: never reset or force-push main. Use git revert for undoing on shared branches.
6. Added branch protection on GitHub to prevent force-pushes to main.Production Debug GuideSystematic recovery paths for accidental resets, lost work, and shared-branch resets.
git fetch origin to get the reset remote state.
4. Hard-reset: git reset --hard origin/main to align with the remote.
5. If you had local commits on top of the old main: cherry-pick them onto the new base.git fsck --unreachable | grep blob pull. Some had local may status before --hard.git reset moves the current branch pointer to a specified commit. The three modes (--soft, --mixed, --hard) control what happens to your working directory and staging index after the move. Only --hard discards changes. The other two are non-destructive.
The distinction between reset and revert is critical: reset rewrites history by moving the branch pointer backward. Revert adds a new commit that undoes a previous change. Reset is for local cleanup. Revert is for shared branches where rewriting history would break teammates.
Common misconceptions: that all reset modes lose work (only --hard does), that reset is the same as revert (reset rewrites history, revert preserves it), and that --hard is unrecoverable (committed work is recoverable via reflog for 30 days, but uncommitted working directory changes are not).
The Three Modes: Hard, Soft, Mixed
All three modes move HEAD (and the current branch pointer) to the specified commit. The difference is entirely in what happens to the working directory and the staging index.
--soft: HEAD moves back. Working directory untouched. Staging index untouched. The changes from the undone commits are staged and ready to recommit. Use this for 'I committed too early' or 'I want to rewrite the last N commits into one'.
--mixed (default): HEAD moves back. Working directory untouched. Staging index cleared. Changes appear as unstaged modifications. Use this for 'I committed the wrong things' or 'I need to regroup and re-add selectively'.
--hard: HEAD moves back. Working directory changed to match the target commit. Staging index cleared. Any uncommitted changes are gone. Use this when you genuinely want to throw away work. This is the only mode that can lose data.
# io.thecodeforge — Git Reset Modes # ───────────────────────────────────────────────────────────── # Current state: 3 commits ahead of where we want to be # ───────────────────────────────────────────────────────────── git log --oneline # d4f8b3c (HEAD) Add unit tests # 9c3e8a2 Fix null check # 7b2d4f1 Add backoff logic # a3f9c2e (origin/main) Initial PaymentRetryService # ───────────────────────────────────────────────────────────── # --soft: undo commits, keep changes STAGED # ───────────────────────────────────────────────────────────── git reset --soft HEAD~3 git status # Changes to be committed: all 3 commits' changes are staged # Use case: squash 3 commits into 1 clean commit git commit -m "feat(payment): add retry logic with exponential backoff and unit tests" # ───────────────────────────────────────────────────────────── # --mixed (default): undo commits, keep changes UNSTAGED # ───────────────────────────────────────────────────────────── git reset HEAD~3 # or: git reset --mixed HEAD~3 git status # Changes not staged for commit: all 3 commits' changes in working dir # Use case: regroup changes before re-adding selectively git add src/main/java/io/thecodeforge/payment/PaymentRetryService.java git commit -m "feat(payment): add retry logic with exponential backoff" # ───────────────────────────────────────────────────────────── # --hard: undo commits, DISCARD changes entirely # ───────────────────────────────────────────────────────────── git reset --hard HEAD~3 git status # nothing to commit, working tree clean # WARNING: the changes from those 3 commits are gone # ───────────────────────────────────────────────────────────── # Reset to a specific commit hash # ───────────────────────────────────────────────────────────── git reset --soft a3f9c2e git reset --hard a3f9c2e # ───────────────────────────────────────────────────────────── # Reset a single file (unstage it from the index) # ───────────────────────────────────────────────────────────── git reset HEAD -- src/main/java/io/thecodeforge/payment/PaymentService.java # This unstages the file but does NOT change the working directory content.
On branch feature/payment-retry
Changes to be committed:
modified: src/main/java/io/thecodeforge/payment/PaymentRetryService.java
new file: src/test/java/io/thecodeforge/payment/PaymentRetryServiceTest.java
# --hard result:
HEAD is now at a3f9c2e Initial PaymentRetryService
- --soft: changes stay staged — ready to recommit immediately
- --mixed: changes unstaged — in working directory, need to re-add selectively
- --hard: changes discarded — working directory matches the target commit
- Only --hard risks data loss. --soft and --mixed are non-destructive undo buttons.
git reset vs git revert: Which to Use
This is the question that trips up the most people in interviews and in practice. The key distinction: reset rewrites history, revert adds to history.
git reset moves the branch pointer backward. The commits you reset past effectively disappear from the branch. This is fine on your local feature branch. On a shared branch, it rewrites public history and requires a force push — which breaks teammates' local copies.
git revert <hash> creates a new commit that undoes the changes of the specified commit. History is preserved. The original commit is still there. You push normally. This is what you use on main, develop, or any shared branch.
Rule of thumb: reset for local cleanup on your own branches. Revert for undoing things on shared branches.
# io.thecodeforge — Reset vs Revert # ───────────────────────────────────────────────────────────── # RESET: rewrites history (local branches only) # ───────────────────────────────────────────────────────────── git reset --hard HEAD~1 # Undo last commit, discard changes git reset --soft HEAD~1 # Undo last commit, keep changes staged # If already pushed: requires force push — DON'T do on shared branches git push origin feature/my-branch --force-with-lease # ───────────────────────────────────────────────────────────── # REVERT: adds a new 'undo' commit (safe for shared branches) # ───────────────────────────────────────────────────────────── git revert HEAD # Undo last commit git revert a3f9c2e # Undo specific commit git revert HEAD~3..HEAD # Undo last 3 commits (creates 3 revert commits) git revert HEAD~3..HEAD --no-commit # Stage all reverts, commit once git push origin main # Normal push — history intact # ───────────────────────────────────────────────────────────── # DECISION: reset or revert? # ───────────────────────────────────────────────────────────── # Branch is local-only (not pushed)? -> reset is safe # Branch is shared (main, develop)? -> revert is required # Need to undo a specific commit? -> revert <hash> # Need to squash local commits? -> reset --soft HEAD~N
1 file changed, 47 deletions(-)
- Reset: moves branch pointer backward, commits disappear from branch
- Revert: creates new commit that undoes previous change, history preserved
- Reset on shared branch: requires force-push, breaks teammates
- Revert on shared branch: normal push, no breakage, clean fast-forward for teammates
git branch -r and check if the branch exists on the remote. If it does, someone else may have pulled it. Use revert. If it does not, reset is safe. When in doubt, use revert — it is never wrong, even on local branches.Recovering from a Bad --hard Reset
git reset --hard is the only mode that can lose data. But the data loss is not as total as it sounds — it depends on whether the changes were committed or not.
Committed changes are recoverable for 30 days via git reflog. Every time HEAD moves, Git records the movement in the reflog. When you reset --hard past a commit, that commit is still in the reflog. You can find it, create a branch pointing to it, and your work is back.
Uncommitted working directory changes are NOT recoverable via reflog. If you had modified files that were not staged or committed, and you ran --hard, those changes are gone. The only recovery path is editor local history (VS Code, IntelliJ) or filesystem-level recovery tools.
The safety habit: always run git status before --hard. If the working tree is not clean, stash first. If you are resetting past commits, check git reflog to confirm the commit hash is recorded before proceeding.
# io.thecodeforge — Recover from Bad --hard Reset # ───────────────────────────────────────────────────────────── # SCENARIO: You ran git reset --hard and lost committed work # ───────────────────────────────────────────────────────────── # Step 1: Find the lost commit in reflog git reflog # Output: # a3f9c2e HEAD@{0}: reset: moving to HEAD~3 # d4f8b3c HEAD@{1}: commit: Add unit tests # 9c3e8a2 HEAD@{2}: commit: Fix null check # 7b2d4f1 HEAD@{3}: commit: Add backoff logic # Step 2: Recreate a branch at the lost commit git branch recovery d4f8b3c # Or reset forward to restore the commits: git reset --hard d4f8b3c # Step 3: Verify the recovery git log --oneline -3 # d4f8b3c Add unit tests # 9c3e8a2 Fix null check # 7b2d4f1 Add backoff logic # ───────────────────────────────────────────────────────────── # SCENARIO: You ran git reset --hard and lost UNCOMMITTED work # ───────────────────────────────────────────────────────────── # Uncommitted changes are NOT in reflog. # Recovery options: # Option 1: Check if the changes were staged before reset # Staged changes may be recoverable as orphaned blobs: git fsck --unreachable | grep blob # This lists blob objects that are not referenced by any commit. # Some may be your lost file contents. # Option 2: Check editor local history # VS Code: right-click file > Local History # IntelliJ: right-click file > Local History > Show History # These may have snapshots of your uncommitted changes. # ───────────────────────────────────────────────────────────── # PREVENTION: Always check status before --hard # ───────────────────────────────────────────────────────────── git status # If dirty: git stash first, then reset, then git stash pop
# a3f9c2e HEAD@{0}: reset: moving to HEAD~3
# d4f8b3c HEAD@{1}: commit: Add unit tests
# 9c3e8a2 HEAD@{2}: commit: Fix null check
# 7b2d4f1 HEAD@{3}: commit: Add backoff logic
# git branch recovery d4f8b3c
# Branch 'recovery' set up to track local branch 'feature/payment-retry'.
# git log --oneline -3
# d4f8b3c Add unit tests
# 9c3e8a2 Fix null check
# 7b2d4f1 Add backoff logic
| Mode | HEAD moves? | Index (staging)? | Working dir? | Data loss? |
|---|---|---|---|---|
| --soft | Yes | Unchanged (changes staged) | Unchanged | No |
| --mixed (default) | Yes | Cleared (changes unstaged) | Unchanged | No |
| --hard | Yes | Cleared | Reverted to target | Yes — uncommitted work lost |
🎯 Key Takeaways
- --soft keeps changes staged, --mixed (default) unstages them, --hard discards them. Only --hard risks losing work.
- git reset is for local branch cleanup. git revert is for undoing changes on shared branches — it adds a new commit rather than rewriting history.
- git reflog saves you after a bad --hard reset — commits you reset past are still recoverable for 30 days via reflog. Uncommitted working directory changes are not.
- Never force-push after resetting a shared branch. The correct tool for shared branches is always git revert.
- Always run git status before --hard. If the working tree is not clean, stash first.
- The --soft mode is the fastest way to squash local commits: reset --soft HEAD~N then commit once.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat are the three modes of git reset and when would you use each?
- QA bad commit was pushed to main. How do you undo it without force pushing?
- QWhat is the difference between git reset and git revert? When is each appropriate?
- QA developer ran git reset --hard on main and force-pushed. Eight teammates now have diverged branches. Walk through the recovery process.
- QHow do you recover committed work after a git reset --hard? What about uncommitted work?
- QWhat is the difference between git reset HEAD <file> and git reset HEAD~1?
Frequently Asked Questions
What is the difference between git reset --hard and --soft?
Both move HEAD back to a previous commit. --soft keeps your changes staged in the index, ready to recommit. --hard discards all changes and reverts your working directory to match the target commit — any uncommitted work is permanently lost.
How do I undo the last commit without losing my changes?
Run git reset --soft HEAD~1 to undo the last commit while keeping all changes staged. Or git reset HEAD~1 (--mixed) to undo the commit and unstage the changes, leaving them in your working directory. Both are non-destructive.
When should I use git revert instead of git reset?
Use git revert on any branch that other people have already pulled from — main, develop, release branches. Revert creates a new commit that undoes the change without rewriting history. Reset rewrites history, which breaks teammates' local repos and requires a force push.
Can I recover work after git reset --hard?
Committed work is recoverable via git reflog for 30 days. Run git reflog to find the commit hash you reset past, then create a branch pointing to it. Uncommitted working directory changes are NOT recoverable via reflog — check your editor's local history (VS Code, IntelliJ) as a last resort.
What is the difference between git reset HEAD and git reset HEAD~1?
git reset HEAD <file> unstages a file from the staging index without changing the working directory. git reset HEAD~1 moves the branch pointer back one commit — it undoes the entire last commit. The tilde (~1) is the critical difference.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.