Everyday Git
Topic 09

Undoing Changes

Core

Three commands cover almost all "undo" in Git, and they are not interchangeable. git restore discards working-tree or staged changes, git reset moves the branch pointer and optionally the index and working tree with it, and git revert creates a new commit that inverts an old one.

Picking the wrong one has real costs: the wrong restore or reset destroys uncommitted work the reflog cannot recover, and a reset on shared history breaks everyone who already pulled it. The deciding question is almost always whether the thing you are undoing has been pushed.

Restoring Files

git restore <file> overwrites the working-tree copy with the staged (or HEAD) version, discarding your uncommitted edits to it. git restore --staged <file> does the opposite kind of undo — it unstages the file but leaves your changes on disk. And git restore --source=<commit> <file> pulls an older version of the file out of a specific commit. The first one loses edits, the second keeps them; choosing the wrong flag is how people lose work here.

Resetting the Branch

git reset moves HEAD to a chosen commit, and its flag decides how many of the three trees follow. --soft moves HEAD only, leaving the index and working tree intact (your changes become staged again). --mixed, the default, also resets the index but keeps the working tree. --hard moves all three — it overwrites the working tree, and any uncommitted changes it discards are gone, because they were never committed for the reflog to track.

Reverting Commits

git revert <commit> does not remove the commit. It computes the inverse of that commit's diff and records it as a brand-new commit on top. History stays intact and linear-forward, which is exactly why it is the safe way to undo something on a branch others have already pulled — no one's existing commits change.

Recovering From Mistakes

A bad reset feels catastrophic but usually is not. Every movement of HEAD is recorded in git reflog, retained about 90 days by default, so the "lost" commit is almost always sitting at an entry like HEAD@{1}. Find it there and git reset --hard <hash> puts you back. The one thing the reflog cannot save is work that was never committed — uncommitted working-tree changes a --hard discarded leave no ref behind.

Choosing the Right Tool

The clean rule: reset for local, unpushed cleanup, revert for anything already shared. Using revert on a purely local mistake just leaves a noisy "Revert" commit in history; using reset --hard on a pushed commit breaks every collaborator who pulled it. Match the tool to whether the history is yours alone or shared.

git reset vs git revert

git reset — rewrites history by moving the branch pointer backward and (with --hard) discarding work. It is clean and safe only on commits you have not pushed, because it makes the branch diverge from what others have.

git revert — keeps history and appends a new commit that undoes the change. It is the only safe undo on a shared branch, since it adds to history rather than rewriting it, at the cost of one extra "Revert" commit in the log.

Common Mistakes
  • Running git reset --hard to undo a commit on a branch already pushed and shared — collaborators who pulled it now hold commits you discarded, producing diverging-history conflicts.
  • Using git reset --hard while uncommitted working-tree changes you meant to keep are present — those changes were never committed, so the reflog cannot bring them back.
  • Reaching for git revert on a local-only mistake where reset would be cleaner, leaving a pointless "Revert" commit in history.
  • Assuming a bad reset is unrecoverable and re-doing the lost work, instead of checking git reflog where the commit still sits.
  • Running git revert on a merge commit without -m to pick the mainline parent, which fails or reverts the wrong side.
Best Practices
  • Use git revert <commit> for anything already pushed to a shared branch, so no one else's history breaks.
  • Reserve git reset --hard for local, unpushed commits, and run git status first to check for uncommitted work.
  • Use git restore --staged <file> to unstage and git restore <file> to discard, instead of the older overloaded git checkout.
  • Check git reflog before assuming any commit is gone; HEAD movements are kept about 90 days by default.
  • When reverting a merge, pass -m 1 to name the mainline parent explicitly.
Comparable toolsMercurial hg revert (restore files), hg backout (Git's revert), hg strip (history removal)Subversion svn revert to discard local changes; a reverse-merge to undo a committed revisionPerforce p4 revertFossil fossil revert; history rewriting discouraged by design

Knowledge Check

You need to undo a commit that has already been pushed to a shared branch. Which command is safe?

  • git revert — it appends an inverse commit, leaving everyone's existing history intact
  • git reset --hard — it removes the commit cleanly so that collaborators no longer see it at all
  • git restore — it rolls the file back to its prior state without touching shared history
  • Either of them works equally well here; the choice between them is purely stylistic

What does git reset --hard touch that git reset --soft leaves alone?

  • The index and the working tree — --soft moves only HEAD, while --hard overwrites all three trees
  • Only the commit message text recorded on the current HEAD commit, and nothing else in the working tree
  • The tracking remote branch on the server in addition to the local branch pointer
  • Nothing different at all between them; the three reset flags are simply aliases

You ran git reset --hard with uncommitted edits in the working tree that you wanted. Can git reflog recover them?

  • No — the reflog tracks committed HEAD positions, and those edits were never committed, so there is no ref to recover
  • Yes — the reflog quietly takes a full snapshot of the entire working tree on disk for every single reset command you run
  • Yes, but only if you act within the first 30 minutes immediately following the reset command
  • Only if the files happened to be previously staged with git add before the reset ran

When is accepting a "Revert" commit in history the right call rather than a smell?

  • When the commit being undone is already shared, since a clean rewrite would break collaborators' history
  • Always — a Revert commit is the single preferred undo for any mistake at all, whether local or shared
  • Only for merge commits with multiple parents, and never for ordinary single-parent ones
  • Never — a Revert commit appearing always indicates the wrong tool was reached for

You got correct