Intermediate 9 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 & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
May 23, 2026
last updated
1,663
articles · all by Naren
 ● 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
✦ Definition~90s read
What is Git Reset?

Git reset is a command that moves the current branch pointer backward in the commit graph, effectively rewriting history by discarding commits from your branch. Unlike git revert, which creates a new commit that undoes changes while preserving history, git reset surgically removes commits from your branch's timeline.

Git reset moves the branch pointer backwards in history.

This makes it dangerous on shared branches because anyone who has pulled those commits will have divergent histories. The command operates in three modes: --soft (keeps all changes staged), --mixed (keeps changes unstaged, default), and --hard (discards all changes entirely).

A --hard reset is the nuclear option — it wipes both commits and working directory changes, which is why losing 8 developers for 2 hours happens when someone runs it on main. Recovery is possible if you have the commit hash from reflog, but only if no garbage collection has run.

For shared branches, always use git revert instead; for local work-in-progress, git reset is fine. The rule of thumb: never reset a commit that has been pushed to a shared branch unless you're prepared to force-push and coordinate every teammate to rebase their work.

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).

What Git Reset Actually Does to Your Commit Graph

Git reset is a command that moves the current branch pointer to a specified commit and optionally updates the index (staging area) and working tree. It rewrites history by changing where your branch points — it does not delete commits immediately, but orphaned commits become eligible for garbage collection. The three modes (--soft, --mixed, --hard) control how far the change propagates: --soft only moves HEAD, --mixed resets the index, --hard resets both index and working tree.

In practice, --hard is the most destructive because it discards uncommitted changes and resets tracked files to the target commit's state. Unlike revert, which creates a new commit that undoes changes, reset actually moves the branch pointer backward. This means any commits after the target become unreachable from that branch — they still exist in the object store for ~30 days (default gc expiry) but are invisible to normal git log.

The primary use case is cleaning up local branches before pushing, or undoing commits that haven't been shared. On shared branches like main, reset is dangerous because other developers' histories diverge. The moment you force-push a reset, every collaborator must rebase or re-clone — a single --hard reset on main can cost a team hours of recovery work.

Reset ≠ Revert
Reset rewrites history by moving the branch pointer; revert creates a new commit that undoes changes. Never reset a branch others have pulled from.
Production Insight
A developer runs git reset --hard HEAD~1 on main to remove a bad commit, then force-pushes. All other developers' local main branches now point to a commit that no longer exists in the remote. Their next git pull fails with 'rejected — non-fast-forward'. The team spends 2 hours rebasing each local branch and re-syncing CI/CD pipelines.
Key Takeaway
Never use --hard on a branch that has been pushed and pulled by others.
Always use git revert to undo public commits — it preserves history and avoids force-push chaos.
If you must reset a shared branch, coordinate a team-wide freeze and provide a recovery script.
Git Reset Modes and Safe Usage Guide THECODEFORGE.IO Git Reset Modes and Safe Usage Guide Understanding hard, soft, mixed reset vs revert and recovery Three Reset Modes Hard: discards commits & changes; Soft: keeps index; Mixed: unstages git reset vs git revert Reset rewrites history; revert creates new commit, safe for shared Never Reset Shared Branch Rewriting public history causes team chaos and lost work Recover from Hard Reset Use git reflog to find lost commits and reset back Undo Uncommitted Changes git checkout -- or git restore to discard working tree Revert Shared Commit git revert creates inverse commit, preserves history ⚠ Hard reset on main branch lost 8 developers 2 hours Always use git revert for shared branches; never git reset --hard THECODEFORGE.IO
thecodeforge.io
Git Reset Modes and Safe Usage Guide
Git Reset

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.

Undo Uncommitted Changes Before They Go Live

You've got dirty files — unstaged edits, staged garbage, maybe both. Don't panic. Before you reach for git clean -fd and nuke everything, understand what you're actually killing. git checkout -- <file> restores a file to its last committed state, discarding uncommitted changes. It's destructive. No undo button. Your changes are gone. Use git diff first to see what you're about to lose. If you want to stash those changes for later, use git stash instead — it tucks them away on a stack so you can pop them back later. This is not a feature for reverting shared history; it's for cleaning your local workspace before a merge, a rebase, or a deploy. The rule: if you haven't committed it, you can't recover it unless you stash it. Production engineers keep a mental model of three states: working tree, index, and HEAD. Knowing which one is dirty saves your ass.

For files you've already staged (git add), git reset HEAD <file> unstages them without touching the working tree. That puts you back to square one — dirty working tree but clean index. Then you can decide: git checkout -- <file> to discard everything, or fix the file and re-stage. Never skip the diff. You'll thank me when you don't accidentally nuke a config file with API keys.

DiscardLocalChanges.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — devops tutorial

// Check what's dirty before discarding
git status
# modified:   src/config/database.yml
# modified:   src/lib/authenticator.py

// View exact changes
git diff src/config/database.yml
# -password: "old_db_pass"
# +password: "new_db_pass"

// Discard unstaged changes for one file
git checkout -- src/config/database.yml

