In Git, what is the difference between reset --soft, --mixed, and --hard?
answer
- Three trees, three depths
- One flag stops early, one goes all the way
- Branch pointer always moves
- Only one mode touches files on disk
- --mixed is the default
basics
~20 sAll three move the current branch pointer to the target commit. --soft stops there, leaving the index and working tree untouched; --mixed also resets the index; --hard additionally overwrites the working tree, destroying uncommitted changes.
solid answer
~40 s`git reset <commit>` does up to three things, and the flag says how far it goes. It always moves the current branch (and therefore HEAD) to `<commit>`. `--soft` stops there: the index and working tree are untouched, so everything from the discarded commits is left staged — that is why `git reset --soft HEAD~3` followed by `git commit` squashes three commits into one. `--mixed`, the default, also rewrites the index to match the target, so those changes stay in the working tree but unstaged. `--hard` goes all the way and overwrites the working tree too, discarding uncommitted modifications permanently. Only `--hard` destroys work you never committed; untracked files survive even that, since removing them is `git clean`'s job. Commits left behind are still reachable through the reflog and `ORIG_HEAD`.
code
bash · 9 lines# squash the last three commits into one
git reset --soft HEAD~3
git commit -m "feat(auth): add refresh token rotation"
# undo the last commit but keep its edits unstaged
git reset HEAD~1 # --mixed is the default
# throw the branch and the working tree back to the remote tip
git reset --hard origin/maingo deeper
Memorise the ladder: soft touches only the branch pointer, mixed also clears the staging area, hard also rewrites your files. Know that hard is the one that loses uncommitted work.
Be ready to narrate reset as three ordered steps over HEAD, index, and working tree, and to show the squash idiom with --soft plus a fresh commit.
Show operational judgment: stash or commit before a --hard, know that untracked files survive it, and explain why discarded commits are still recoverable while discarded edits are not.
Frame it as a team risk question — which reset modes belong in shared runbooks and aliases, and when a revert-style forward fix is the correct choice over rewinding a branch at all.
## The three trees Every `git reset` question is really a question about which of Git's three snapshots changes. - **HEAD** — a pointer to the commit you are on. Normally HEAD is symbolic: it names a branch such as `refs/heads/main`, and the branch names the commit. "Moving HEAD" with reset therefore means moving the branch. - **The index** (staging area, stored in `.git/index`) — a complete snapshot of what the *next* commit would contain. `git add` copies working-tree content into it. - **The working tree** — the files on disk that you edit. ## What reset does, in three steps `git reset [<mode>] <commit>` performs up to three steps in order: 1. Point the current branch at `<commit>`. This step always happens (and it is the only step `--soft` performs). 2. Make the index match `<commit>`. Happens for `--mixed` and `--hard`. 3. Make the working tree match the index. Happens only for `--hard`. So the modes are cumulative: `--soft` ⊂ `--mixed` ⊂ `--hard`. `--mixed` is the default when you write `git reset <commit>` with no mode. ## --soft Only the branch pointer moves. Because the index still holds the newer content while HEAD now points at an older commit, `git status` shows all the differences as *staged*. This makes `--soft` the standard squash tool: `git reset --soft HEAD~3` then `git commit -m "one message"` collapses the last three commits into a single new commit with identical content. ## --mixed The branch moves and the index is rewritten from the target commit. The working tree is untouched, so the same differences reappear as *unstaged* changes. Two everyday uses: `git reset HEAD~1` to undo the last commit but keep its edits for reworking, and a bare `git reset` (target defaults to HEAD) to unstage everything at once. ## --hard The branch moves, the index is rewritten, and the working tree is forced to match. Any modification you had not committed is gone — there is no object in the database holding it, so no reflog or `git fsck` can bring it back. That is the one genuinely destructive mode. Two nuances people miss: - **Untracked files are not deleted** by `--hard`. Files Git never knew about stay on disk; removing those is `git clean`. - **Commits are not destroyed.** `git reset --hard HEAD~5` leaves those five commits in the object database, still listed in the reflog, until garbage collection eventually prunes unreachable objects. ## The path-limited form `git reset <commit> -- <path>` is a different operation: it copies the named paths from `<commit>` into the index and **never moves the branch pointer**. `--soft` and `--hard` are not accepted with paths. This is the old spelling of "unstage this file"; modern Git prints `git restore --staged <file>` in `git status` instead. ## Safety net Before a reset moves your branch, Git records the previous tip in `ORIG_HEAD`, and the move is appended to the branch's reflog. Recovering a mistaken reset therefore means pointing the branch back at the old commit — but only commits are covered. Uncommitted edits wiped by `--hard` were never objects and are unrecoverable, which is why `git stash` or a throwaway commit is worth the two seconds before a risky reset. ## Other modes `--keep` resets the branch and index but refuses if a file that differs between the two commits has local modifications, so it aborts instead of clobbering. `--merge` is similar and is mainly used to bail out of a conflicted merge. Both are safer middle grounds that senior candidates sometimes mention; `--soft`, `--mixed`, and `--hard` are what interviews actually test.
- How would you undo a reset that moved your branch to the wrong commit?Point the branch back at its old tip. Git writes the pre-reset position to `ORIG_HEAD` and appends the move to the branch's reflog, so `git reset --hard ORIG_HEAD` or a reset to the reflog position restores it. This recovers commits only — working-tree edits destroyed by `--hard` were never stored as objects and are gone.
- What does git reset <commit> -- <path> do differently?With a pathspec, reset never moves the branch pointer. It only copies the named paths out of `<commit>` into the index, leaving the working tree alone — the classic way to unstage a file. `--soft` and `--hard` are rejected in this form, since there is no branch move and no working-tree step to perform.
- Which mode would you use to squash the last three commits?`git reset --soft HEAD~3` followed by `git commit`. The branch rewinds three commits while the index keeps the final content, so a single new commit reproduces exactly the tree you had. `--mixed` would work too but forces you to re-add everything first, and `--hard` would throw the content away.
saying these in an interview costs you the question
- Claiming reset --hard deletes the commits from the database
- Saying --soft leaves the branch pointer where it was
- Thinking reset --hard also removes untracked files
- Believing --mixed unstages by discarding working-tree edits
- Assuming git reset <path> moves the branch too