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.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- --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
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.
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.
- --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.
- 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.
- 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.
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.
git checkout -- . with a dot will nuke all unstaged changes in the entire repo. One typo, hours of work gone. Always target specific files.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.
git revert --no-edit to skip the editor and auto-generate a revert message. Saves time in emergency patches.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.
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.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.
git pull --rebase or they'll resurrect your mess.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 with git diff --cached before committing to verify exactly what your squash captured. Saves you from accidentally bundling debug print statements.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.
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.
git tag backup-$(date +%s) so you can recover if the reset was premature.git reset --hard on main + force push: 8 Developers Lose 2 Hours
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.- 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.
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.Key takeaways
Interview Questions on This Topic
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Git. Mark it forged?
9 min read · try the examples if you haven't