// Unstage a staged file, then discard
git reset HEAD src/lib/authenticator.py
git checkout -- src/lib/authenticator.py

// Verify clean state
git status
# nothing to commit, working tree clean
Output
nothing to commit, working tree clean
Production Trap:
git checkout -- . with a dot will nuke all unstaged changes in the entire repo. One typo, hours of work gone. Always target specific files.
Key Takeaway
Diff first, then discard. Use stash if you might need it later. Never checkout blind.

Revert a Shared Commit Without Rewriting History

You pushed a commit that broke production — maybe it introduced a bug, maybe it deleted a critical file. Your first instinct might be git reset --hard HEAD~1 && git push --force-with-lease. Don't. That's rewriting shared history. Everyone else who pulled that commit now has a detached HEAD and a broken merge. The correct tool is git revert. It creates a new commit that undoes the changes of the target commit. No history rewrite. No force push. No teammates getting wrecked. git revert HEAD creates a reverse commit and opens your editor for a message. You can also revert multiple commits in one shot: git revert HEAD~3..HEAD reverts the last three commits, creating three new commits in reverse order. If you want a single revert commit for multiple commits, use git revert -n HEAD~3..HEAD to stage all the changes, then commit them manually.

This is the safe hammer for commits that have been pushed to a shared branch like main or release. It leaves a clear audit trail. Anyone looking at the log sees: commit A introduced the bug, commit B reverted it. That's good for blame, good for compliance, good for your sanity. Never force-push a shared branch to undo history. That's how you get paged at 3 AM.

RevertSharedCommit.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — devops tutorial

// Revert the last commit
git revert HEAD
# [main 5f3a2b1] Revert "Add new pricing endpoint"
# 1 file changed, 20 deletions(-)

// Revert multiple commits (creates one revert per commit)
git revert HEAD~3..HEAD
# [main a1b2c3d] Revert "Fix rate limiting"
# [main e4f5g6h] Revert "Add logging middleware"
# [main i7j8k9l] Revert "Deploy to staging"

// Revert multiple commits as a single commit
git revert -n HEAD~3..HEAD
git commit -m "Revert last 3 commits that broke auth"
# [main m0n1o2p] Revert last 3 commits that broke auth

// Push safely (no force)
git push origin main
# To github.com:acme/payments.git
#    a1b2c3d..m0n1o2p  main -> main
Output
To github.com:acme/payments.git
a1b2c3d..m0n1o2p main -> main
Senior Shortcut:
Use git revert --no-edit to skip the editor and auto-generate a revert message. Saves time in emergency patches.
Key Takeaway
Revert on shared branches to preserve history. Reset only on local or unreleased branches.

Discard Uncommitted Changes to a File — The Surgical Approach

A single config file is corrupted. You don't want to reset the branch. You don't want to revert a commit. You just want that one file back to its last committed state. Use git restore <file> — it's the modern replacement for git checkout -- <file>. It's explicit, it's safe, and it only touches the working tree. git restore --staged <file> unstages a file without destroying your local edits. git restore --source=HEAD~1 <file> restores a file to its state from a previous commit. This is your scalpel for surgery on a dirty working tree. No muss, no fuss, no collateral damage.

But here's where juniors burn themselves: git restore . restores all files in the current directory. That's a shotgun blast. You wanted one config file, now you've undone all your local work. Always specify the path. Always run git status first to see what you're about to destroy. If you have a mix of files you want to keep and files you want to revert, git add the keepers first, then git restore . — it only touches uncommitted files that aren't staged. That's the pro move: stage what you want, restore everything else.

SurgicalFileRestore.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — devops tutorial

// Check current state
git status
# modified:   deployment/production.yml
# modified:   src/handlers/checkout.py

// Stage the file you want to keep
git add src/handlers/checkout.py

// Restore the corrupted config to last commit
git restore deployment/production.yml

// Verify
git status
# modified:   src/handlers/checkout.py

// Alternatively, restore from a specific commit
git restore --source=HEAD~2 src/handlers/checkout.py
# restores checkout.py to how it looked 2 commits ago
Output
modified: src/handlers/checkout.py
Senior Shortcut:
Pair git restore with git stash for selective undo: stash changes to a file, restore it, then pop the stash if you want the edits back.
Key Takeaway
Restore specific files. Never restore a directory without staging what you want to keep.

Why You Should Never git reset a Shared Branch

Hard resets rewrite history. When you force push a reset branch, every teammate who pulled the old commits now has a divergent graph. Git won't merge cleanly — you'll get fork warnings, conflicts, and confused devs blaming the build.

The WHY: git reset moves the branch pointer backward. On shared branches like main or develop, other developers' local refs still point to the deleted commits. The next git pull tries to merge those orphaned commits back in, resurrecting everything you tried to erase. This is not a rollback — it's a time bomb.

Instead, use git revert to create a new commit that undoes the changes. The branch history stays linear, your team can pull without errors, and CI doesn't break. Reserve hard resets strictly for local branches nobody else has touched. If you're asking "can I reset main?" — the answer is no.

