N
Naveenr.dev
Chapter 04
10 min read2026-08-08
📖 Git SeriesChapter 04 · 11 chapters

Undoing Changes: restore, reset, revert, and stash

Git has four different ways to undo something, and they are not interchangeable. Using the wrong one is how "undo my last commit" turns into "where did my work go" — understanding what each command actually moves (working directory, staging area, or history) fixes that.

Git undo: restore, reset, revert, and stash
Git undo: restore, reset, revert, and stash

I once watched a teammate try to "undo" a commit on a shared branch with git reset --hard HEAD~1, push with --force, and wipe out two other people's already-pushed work in the process.

The command did exactly what it was asked. That was the problem.

Git's undo commands are precise, but each one operates on a different layer — your working files, the staging area, or commit history itself. Picking the wrong one turns "I just want to undo this" into a much bigger mess than the thing you were trying to fix.

Three layers — three different kinds of "undo"

Every change in Git passes through three places: the working directory (your actual files on disk), the staging area (also called the index — where you assemble the next commit), and the commit history (permanent snapshots). git add moves changes from working directory to staging. git commit moves them from staging into history.

Whether you want restore, reset, or revert depends entirely on which of these three you're targeting.

git restore — working directory and staging only

restore only touches uncommitted changes. It never rewrites history, which makes it the safest undo command.

Discard uncommitted file changes

bash
git restore config.js

Reverts config.js back to what it looked like in the last commit. The file's content in your working directory is overwritten.

  • This is permanent for uncommitted changes. There's no reflog for working directory edits — once you restore an uncommitted change, it's gone.

Un-stage a file (keep the changes, just un-stage)

bash
git restore --staged config.js

Moves the file from "staged" back to "modified but not staged." Your actual file content is untouched.

  • restore is the modern replacement for git checkout -- file (discard changes) and git reset file (unstage). Those still work but restore makes the intent much clearer.

git reset — moving the branch pointer

reset moves your current branch's pointer to a different commit — usually backward. The three modes decide what happens to the staging area and working directory along the way.

  • Because reset actually rewrites history (your branch tip changes), the same shared-branch caution from rebase applies: reset your own unpushed commits freely, be very careful resetting anything already pushed and pulled.

--soft — un-commit, keep changes staged

bash
git reset --soft HEAD~1

Moves the branch pointer back one commit, but leaves everything from that commit still staged and ready to re-commit. The commit is gone from history, but every file change it contained is sitting in your staging area.

  • You'll use this when: You want to fix the commit message, or combine this commit with the next one.

--mixed — un-commit, un-stage (default)

bash
git reset --mixed HEAD~1
# or just:
git reset HEAD~1

Moves the pointer back and un-stages the changes, but leaves them in your working directory as uncommitted edits. The commit is gone, but no work is lost.

  • You'll use this when: You committed too early and want to add more changes before committing again.

--hard — un-commit, discard everything

bash
git reset --hard HEAD~1

Moves the pointer back and discards the changes from your working directory entirely. This is the only one of the three that can genuinely lose work.

  • The danger command. After --hard, the changes from the discarded commit are gone from your working directory. They're only recoverable via git reflog (covered in the next chapter) — but only if you haven't run git gc since.

Memory trick: soft keeps the most (staged), mixed keeps some (in working dir), hard keeps the least (gone).

git revert — undoing without rewriting history

reset rewrites history by moving the branch pointer backward. revert does the opposite: it adds a new commit that undoes the changes from an earlier one, leaving the original commit and everything after it intact.

bash
git revert a1b2c3d

Creates a new commit whose changes are the exact inverse of commit a1b2c3d. The original commit stays in history — nothing is rewritten, no commit hashes become invalid. History moves forward with a new commit that says "Revert [original message]."

  • revert is the correct choice for shared branches. If the bad commit is already on main and others have pulled it, revert is how you undo it safely. reset --hard + force-push is how you create a crisis.

Reverting a merge commit

bash
git revert -m 1 <merge-commit-hash>

-m 1 tells Git which parent to consider "mainline" — usually meaning "undo what this merge introduced relative to the branch it was merged into."

git stash — setting work aside temporarily

None of the above fit this situation: you're mid-change, need to switch branches for something urgent, but the work isn't ready to commit.

bash
git stash

Saves your uncommitted changes (staged and unstaged) onto a stack and restores your working directory to the last commit. Clean, as if nothing was in progress.

Switch branches, handle the urgent thing, then come back:

bash
git stash pop

Reapplies the most recent stash and removes it from the stack.

Managing the stash stack

bash
git stash list              # see all stashes
git stash show -p stash@{1} # see what's in stash #1
git stash apply stash@{1}   # apply without removing from stack
git stash drop stash@{1}    # delete a specific stash
  • Don't let stashes pile up. If you have more than two or three, it's a sign to commit work-in-progress to a branch and clean it up with interactive rebase later. Stashes are easy to forget and lose.

Quick reference — which one do I use?

SituationCommand
Uncommitted file change I want to discardgit restore <file>
File in staging area I want to un-stagegit restore --staged <file>
Last commit was private, I want to re-do itgit reset --soft HEAD~1
Last commit was private, I want to un-commit and keep editinggit reset HEAD~1
Last commit was private, I want to throw it awaygit reset --hard HEAD~1
Commit is on a shared branch and needs to be undonegit revert <hash>
Mid-work, need to context-switch temporarilygit stash / git stash pop

The key rule: Has anyone else already pulled the commit you're about to change? If yes → revert. Don't reset.

reset --hard and force-pushes both raise the same question: is the "lost" commit actually gone? Almost always, no. The next chapter covers git reflog — the local log that records every position your branches and HEAD have been at — along with git bisect for finding which commit introduced a bug.

Enjoyed this chapter?

Get an email when I publish the next chapter. No spam — just new technical deep-dives.

Comments

Share feedback or questions about this blog post.

No comments yet. Be the first to share your thoughts.