Git Course 0%

git reset: soft, mixed, hard

Advanced Git CLI ≈ 12 min

What you will learn

  • What each mode leaves behind, demonstrated on identical repositories
  • Which mode to choose for which job
  • Why --hard is the one command in this section that can lose work

After this lesson you can

  • I can use reset deliberately instead of copying a command from an answer site

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.

git reset: soft, mixed and hard compared All three modes move the branch back to the target commit. They differ in what happens to the change that was in the undone commit. With --soft the change is left staged, ready to commit again, which git status shows as M in the left column. With --mixed, which is the default, the change is left in the working tree but unstaged. With --hard the change is discarded entirely, and because it was never anywhere else, it cannot be recovered. A caption notes that all three rewrite history, so they are only safe on commits you have not pushed. Before, in all three cases one commit on top of the target, working tree clean: … → d3b92ec → 95a0014 (HEAD, main) --soft Branch moves back Staging area holds the change Working tree untouched M docs/… staged Use it to redo the commit: split it, rename it, add a file. --mixed (default) Branch moves back Staging area cleared Working tree keeps the change M docs/… not staged Use it to start again from the files, staging by hand. --hard Branch moves back Staging area cleared Working tree overwritten (nothing: status is clean) The commit is findable in the reflog. Uncommitted work is not. All three rewrite history: use them only on commits you have not pushed. On a pushed commit, use git revert instead, which adds a commit rather than removing one.
All three move the branch. They differ in whether the change is left staged, left in your files, or discarded.

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

Terminal
$ git reset --soft HEAD~1
$ git status --short
M  docs/getting-started.md
Terminal
$ git log --oneline -1
d3b92ec 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

Terminal
$ git reset --mixed HEAD~1
Unstaged changes after reset:
M	docs/getting-started.md
Terminal
$ 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

Terminal
$ git reset --hard HEAD~1
HEAD is now at d3b92ec chore: add CI configuration and contributing guide
Terminal
$ git status --short

Nothing. The working tree is clean and the Windows section is gone from the file:

Terminal
$ 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:

  1. git status — is anything uncommitted? If yes, --hard will destroy it.
  2. git stash -u if there is something to keep.
  3. git log --oneline -3 — confirm you are aiming at the commit you think you are.
  4. Then reset.

How to do it

Terminal
$ 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 too

git 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.

Common mistakes

  • --hard with 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> with git reset --hard. The first unstages a file; the second rewrites everything.
  • Counting HEAD~n wrongly. Check with git log --oneline first; a hash is safer than a count.
  • Reaching for reset when git revert is the correct answer.

Try it yourself

Goal: run all three modes from the same starting point and watch the difference.

  1. Make a commit adding two lines to docs/faq.md. Note the hash of the commit before it.
  2. Run git reset --soft HEAD~1, then git status --short. Note which column the M is in.
  3. Re-commit, then run git reset HEAD~1 and check git status --short again.
  4. Re-commit once more, then run git reset --hard HEAD~1 and check both git status and the end of the file.
  5. 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.

Check yourself

1. What do all three reset modes do?
2. You want to redo the last commit with a better message and one more file, nothing pushed. Which?
3. What can git reset --hard destroy that the reflog cannot recover?

Key terms

Staging area Working tree HEAD Rewriting history