Intermediate 3 min · March 30, 2026

Git Reset — Hard Reset on Main Lost 8 Developers 2 Hours

Eight developers saw 'Your branch and origin/main have diverged' after a git reset --hard and force push.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • --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
Plain-English First

Git reset moves the branch pointer backwards in history. The three modes answer one question: what happens to the changes in the commits you're 'undoing'? Hard: throw them away. Soft: keep them staged. Mixed (the default): unstage them but keep the files. Hard is permanent. Soft and mixed are undo buttons.

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/ResetModes.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 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.
Output
# --soft result:
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
The Three Modes Answer One Question: What Happens to the Changes?
  • --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.
Production Insight
The --soft mode is the most underused production reset. When a developer makes 5 WIP commits and wants to squash them into one clean commit before a PR, they reach for interactive rebase. But --soft HEAD~5 followed by a single git commit achieves the same result in two commands with no interactive editor. The changes are staged and ready — just write a clean commit message and commit. This is faster and less error-prone than interactive rebase for simple squash operations.
Key Takeaway
Three modes, one question: what happens to the changes? --soft keeps them staged. --mixed (default) unstages them. --hard discards them. Only --hard risks data loss. Use --soft for squashing commits, --mixed for regrouping, --hard for throwing away work.

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/git/ResetVsRevert.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 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
Output
[feature/payment-retry 9f2c4a1] Revert "Add PaymentRetryService with exponential backoff"
1 file changed, 47 deletions(-)
Reset Rewrites History. Revert Adds to History.
  • 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
Production Insight
The reset-vs-revert decision is about whether the branch is shared. If you are the only person who has seen the commits, reset is fine. If anyone else has pulled those commits, revert is required. The test: run 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.
Key Takeaway
Reset rewrites history — safe on local branches only. Revert adds to history — safe on all branches. Rule of thumb: reset for local cleanup, revert for shared branches. When in doubt, use revert — it is never wrong.

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/git/RecoverFromReset.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 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
Output
# git reflog
# 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
Committed Work Is Recoverable. Uncommitted Work Is Not.
  • Committed work: recoverable via git reflog for 30 days
  • Uncommitted work: NOT in reflog — permanently lost if not staged
  • Staged but uncommitted: may be recoverable via git fsck --unreachable
  • Prevention: always git status before --hard. If dirty, stash first.
Production Insight
The reflog safety net is the reason --hard is less dangerous than it appears — but only for committed work. The real danger is uncommitted changes. A developer modifies 3 files, does not stage or commit, runs --hard to 'start fresh', and the changes are gone. No reflog entry. No recovery path except editor local history. The discipline: never run --hard without checking git status first. If dirty, stash. If not dirty, --hard is safe because reflog has your back.
Key Takeaway
Committed work is recoverable via reflog for 30 days. Uncommitted working directory changes are NOT recoverable. Always run git status before --hard. If dirty, stash first. The reflog safety net only covers committed work.
● Production incidentPOST-MORTEMseverity: high

git reset --hard on main + force push: 8 Developers Lose 2 Hours

Symptom
Eight developers reported 'Your branch and origin/main have diverged' on their next git commits on top of the reset commits that were now orphaned. One developer merged the diverged branches, creating a merge commit with duplicate changes. The CI pipeline built from the force-pushed main and deployed without the 5 commits that were intended to be temporary.
Assumption
The developer assumed that resetting main and force-pushing would cleanly undo the bad deploy. They did not realize that eight teammates had already pulled the 5 commits and built feature branches on top of them. They did not announce the force-push.
Root cause
1. A bad commit was pushed to main and deployed to production, causing errors. 2. The developer ran 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.
Fix
1. Immediate: the developer force-pushed again to restore the 5 commits (found via their own reflog). 2. Instead of reset, used 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.
Key lesson
  • git reset on a shared branch rewrites public history. Every teammate who has pulled the old commits must recover manually.
  • git revert is always the correct tool for undoing changes on shared branches. It adds a new commit without rewriting history.
  • Branch protection rules on GitHub/GitLab prevent force-pushes to main and develop. Configure them immediately.
  • If you accidentally force-push to main, announce it immediately and coordinate recovery before anyone merges the diverged state.
Production debug guideSystematic recovery paths for accidental resets, lost work, and shared-branch resets.6 entries
Symptom · 01
'Your branch and origin/main have diverged' after a teammate reset and force-pushed main
Fix
1. Your local branch is based on old commits that no longer exist on the remote. 2. Do NOT merge — this creates duplicate commits. 3. Fetch: 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.
Symptom · 02
Ran git reset --hard and lost uncommitted work
Fix
1. Uncommitted working directory changes are NOT in reflog. They are permanently lost if not staged. 2. If the changes were staged before the reset: git fsck --unreachable | grep blob pull. Some had local may status before --hard.
Symptom · 03
Ran git reset --hard and lost committed work
Fix
Symptom · 04
Force-pushed after reset on shared branch — team has diverged branches
Fix
Symptom · 05
git reset HEAD <file> did not unstage the file
Fix
Symptom · 06
Used --hard when you meant --soft — changes appear gone
Fix
Git Reset Modes Compared
ModeHEAD moves?Index (staging)?Working dir?Data loss?
--softYesUnchanged (changes staged)UnchangedNo
--mixed (default)YesCleared (changes unstaged)UnchangedNo
--hardYesClearedReverted to targetYes — uncommitted work lost

Key takeaways

1
--soft keeps changes staged, --mixed (default) unstages them, --hard discards them. Only --hard risks losing work.
2
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.
3
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.
4
Never force-push after resetting a shared branch. The correct tool for shared branches is always git revert.
5
Always run git status before --hard. If the working tree is not clean, stash first.
6
The --soft mode is the fastest way to squash local commits
reset --soft HEAD~N then commit once.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between git reset --hard and --soft?
02
How do I undo the last commit without losing my changes?
03
When should I use git revert instead of git reset?
04
Can I recover work after git reset --hard?
05
What is the difference between git reset HEAD and git reset HEAD~1?
🔥

That's Git. Mark it forged?

3 min read · try the examples if you haven't

Previous
Git Cherry Pick: Apply Commits Across Branches
17 / 19 · Git
Next
Git Fetch vs Pull: What's the Difference