Git & GitHub · lesson 3 of 12
Undoing Changes: restore, reset, and revert
See the real difference between git restore, the three reset modes, and git revert — including which ones rewrite history and which ones don't.
By SkillByExample · Published
Git has three different tools for “undo,” and they operate at three different levels: files, the index, and history itself. Using the wrong one either doesn’t go far enough or goes further than you wanted.
1) git restore reverts a file to its last committed content
$ # ...edit app.py, change "Hello" to "Hi"...
$ git diff --stat
app.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git restore app.py
$ git diff --stat
No output the second time — git restore (no --staged) throws away the unstaged
edit and puts the working copy back to what’s in the index (here, the last commit).
2) reset —soft undoes the commit, keeps the change staged
$ # ...re-edit app.py, commit it as "Change greeting to Hi (oops)"...
$ git reset --soft HEAD~1
$ git status -s
M app.py
The commit is gone from history — HEAD moved back one step — but the change it
introduced is still sitting in the index, staged and ready to be committed differently
or dropped.
3) The default (—mixed) also unstages it
$ git reset HEAD
$ git status -s
Unstaged changes after reset:
M app.py
M app.py
Same commit, but now the change is only in the working directory — unstaged. This is
what plain git reset (no flag) does by default: undo the commit, undo the staging,
keep the file edit.
4) reset —hard discards the change entirely
$ git reset --hard HEAD
$ git status -s
HEAD is now at bd316aa Merge feature/add-tests into main
Empty status output — the working directory now matches HEAD exactly. This is the
only one of the three reset modes that can lose uncommitted work, because it overwrites
the working directory too.
5) Once a commit is shared, revert instead of reset
$ # ...commit "Change greeting to Hi (oops)" again, simulating it being pushed...
$ git revert --no-edit HEAD
$ git log --oneline -3
d7da0e1 Revert "Change greeting to Hi (oops)"
ccd5e66 Change greeting to Hi (oops)
bd316aa Merge feature/add-tests into main
Both the mistake and its fix stay in history. Anyone who already pulled ccd5e66 will
cleanly pull d7da0e1 too — nothing was rewritten out from under them, which is why
revert (not reset) is the safe choice for shared branches.
Practice
-
Run
git restore --staged <file>after staging a change (without reverting it). Confirm the change stays in your working directory but drops out of the index — the opposite half of what--softreset does. -
Reproduce step 4 above but first stage the edit (
git add) before runninggit reset --hard HEAD. Confirm staged-but-uncommitted changes are lost too, not just working-directory ones.Solution
$ echo "x = 1" >> app.py && git add app.py $ git reset --hard HEAD $ git status -sEmpty output —
--hardresets both the index and the working directory to matchHEAD, regardless of what was staged. -
Run
git reverton a commit that isn’t the most recent one (git revert <hash>for an older commit) and observe that Git still applies the inverse diff correctly, creating a new commit on top rather than modifying history in place. -
Compare
git reset --hard <hash>againstgit revert <hash>on the same target commit in two separate scratch clones. Confirm one rewritesgit logand the other appends to it. -
Run
git reflogright after areset --hard(before doing anything else) and find the commit you just “lost” still listed — it’s not deleted yet, just unreachable from any branch. (Recovering it is covered in a later lesson.)