Skip to content
Home DevOps Git Reset — Hard Reset on Main Lost 8 Developers 2 Hours

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

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Git → Topic 17 of 19
Eight developers saw 'Your branch and origin/main have diverged' after a git reset --hard and force push.
⚙️ Intermediate — basic DevOps knowledge assumed
In this tutorial, you'll learn
Eight developers saw 'Your branch and origin/main have diverged' after a git reset --hard and force push.
  • --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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
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
Production Incident

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

A developer ran git reset --hard HEAD~5 on main to undo a bad deploy, then force-pushed. Eight teammates who had pulled those 5 commits got diverged branches. The team spent 2 hours coordinating recovery.
SymptomEight 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.
AssumptionThe 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 cause1. 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.
Fix1. 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 Guide

Systematic recovery paths for accidental resets, lost work, and shared-branch resets.

'Your branch and origin/main have diverged' after a teammate reset and force-pushed main1. 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.
Ran git reset --hard and lost uncommitted work1. 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.

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.sh · BASH
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
# 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
Mental Model
The Three Modes Answer One Question: What Happens to the Changes?
Soft = staged. Mixed = unstaged. Hard = gone.
  • --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.sh · BASH
1234567891011121314151617181920212223242526
# 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(-)
Mental Model
Reset Rewrites History. Revert Adds to History.
Reset = erase from history. Revert = undo in 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.sh · BASH
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
# 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.
git reflog saves committed work for 30 days after a --hard reset. But uncommitted working directory changes are NOT in reflog. If you run --hard with dirty files, those changes are permanently lost unless your editor has local history. Always run git status before --hard.
📊 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.
🗂 Git Reset Modes Compared
Choose based on what you want to happen to the changes in the undone commits.
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

  • --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

    Running git reset --hard on a shared branch and force-pushing — this rewrites public history and breaks every teammate who has fetched those commits.
    Using --hard when you meant --soft, losing staged changes — always run git status first to understand exactly what's in your working directory and index.
    Confusing 'git reset HEAD <file>' (unstage a file) with 'git reset HEAD~1' (undo a commit) — the presence or absence of a tilde makes a massive difference.
    Using reset instead of revert on main to undo a bad deployment — revert is always correct for shared branches; reset + force push will cause merge conflicts for every developer who pulled the bad commit.
    Running git reset --hard without checking git status — uncommitted working directory changes are permanently lost, not recoverable via reflog.
    Not using --force-with-lease when force-pushing after a local reset — --force blindly overwrites, --force-with-lease fails if the remote has commits you do not know about.

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.

🔥
Naren Founder & Author

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.

← PreviousGit Cherry Pick: Apply Commits Across BranchesNext →Git Fetch vs Pull: What's the Difference
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged