Undoing changes: revert, reset, stash and reflog
Pick the right undo every time: discard an edit, unstage a file, take back a local commit with reset, undo a pushed one with revert, set work aside with stash, recover with reflog.
By the end of this lesson you'll be able to
0 of 5 completeWhich undo?
Every undo starts with one question: where is the mistake now? The answer picks the command:
| The mistake is… | Undo with | Safe after pushing? |
|---|---|---|
| an edit you haven't staged | git restore file | n/a (never left your machine) |
| staged, not committed | git restore --staged file | n/a |
| the last commit, message or missing file | git commit --amend (Rewriting history) | no |
| in local commits, not pushed | git reset | no |
| in a pushed commit | git revert | yes |
| "I just lost something" | git reflog | it's local |
The dividing line is pushed. Before pushing, you may rewrite your own history freely. After pushing, other people have those commits, so you add new commits instead of removing old ones.
Revert: undo a commit everyone has
git revert makes a new commit that does the opposite of an old one. History only grows, so
nobody's clone is disturbed:
$ git log --oneline 26ec4e7 Lower the timeout to 1 second 5a5f6da Add config 5b4574d Add app $ git revert --no-edit HEAD [main f390571] Revert "Lower the timeout to 1 second" 1 file changed, 1 insertion(+), 1 deletion(-) $ git log --oneline -2 f390571 Revert "Lower the timeout to 1 second" 26ec4e7 Lower the timeout to 1 second $ cat config.py TIMEOUT = 30 $ git push
git log --onelinegitversion controlloghistory
cat config.pycatprint a file
git pushpushupload commits
Revert any commit by ID, not only the last: git revert 26ec4e7. To revert a merge commit, say which
parent is the "main line": git revert -m 1 <merge-commit>. On GitHub, every merged pull request has a
Revert button that opens a pull request doing exactly this.
Reset: move your branch back
git reset moves your current branch to another commit, as if the later commits had never happened.
Its three modes differ only in what they do with the changes from those commits:
$ git reset --soft HEAD~1 # undo the commit, keep its changes staged $ git status --short M app.py $ git reset HEAD~1 # --mixed (default): undo the commit, keep changes unstaged $ git status --short M app.py $ git reset --hard HEAD~1 # undo the commit and throw its changes away HEAD is now at d62b38c Commit A
git reset --soft HEAD~1gitversion control
git status --shortstatuswhat changed
| Mode | Use it to |
|---|---|
--soft | combine your last few commits into one: git reset --soft HEAD~3, then one git commit |
--mixed | redo the last commit differently: split it, or leave a file out |
--hard | throw away local commits and edits entirely: start again from a known point |
HEAD~1 means one commit before HEAD; HEAD~3 is three back. git reset --hard origin/main is the
classic "make my branch exactly match GitHub's", and it deletes anything you had that GitHub doesn't.
Stash: put work aside
You're halfway through something and need a clean working folder: an urgent fix, a pull, a quick look
at another branch. git stash saves your changes and tidies the folder:
$ git stash # tracked changes only Saved working directory and index state WIP on main: 0bb860e B3 $ git status --short ?? new.py $ git stash -u -m "half-done search" # -u: include untracked (new) files, -m: a label $ git stash list stash@{0}: On main: half-done search stash@{1}: WIP on main: 0bb860e B3 $ git stash show -p stash@{1} # what's in it $ git stash pop # apply the newest and remove it from the list $ git stash apply stash@{1} # apply one, but keep it in the list $ git stash drop stash@{1} # delete one
git stashgitversion control
git status --shortstatuswhat changed
Without -u, new files stay behind in your folder; that's the most common stash surprise. Stashes are
for minutes or hours, not weeks: anything longer belongs on a branch (git stash branch fix/x turns a
stash into one).
Reflog: your safety net
Git quietly records every place HEAD has been: every commit, checkout, reset, rebase and merge. That
log, the reflog, is how you get back "lost" commits:
$ git reset --hard HEAD~1 # oops: B3 is gone from the branch HEAD is now at d62b38c Commit A $ git reflog -3 d62b38c HEAD@{0}: reset: moving to HEAD~1 0bb860e HEAD@{1}: commit: B3 d62b38c HEAD@{2}: reset: moving to HEAD~1 $ git reset --hard HEAD@{1} # go back to where HEAD was one step ago HEAD is now at 0bb860e B3
git reset --hard HEAD~1gitversion control
It works for a deleted branch too: git branch -D spike printed (was 1bae2b4), so
git branch spike 1bae2b4 brings it back. If you didn't see the ID, look for the last commit on that
branch in git reflog.
The reflog is local (it's not pushed or cloned) and entries expire, by default after 90 days. A commit that was committed is almost never truly lost within that time; it's just unlabelled.
A bad deploy and a bad reset
A commit lowered a timeout and broke production after it was pushed. Undo it the safe way. Then, while tidying up locally, you hard-reset away a good commit by mistake. Get it back. End with the timeout restored and no work lost.
TIMEOUT = 30 on main, and the 'Add retries' commit recoveredReveal one safe solution
$ git init --bare remote.git && git clone remote.git shop && cd shop $ echo "TIMEOUT = 30" > config.py && git add . && git commit -m "Add config" && git push -u origin main $ echo "TIMEOUT = 1" > config.py && git commit -am "Lower the timeout to 1 second" && git push $ git revert --no-edit HEAD && git push # 1. production fix: a new commit $ cat config.py TIMEOUT = 30 $ echo "RETRIES = 3" >> config.py && git commit -am "Add retries" $ git reset --hard HEAD~1 # 2. the mistake $ grep -c RETRIES config.py 0 $ git reflog -2 5c40083 HEAD@{0}: reset: moving to HEAD~1 be5be3f HEAD@{1}: commit: Add retries $ git reset --hard HEAD@{1} # 3. the recovery $ grep RETRIES config.py && git push RETRIES = 3 $ git log --oneline
git init --bare remote.git && git clone remote.git shop && cd shopgitversion controlinitcreate a repository here&&run the next command only if this one workedclonecopy a repositorycdchange directory
echo "TIMEOUT = 30" > config.py && git add . && git commit -m "Add config" && git push -u origin mainechoprint text>write output to a file, replacing itaddstage changes.this foldercommitsave staged changespushupload commits
cat config.pycatprint a file
echo "RETRIES = 3" >> config.py && git commit -am "Add retries">>append output to a file
grep -c RETRIES config.pygrepfind lines that match a pattern-ccount matching lines
git log --onelineloghistory
The final log reads: Add retries, Revert "Lower the timeout…", Lower the timeout…, Add config. The bad commit is still in history, deliberately: it records what happened, and the revert records the fix.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
A commit that broke production is already on main and pushed. How do you undo it?
Official reading for this lesson
Or get every quiz answer right and it completes itself.