git reset (soft, mixed, hard)
git reset is the command that moves the current branch pointer to a different commit. Depending on the flag you pass, it can leave the index and working directory alone, reset the index too, or reset everything. It is one of Git's most powerful — and most easily misused — commands.
What reset actually does
Reset moves the branch pointer, then optionally the trees
Before: After git reset HEAD~1:
A ─ B ─ C ◀── main A ─ B ◀── main
▲ ▲
HEAD HEAD (C is now unreachable from main,
but still in the object DB)The three modes
Mode | Branch pointer | Index (staging) | Working dir | Use when |
|---|---|---|---|---|
| Moved | Untouched | Untouched | You want to redo the last commit with the same files staged. |
| Moved | Reset to target | Untouched | You want to uncommit and unstage, but keep your edits. |
| Moved | Reset to target | Reset to target | You want to throw away everything since the target. Destructive. |
--soft: uncommit, keep everything staged
Soft reset moves the branch pointer back but does not touch the index or working tree. The files you committed are still staged and ready to be re-committed.
Redo the last commit
git reset --soft HEAD~1 git status # Changes to be committed: # modified: src/util.js # Now you can re-commit with a different message, # combine in more files, split into multiple commits, etc.
--mixed (default): uncommit and unstage
Mixed reset moves the pointer and rewrites the index to match. Your working-tree edits stay, but they become unstaged modifications.
The everyday uncommit
git reset HEAD~1 # --mixed is implied git status # Changes not staged for commit: # modified: src/util.js
--hard: throw it all away
Discard the last commit AND its changes
git reset --hard HEAD~1 git status # nothing to commit, working tree clean # Wipe all local changes back to the last commit git reset --hard HEAD # Reset to whatever the remote has git fetch origin git reset --hard origin/main
Path-mode reset (per-file)
When you pass a path to git reset, it does not move the branch pointer. Instead it copies the file from the target commit into the index only. This is the old way to unstage a file (the modern way is git restore --staged).
Unstage one file the old way
git reset HEAD src/util.js # Index reverts for that one file; working tree untouched. # Modern equivalent: git restore --staged src/util.js
--keep: safer than --hard for switching
--keep resets the index and working tree to the target — but fails if local modifications would be overwritten. It is a safer way to jump to another commit when you suspect you might have unsaved work.
A safer hard
git reset --keep origin/main # error: Entry 'src/util.js' not uptodate. Cannot merge. # (so you know to stash or commit first)
reset vs revert vs checkout/restore
Command | What it moves | Rewrites history? | Safe to push? |
|---|---|---|---|
| Branch pointer (+ trees) | Yes | No — force-push required |
| Nothing — appends a new commit | No | Yes |
| Files only | No | Yes (only changes your files) |
Recovery if you reset to the wrong place
Every reset is logged in the reflog. If you regret a reset --hard, you can almost always come back as long as the lost commits had been committed at some point.
Undo a bad reset
git reflog
# 9c1a2b3 HEAD@{0}: reset: moving to HEAD~3
# 7f8d9e0 HEAD@{1}: commit: the work I just blew away
git reset --hard HEAD@{1} # back to safety
# Or with the SHA directly
git reset --hard 7f8d9e0