git reset — The Three Modes
A developer learns that
git reset --hard"undoes commits." They use it liberally — to discard changes, to move branches, to fix anything. One day they rungit reset --hardwith dirty working-directory changes they had not committed yet, and the working directory is wiped. Two hours of uncommitted work, gone. The reflog cannot recover uncommitted changes. They had confusedgit reset --hardwithgit reset --soft, which would have kept everything. Both "reset the branch," but they differ on what happens to your working directory and index.
git resetis three different commands sharing a name. The three modes (--soft,--mixed,--hard) all move branch refs, but they treat the index and working directory differently. Understanding the table of "what each mode touches" is the entire lesson — after which you pick the right one every time and never accidentally--hardaway uncommitted work.
The Core Behavior
git reset <commit> moves your current branch's ref to point at <commit>. What happens to the index and working directory is controlled by the mode flag:
| Mode | HEAD (branch) | Index | Working directory |
|---|---|---|---|
--soft | Moved | Unchanged | Unchanged |
--mixed (default) | Moved | Reset to match HEAD | Unchanged |
--hard | Moved | Reset to match HEAD | Reset to match HEAD |
Read that table until it sticks. Every git reset question reduces to which row you want.
Always think "three states" when you see reset: HEAD, index, working directory. --soft moves HEAD only. --mixed also resets the index. --hard ALSO resets the working directory (destroying uncommitted edits). Every "I lost work to reset" story is someone using --hard when they meant --soft or --mixed.
--soft: Move the Branch, Keep Everything Staged
git reset --soft HEAD~1
- Branch moves to HEAD~1.
- Index is unchanged — your staged changes are still staged.
- Working directory is unchanged.
Effect: it is as if the last commit never happened, but everything that was in that commit is now staged again, ready to be re-committed (perhaps as multiple smaller commits, or with a different message).
Use case: "Undo the last commit but keep the changes staged"
# You committed too early; want to regroup
git commit -m "wip"
# ... realize you want to split this into 3 commits
git reset --soft HEAD~1
# Last commit is gone; all its changes are back in the index
git status
# Changes to be committed:
# modified: file1.py
# modified: file2.py
# modified: file3.py
# Re-commit in smaller pieces
git reset HEAD file2.py file3.py # unstage some
git commit -m "feat: part 1"
git add file2.py
git commit -m "feat: part 2"
git add file3.py
git commit -m "feat: part 3"
Use case: "Combine the last N commits into one"
git reset --soft HEAD~3
# Three commits gone; all their combined changes still staged
git commit -m "feat: combined message"
# One new commit replaces the three
This is the poor-man's squash; much faster than interactive rebase for this specific task.
--mixed (Default): Move the Branch, Unstage But Keep Changes
git reset HEAD~1 # --mixed is default
# equivalent: git reset --mixed HEAD~1
- Branch moves to HEAD~1.
- Index reset to match HEAD~1 (your previously staged changes are now unstaged).
- Working directory unchanged.
Effect: the commit is gone, your changes are back in your working directory as unstaged edits.
Use case: "Undo the last commit; let me re-stage carefully"
git commit -m "feat: big change"
# ... realize the commit is a mess; want to rebuild with git add -p
git reset HEAD~1
git status
# Changes not staged for commit:
# modified: file1.py
# modified: file2.py
# Selectively stage
git add -p
git commit -m "feat: logical piece 1"
git add -p
git commit -m "feat: logical piece 2"
Use case: "Unstage files"
git add huge-file.bin # accidentally staged a binary
git reset HEAD huge-file.bin
# Unstaged; the file is untouched in working dir
# Modern equivalent:
git restore --staged huge-file.bin
git reset HEAD <file> is the canonical "unstage" command. Some teams prefer the newer git restore --staged <file> for clarity.
--hard: Move the Branch, Nuke Everything
git reset --hard HEAD~1
- Branch moves to HEAD~1.
- Index reset to match HEAD~1.
- Working directory reset to match HEAD~1 — any uncommitted changes are destroyed.
Effect: as if the last commit never existed, and you have no pending work. Your working directory exactly matches HEAD~1.
Use case: "Throw away all work since the last commit"
# I experimented, made a mess, want to bail
git reset --hard HEAD
# Working dir matches HEAD. All uncommitted edits gone.
# Or to go back further:
git reset --hard HEAD~3
# Working dir matches HEAD~3. Last three commits gone AND any working changes.
Use case: "Reset my branch to match origin"
git fetch origin
git reset --hard origin/main
# My local main now exactly matches the remote.
# Any local commits on main that were NOT pushed are gone from the branch (reflog has them).
# Any uncommitted changes are GONE FOREVER.
Danger
--hard is the one that bites. Before running it, check:
git status
# ... uncommitted changes? they will be DESTROYED
Reflog saves committed work. Reflog does not save uncommitted working-directory changes. Once --hard wipes them, they are gone.
Before any git reset --hard, run git status. If there are modified or new files you have not committed, those will be lost. If you want to save them first, either commit them (even as wip, use --amend later), or git stash them. The reflog is a safety net for committed state, not for uncommitted state.
Visual Comparison
git reset <file> — The Special File Form
git reset HEAD <file> # unstage a file; working dir unchanged
# equivalent to: git reset --mixed HEAD -- <file>
# modern alternative: git restore --staged <file>
When you provide a file path, git reset does not move HEAD or the branch. It only unstages the file (the default --mixed applied to just that file in the index).
git reset --hard <file>
# error: --hard is INCOMPATIBLE with files
--hard and --soft do not work with file paths — they only work on the whole repo.
The Three Modes vs checkout/restore
# Change which commit HEAD points at, keep working dir
git reset --soft <commit>
# Change working dir and index, but don't move HEAD
git checkout <commit> -- <files>
# or: git restore --source=<commit> <files>
# Get a file from a specific commit without moving anything
git show <commit>:<file> > <file>
Relationship:
git resetmoves your branch's pointer.git checkout/git restoremove files without moving the branch.
If you only want to change file contents, use checkout/restore. If you want to change the branch's commit, use reset.
Scenarios and Their Modes
"I committed something I shouldn't have"
# Option 1: undo the commit, keep changes staged
git reset --soft HEAD~1
# Fix what you want, re-commit
# Option 2: undo the commit, unstage everything
git reset HEAD~1 # default --mixed
# Selectively re-stage with git add
# Option 3: undo the commit, discard ALL the changes
git reset --hard HEAD~1
# Changes are gone; lean on reflog if you change your mind
"I pushed a bad commit"
# DO NOT reset shared history. Instead:
git revert HEAD
# Creates a new commit that undoes the last one. Safe for shared branches.
git push
See Module 5 Lesson 1 for revert in detail.
"I want to unstage a file"
git reset HEAD <file>
# or
git restore --staged <file>
"I want to discard working-dir changes in a file"
git checkout -- <file>
# or
git restore <file>
"I want to abandon my whole branch and match origin"
git fetch origin
git reset --hard origin/<branch>
# Works, but DESTROYS any uncommitted changes. Check git status first.
"I want to split my last commit into multiple commits"
git reset HEAD~1 # default --mixed: commit undone, changes unstaged
git add -p # selectively stage first logical piece
git commit -m "feat: part 1"
git add -p # second piece
git commit -m "feat: part 2"
# ... etc
"I want to combine my last 3 commits"
git reset --soft HEAD~3
git commit -m "feat: combined message"
# One new commit replaces the three, with their combined changes
Recovery: "I reset --hard and need it back"
git reflog
# <new> HEAD@{0}: reset: moving to HEAD~5
# <old> HEAD@{1}: <last thing before reset>
git reset --hard HEAD@{1}
# Back to pre-reset state. Committed changes recovered.
But: uncommitted changes that were in your working directory at the time of --hard are NOT in the reflog. They were never committed, so no SHA exists to recover. Gone forever.
The only save for "reset --hard with uncommitted changes" is if your editor has autosaves, your IDE has local history (IntelliJ does, VS Code Timeline does), or you made a stash.
Config Tricks
# Warn on dangerous reset (not enabled by default)
git config --global advice.resetQuiet false
# More verbose output for reset operations
# Make reset safer on shared branches — refuse if work would be lost
# (there's no built-in flag; this is a useful hook or alias pattern)
alias safe-reset='git stash && git reset --hard $1 && git stash pop'
# Stash first so uncommitted work is saved before the reset
A common habit: git stash before any --hard reset as a reflex.
# Muscle memory:
git stash push -m "before-reset"
git reset --hard <commit>
# If you didn't need to reset, restore your work:
git stash pop
# If you did need to reset, drop the stash:
git stash drop
The stash is saved in the reflog even if dropped (for the 30-day window).
Reset vs Revert vs Restore
Three commands, distinct purposes:
| Command | Purpose | Touches history? |
|---|---|---|
git reset | Move the branch pointer; optionally touch index/working | YES (rewrites history) |
git revert | Create a NEW commit that undoes changes | NO (adds to history) |
git restore | Modify working-dir files without touching history | NO |
For local-only branches, reset is fine. For shared branches, revert is correct. For "I just want to undo a file edit," restore (or old checkout -- <file>).
The rule: reset for local; revert for shared. If in doubt about whether others have your commits, revert. reset rewrites history — only safe on branches you have not shared.
Key Concepts Summary
git resethas three modes:--soft,--mixed(default),--hard.- All three move the branch ref to a specified commit.
--softkeeps index and working directory unchanged.--mixedresets index to match the new HEAD; working directory unchanged.--hardresets both index and working directory — destroys uncommitted changes.git reset <file>only unstages the file (--mixed on one path); never moves HEAD.- Reflog saves committed history from reset mistakes. Uncommitted changes from
--hardare unrecoverable. - For shared branches, use
git revert(creates new undo commit) instead ofreset. git restore(modern) replaces many uses ofgit checkout.git stashbefore--hardis a useful reflex when uncertain.
Common Mistakes
- Running
git reset --hardwith uncommitted changes that were not yet committed. They are permanently gone; reflog cannot recover them. - Confusing
--softwith--hard. Mnemonic: "hard" is the aggressive one; "soft" is gentle. - Using
git reset --hardon a shared branch and force-pushing; collaborators' work is lost. - Running
git reset HEAD~1expecting--hardbehavior. Default is--mixed; your changes are still there in the working dir. - Trying
git reset --hard file.pyexpecting the file to be reset.--harddoes not accept file paths. - Resetting to the wrong SHA. Always check
git logfirst; alwaysgit statusafter. - Not stashing before a risky reset, losing in-flight work.
- Using
git resetwhen the right command isgit restore(unstage a file, discard an edit). - Assuming reflog has everything. It has committed history. Working-dir changes are ephemeral.
- Running
git reset --hard origin/mainafter a fetch to "sync" without reviewing what will be destroyed. Alwaysgit log HEAD..origin/mainto see what is being accepted, andgit statusto confirm no uncommitted work will be lost.