Git Reset: Hard, Soft and Mixed Explained
- --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.
git reset is one of those commands where the options have dramatically different consequences and the documentation is just dense enough to send people to Stack Overflow every time. I've taught Git to dozens of developers and 'git reset --hard and I lost my work' is in the top three support requests, every time.
The mental model that prevents mistakes: think of git reset as moving a bookmark in a book. The three modes control whether you keep the pages you're jumping over.
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.
# 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 # --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 # --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) git reset HEAD -- src/main/java/io/thecodeforge/payment/PaymentService.java
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
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: 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
1 file changed, 47 deletions(-)
| Mode | HEAD moves? | Index (staging)? | Working dir? | Data loss? |
|---|---|---|---|---|
| --soft | Yes | Unchanged (changes staged) | Unchanged | No |
| --mixed (default) | Yes | Cleared (changes unstaged) | Unchanged | No |
| --hard | Yes | Cleared | Reverted to target | Yes β uncommitted work lost |
π― Key Takeaways
- --soft keeps changes staged, --mixed (default) unstages them, --hard discards them. Only --hard risks losing work.
- git reset is for local branch cleanup. git revert is for undoing changes on shared branches β it adds a new commit rather than rewriting history.
- git reflog saves you after a bad --hard reset β commits you reset past are still recoverable for 30 days via reflog. Uncommitted working directory changes are not.
- Never force-push after resetting a shared branch. The correct tool for shared branches is always git revert.
β 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.
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?
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.
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.