DevOps & Workflow5 min read

    How to use git reset

    Share

    git reset moves the current branch pointer to a different commit. Depending on the mode you pass, it also rewrites the staging area (the index) and your working tree. Most "I broke my repo with reset" stories come from confusion about which of those three things is being touched.

    This article is the mental model that makes git reset boring.


    The three things git reset can change

    Every commit in git is a snapshot of three connected pieces of state:

    1. HEAD / branch pointer — which commit is the tip of your current branch.
    2. Index — the staging area (git add writes here).
    3. Working tree — the actual files on disk.

    The mode flag tells git reset how far to reach:

    Mode HEAD moves Index updated Working tree updated
    --soft yes no no
    --mixed (default) yes yes no
    --hard yes yes yes

    That table is the whole feature.


    --soft — undo a commit, keep everything staged

    git reset --soft HEAD~1
    

    Removes the most recent commit. Every file change is left staged, ready to be re-committed. Use this when:

    • You committed too early.
    • The message was wrong and you want to redo it from scratch.
    • You want to combine several commits into one (without rebase).
    git reset --soft HEAD~3   # squash the last 3 commits into a fresh commit
    git commit -m "feat: search filter UI"
    

    --mixed — undo a commit, keep changes unstaged

    git reset HEAD~1          # --mixed is the default
    git reset --mixed HEAD~1  # explicit
    

    Removes the most recent commit. File changes are kept in the working tree but unstaged. Use this when:

    • You want to re-pick which changes to commit (git add -p).
    • You want to inspect the changes again before committing.

    It's also the default for git reset <path>, which un-stages a single file:

    git reset path/to/file    # un-stage path/to/file
    

    (git restore --staged path/to/file is the modern equivalent and is clearer.)


    --hard — undo a commit AND throw the changes away

    git reset --hard HEAD~1
    

    Removes the most recent commit, clears the index, and overwrites the working tree to match. Any uncommitted changes — staged or not — are gone.

    This is destructive. Use only when:

    • The work is genuinely garbage you don't want.
    • You're resetting to a known-good remote ref to discard local mistakes (git reset --hard origin/main).
    • You've separately confirmed there's nothing valuable in the working tree.

    Always run git status first. A --hard over uncommitted work that you needed is recoverable for tracked files via git reflog, but not for untracked files. Those are gone.


    What can you reset to?

    Anything git can resolve to a commit:

    git reset --soft HEAD~1               # one commit back
    git reset --soft HEAD~5               # five commits back
    git reset --hard origin/main          # match the remote main
    git reset --hard <sha>                # a specific commit
    git reset --soft main                 # the tip of main
    

    HEAD~N and HEAD^N are different — HEAD~3 means "three commits back along the first parent", which is what you usually want.


    Resetting one file

    If you give git reset a path, it only updates the index for that file (it cannot move HEAD when given paths):

    git reset HEAD path/to/file       # un-stage one file
    git reset <sha> -- path/to/file   # set the index version of file to that commit
    

    For most "un-stage" cases, prefer the modern verb:

    git restore --staged path/to/file
    

    Practical recipes

    Squash the last N commits into one fresh commit

    git reset --soft HEAD~N
    git commit -m "feat: ..."
    

    Throw away local changes and match the remote

    git fetch origin
    git reset --hard origin/main
    

    Move a branch to point at a different commit

    git switch feature/x
    git reset --hard <sha>
    

    Un-commit but keep changes for a different branch

    git reset --soft HEAD~1
    git stash
    git switch other-branch
    git stash pop
    

    Un-stage everything

    git reset
    

    Reset vs revert vs checkout/restore

    These are constantly confused. Quick disambiguation:

    • reset — moves the current branch pointer (and optionally the index / working tree). Rewrites local history.
    • revert — adds a new commit that undoes a previous commit. Safe on shared branches.
    • checkout / restore — restores files in the working tree from a given source, without moving any branches.

    If commits have been pushed, your default tool is revert, not reset.


    Recovering from a bad reset

    git reset --hard doesn't immediately delete commits — it just moves the branch pointer. The orphaned commits stay around for ~30 days (controlled by gc.reflogExpire).

    git reflog
    # find the SHA from before the reset, e.g. abc1234
    git reset --hard abc1234
    

    If reflog no longer has it, try:

    git fsck --lost-found
    

    Both work for committed work. Untracked files that were wiped by --hard are not recoverable through git.


    Pitfalls

    • git reset --hard will not warn you. Always git status first.
    • Don't reset shared history. If the commits have been pushed and pulled by others, use git revert instead.
    • Force-pushing after a reset rewrites history on the remote. Use --force-with-lease, never plain --force, and don't do it on main / master without coordination.
    • Reset doesn't touch untracked files. A --hard resets tracked files but leaves untracked files alone — except when they would conflict with the new state, in which case it errors.

    Summary

    • --soft keeps changes staged, --mixed keeps them unstaged, --hard wipes them.
    • HEAD always moves; only the index and working tree differ between modes.
    • Use revert for pushed commits — reset is for local cleanup.
    • After a destructive reset, git reflog is the recovery tool.
    • When unsure, copy your branch first: git branch backup-before-reset.