git reset: soft, mixed, hard
Advanced Git CLI
Why this matters
git reset is the command most often copied from the internet without understanding, and --hard is the flag most often copied along with it. The three modes differ in exactly one way, and once you see it, the command stops being frightening.
What all three have in common
Every mode moves the branch pointer to the commit you name. That part never varies. What varies is what happens to the change that was in the commit you just moved past.
The same repository, three times
Each example below starts from an identical repository: one commit on top of d3b92ec, adding a Windows section to the installation guide, with a clean working tree.
--soft
$ git reset --soft HEAD~1
$ git status --shortM docs/getting-started.md$ git log --oneline -1d3b92ec chore: add CI configuration and contributing guide
The commit is gone from the branch and its change is staged, ready to be committed again. The M is in the left column, which is the staging column (lesson 5.1).
Use it when the change was right and the commit was not: a bad message, a commit that should be two, a file you forgot.
--mixed, the default
$ git reset --mixed HEAD~1Unstaged changes after reset:
M docs/getting-started.md$ git status --short M docs/getting-started.md
The commit is gone and the change is in your files but not staged. The M moved to the right column. This is what plain git reset HEAD~1 does, since --mixed is the default.
Use it when you want to rebuild the commit from scratch, staging pieces deliberately.
--hard
$ git reset --hard HEAD~1HEAD is now at d3b92ec chore: add CI configuration and contributing guide$ git status --short
Nothing. The working tree is clean and the Windows section is gone from the file:
$ tail -3 docs/getting-started.md
- Read the [FAQ](faq.md).
- Read [CONTRIBUTING](../CONTRIBUTING.md) before making changes.The commit itself is still findable in the reflog, so the committed work is recoverable. Anything that was uncommitted when you ran it is not.
Which to use
| You want | Mode |
|---|---|
| To redo the last commit differently, keeping everything staged | --soft |
| To rebuild the commit from the files, staging by hand | --mixed (or just git reset) |
| To throw the commit and its changes away entirely | --hard, after git status |
| To unstage one file, not move the branch | git restore --staged <file> |
Naming the target
Reset takes any commit reference:
| Target | Means |
|---|---|
HEAD~1 |
One commit back; HEAD~3 is three |
d3b92ec |
That exact commit |
origin/main |
Whatever the remote has, which is the "make my branch match the server" move |
ORIG_HEAD |
Where you were before the last big operation |
HEAD@{2} |
A position from the reflog (lesson 12.1) |
git reset --hard origin/main is worth knowing as the "start again from the server" command. It is also the one that has destroyed the most unfinished work, because it discards everything local without asking.
The safety habit
Three seconds of typing prevents the only irreversible outcome:
git status— is anything uncommitted? If yes,--hardwill destroy it.git stash -uif there is something to keep.git log --oneline -3— confirm you are aiming at the commit you think you are.- Then reset.
How to do it
$ git reset --soft HEAD~1 # keep everything staged
$ git reset HEAD~1 # keep changes in the files, unstaged
$ git reset --hard HEAD~1 # discard the changes toogit reset without a mode and without a target does something different again: git reset <file> unstages that file, which is the old spelling of git restore --staged.
Undo Last Commit in the Source Control ⋯ menu is git reset --soft HEAD~1. There is no interface for the other modes; the Command Palette's Git: Undo Last Commit is the same thing.
Right-click a commit in the Log → Reset Current Branch to Here…, then choose the mode in the dialog: Soft, Mixed, Hard and Keep, each with a one-line explanation. Keep is IntelliJ's extra: like hard, but it refuses if you have uncommitted changes.
Nothing on the platform resets a branch; the closest is deleting and recreating it, which is why protected branches exist. Undoing pushed work is revert territory.
The same: no reset in the interface, deliberately.
Common mistakes
--hardwith uncommitted work open, which is the one unrecoverable outcome here.- Using reset on a pushed branch, which needs a force push to publish (lesson 12.8).
- Confusing
git reset <file>withgit reset --hard. The first unstages a file; the second rewrites everything. - Counting
HEAD~nwrongly. Check withgit log --onelinefirst; a hash is safer than a count. - Reaching for reset when
git revertis the correct answer.
Try it yourself
Goal: run all three modes from the same starting point and watch the difference.
- Make a commit adding two lines to
docs/faq.md. Note the hash of the commit before it. - Run
git reset --soft HEAD~1, thengit status --short. Note which column theMis in. - Re-commit, then run
git reset HEAD~1and checkgit status --shortagain. - Re-commit once more, then run
git reset --hard HEAD~1and check bothgit statusand the end of the file. - Recover the commit from the reflog, so the exercise leaves nothing behind.
Expected result: staged, then unstaged, then gone; and the reflog brings the commit back in step 5.
Show solution
The M column in steps 2 and 3 is the whole difference between soft and mixed, and step 4 shows what hard adds. Step 5 is the reassurance: the commit was recoverable throughout, because it had been committed. Had you run step 4 with unsaved edits in the file, those would not have been.