revert-vs-reset-shared.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — devops tutorial

# Bad: rewrites shared history
- branch: main
  action: reset
  commits: 3
  result: "❌ DevOps on-call pages you at 2am"

# Correct: append a revert commit
- branch: main
  action: revert
  target_commit: "abc123"
  command: git revert abc123
  result: "✅ Team pulls, CI passes, sleep uninterrupted"
Output
❌ Team forks on pull
✅ Single clean revert commit created
Production Trap:
If you've already reset a shared branch and force-pushed, tell your team to rebase onto the new HEAD — not merge. git pull --rebase or they'll resurrect your mess.
Key Takeaway
Never hard reset a branch that other developers have pulled. Use revert to undo shared history safely.

git reset --soft: The Commit Squash You're Looking For

Most devs know --hard obliterates work and --mixed unstages files. But --soft is the unsung hero for squashing messy commit histories before a PR. It moves the branch pointer back but leaves every change staged. Exactly what you need when you've committed ten "fix typo" messages and want one clean diff.

The WHY: git reset --soft HEAD~3 keeps all your file changes perfectly staged. You don't lose a single line of work. You just undo the commits themselves. Then one git commit -m "Implement payment module" bundles everything into a single, reviewable chunk. No interactive rebase gymnastics, no cherry-picks, no force pushes if you haven't shared the branch yet.

This is the senior dev shortcut for local branch hygiene. Work iteratively, commit often as checkpoints, then soft-reset and squash before pushing for review. Your reviewer sees one coherent change. Your git log stays readable. Clean code wins code reviews.

soft-reset-squash.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — devops tutorial

# Before: messy local history
- commits:
    - "wip broken"
    - "fix import"
    - "add types"
    - "oops"

# Squash with soft reset
action: reset --soft HEAD~4
staged_files: 12  # all changes preserved

# Single clean commit
command: git commit -m "Implement audit logging"
result: "1 commit, clean diff, PR approved in 10 minutes"
Output
12 files changed, 347 insertions(+), 89 deletions(-)
Senior Shortcut:
Pair --soft with git diff --cached before committing to verify exactly what your squash captured. Saves you from accidentally bundling debug print statements.
Key Takeaway
Use git reset --soft to squash local commits into one clean PR-ready bundle without losing any changes.

Prerequisites

Before using git reset, confirm you have a solid understanding of Git's three-tree architecture: the working directory, the staging index, and the commit history. A reset modifies where HEAD points and can rewrite history, so only proceed on branches that are not shared with other team members. You should know how to check your current branch with git branch, inspect logs with git log --oneline, and verify remote tracking branches using git remote -v. Always ensure you have a clean working state—either stash or commit uncommitted changes first—since a --hard reset will permanently discard them. If you plan to reset to a remote branch, fetch the remote's latest state to avoid resetting to stale references.

prerequisites_check.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — devops tutorial
// 25 lines max
- name: Verify current branch
  shell: git branch --show-current
  register: current_branch
- name: Check for uncommitted changes
  shell: git status --porcelain
  register: dirty_state
- name: Stash if dirty
  shell: git stash
  when: dirty_state.stdout != ""
- name: Fetch remote tracking refs
  shell: git fetch origin
- name: Confirm branch not shared
  shell: git branch -r --list "origin/{{ current_branch.stdout }}"
Output
No uncommitted changes detected. Branch 'feature-x' is local only.
Production Trap:
Resetting a shared branch with --hard will force-push a rewritten history, breaking every other developer's clone. Always use git revert on public branches instead.
Key Takeaway
Only reset branches that exist exclusively on your local machine.

Steps to Reset a Git Branch to a Remote Repository

To align your local branch exactly with the remote version, first fetch the latest changes from the remote repository using git fetch origin. This downloads the up-to-date refs without merging them into your local branch. Next, inspect the remote branch reference—for example, origin/main—using git log origin/main --oneline to confirm the commit you want. Execute the appropriate reset: git reset --hard origin/main discards all local commits and uncommitted changes, making your HEAD point to the same commit as the remote. For a soft reset that preserves your local changes as staged, use --soft instead. Finally, verify the result with git status and git log --oneline -5. If the remote has diverged, a reset may discard valuable work; consider git merge --ff-only if you only need a fast-forward.

reset_to_remote.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — devops tutorial
// 25 lines max
- name: Fetch remote latest
  shell: git fetch origin
- name: Inspect remote HEAD
  shell: git log origin/main --oneline -3
- name: Hard reset to remote
  shell: git reset --hard origin/main
- name: Verify new state
  shell: git log --oneline -5
- name: Show working tree
  shell: git status --short
Output
HEAD is now at 9a4b3c7 Deploy hotfix v1.2. Working tree clean.
Safety Net:
Before a hard reset, tag your current HEAD with git tag backup-$(date +%s) so you can recover if the reset was premature.
Key Takeaway
Always fetch before resetting to ensure you're pointing to the true remote state, not a stale local cache.
● 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?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
May 23, 2026
last updated
1,663
articles · all by Naren
🔥

That's Git. Mark it forged?

9 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