Undoing Things Safely
A decision guide for every common mess: restore, clean, amend, reset soft/mixed/hard, revert, and reflog rescue — with a clear line between what is recoverable and what is not.
A decision guide for every common mess: restore, clean, amend, reset soft/mixed/hard, revert, and reflog rescue — with a clear line between what is recoverable and what is not.
Sooner or later — possibly this afternoon — you will stage a file you did not mean to stage, write a commit message with a typo in it, or make a commit that should never have existed. That is the moment most people quietly panic and start copying files to the desktop "just in case".
You do not need to. Git has a precise tool for each of these messes. By the end you will know which one to reach for in each situation, and — just as importantly — which of them destroy work permanently and which of them cannot.
Git's undo commands look like a jumble of unrelated words:
restore, reset, revert, clean, amend. They get much
easier to hold once you see that they differ along only two
axes, and that the two axes are two different kinds of
trouble.
restore <file>, clean -fd. They overwrite work Git never
recorded, and Git cannot give back what it was never given.
reset --hard. Both kinds of trouble at once — which is why
it is the command with the reputation.
restore --staged, revert. Nothing is lost and nobody
else's copy is disturbed.
reset --soft, reset, amend. Free at your own desk;
genuinely disruptive once somebody else has those commits.
You ran git add . and swept up a scratch file along with the
real change. Nothing is committed yet, so this is harmless to
fix:
git restore --staged notes-to-self.txtThe file drops out of the staging area and back into the plain "modified" pile. Your edits to it are untouched — you have only changed Git's answer to "what goes in the next commit?" Neither axis is in play: no working tree changes, no history rewritten.
You will also see git reset notes-to-self.txt recommended, and
it does the same job. It is the older spelling, from before Git
had a verb dedicated to files. Prefer git restore: it says what
it does, and it will not be confused with the commit-moving
reset you meet later in this lesson.
You spent twenty minutes going down a wrong path in one file and want it back exactly as it was at the last commit:
git restore src/pricing.jsThe file is rewritten from the last commit, and your twenty minutes are gone. Not in a bin, not in a hidden folder — gone. Those edits only ever existed in the file, and you just overwrote the file.
This is the first command here that can genuinely cost you something, and the rule behind it is worth memorising: Git protects what you have committed, and nothing else.
Sometimes the experiment is a write-off and you want the whole folder back to the last commit. That takes two commands, because Git treats tracked and untracked files differently. First, reset every tracked file:
git restore .That leaves behind the files Git has never seen — the stray
test-copy.html, the debug script, the folder you made at 2am.
git clean removes those, and you should always ask it what it
plans to do first:
git clean -nd # -n = dry run, -d = include directoriesRead the list. Every path it prints will be deleted, with no way
back. When you are happy with it, swap the -n for -f:
git clean -fd # -f = force, actually delete themA third flag, -x, also deletes files matched by your
.gitignore. That sounds tidy and is quietly the most
destructive option here: .gitignore is exactly where local
.env files, credentials, and databases live. Reserve -x for
when you truly want a machine-fresh checkout, and dry-run it even
then.
Your commit is made, the message says "Fix pricingg table", and you have not shared it with anyone. Rewrite it:
git commit --amend -m "Fix pricing table rounding"If you instead forgot to include a file, stage it and amend without touching the message:
git add src/pricing.test.js
git commit --amend --no-editThe important part: --amend does not edit the old commit.
Commits are immutable, so Git builds a new commit with the
corrected content and a new hash, then points your branch at it
and orphans the old one.
That is invisible while the commit lives only on your machine. If you have already pushed it, your history and the shared history now disagree, and the only way to push is to force — which overwrites work for anyone who pulled it.
You committed too early, or bundled two ideas into one commit. You want the commit gone but every line of code kept:
git reset --soft HEAD~1HEAD~1 means "one commit before where I am now". The commit
disappears from your branch and its changes reappear in the
staging area, ready to be re-committed however you like.
To keep the changes but unstage them — back to plain edited files
— leave the flag off. --mixed is the default:
git reset HEAD~1The three flags differ only in how far they reach through the three areas from lesson 3:
--soft — moves the branch. Staging area and working tree are
left exactly as they are.--mixed (default) — moves the branch and clears staging. Your
files keep their content.--hard — moves the branch, clears staging, and overwrites
your files to match the target commit.git reset --hard HEAD~1 # discards the commit AND your editsgit reset --hard is the one command here that destroys both
committed and uncommitted work in a single stroke. The commit it
drops is recoverable through the reflog. Any uncommitted edits
sitting in your working tree when you run it are not.
Now reset is off the table. Other people have that commit; you cannot un-give it to them. What you can do is publish a correction:
git revert 9f8e7d6git revert reads the commit you name, works out the exact
opposite of it, and makes that opposite a new commit on top
of your branch. History grows instead of changing, so everyone
else pulls one more commit and their copy stays consistent with
yours.
Think of an accounting ledger. You do not scratch out an entry
that has already been audited — you post a correcting entry
underneath, and the record shows both. reset is the eraser;
revert is the correcting entry.
The choice between them is not about which is nicer. It is about who has seen the commit:
git reset.git revert, every time.Reverting leaves the mistake visible in the log, and that is a feature. "Revert 'Enable new checkout flow'" tells a month-later reader more than a gap where a commit used to be.
You ran git reset --hard HEAD~1, and thirty seconds later
realised the commit mattered. Git keeps a private log of every
position HEAD has occupied — the reflog:
git reflogYou get something like this, newest first:
a1b2c3d HEAD@{0}: reset: moving to HEAD~1
9f8e7d6 HEAD@{1}: commit: Add pricing table
3e4f5a6 HEAD@{2}: commit: Update READMELine HEAD@{1} is where you stood before the reset, and
9f8e7d6 is the commit you thought you had destroyed. Put your
branch back on it:
git reset --hard 9f8e7d6Your commit is back. The reflog is local, never pushed, and entries expire after about ninety days — plenty of time to rescue an afternoon. The full tour lives in the intermediate course; this much is enough to get you out of trouble today.
Occasionally you do not want to undo a commit at all. You only want one file as it was three commits ago, while everything else stays current:
git restore --source=9f8e7d6 -- src/pricing.jsThe --source flag says where to read from instead of the last
commit, and -- separates the commit from the path so Git never
confuses a filename with a branch name. A relative reference like
--source=HEAD~3 works just as well.
The file in your working tree is overwritten immediately, so the uncommitted-work warning applies here too. No history is rewritten — you now have an old version of one file sitting as an ordinary edit, ready to commit if you like it.
| Command | What it changes | Rewrites? | Recoverable? |
|---|---|---|---|
restore <file> | Working tree | No | No |
restore --staged | Staging area | No | Yes |
reset --soft | Branch pointer | Yes | Yes (reflog) |
reset (mixed) | Pointer + staging | Yes | Yes (reflog) |
reset --hard | Pointer + both areas | Yes | Commits only |
revert <commit> | Adds a new commit | No | Yes |
clean -fd | Untracked files | No | No |
Read the last column as the real safety rating. Anything marked "No" is a one-way door — and both of those rows are commands that reach into your working tree, which is the top-left corner of the grid at the start of this lesson.
Every command from this lesson, in the order you are likely to need them:
git restore --staged <file> # unstage, keep the edits
git restore <file> # discard edits (NOT recoverable)
git restore . # discard all tracked-file edits
git clean -nd # dry run: list untracked files
git clean -fd # delete them (NOT recoverable)
git clean -fdx # also delete ignored files
git commit --amend -m "msg" # rewrite the last message
git commit --amend --no-edit # add staged files to last commit
git reset --soft HEAD~1 # drop commit, keep changes staged
git reset HEAD~1 # drop commit, keep changes unstaged
git reset --hard HEAD~1 # drop commit and all changes
git revert <commit> # new commit undoing an old one
git reflog # every position HEAD has held
git reset --hard <hash> # jump back to a recovered commit
git restore --source=<c> -- <p> # one file from one commitYou now have an answer for every everyday mess: unstage with
restore --staged, discard with restore or clean, fix the
last commit with --amend, unwind commits with reset, undo
published ones with revert, and rescue yourself with reflog.
Better still, you can tell before you press Enter whether a
command is going to cost you anything.
Next comes branching and merging basics, where undoing gets easier again — a branch keeps a risky experiment somewhere it cannot touch your main line of work, and throwing the whole branch away costs nothing.
Before you move on, practise in a throwaway repository. Make a few commits, then break things on purpose: amend one, soft reset another, hard reset a third and fish it back out of the reflog. Doing that once when nothing is at stake is worth more than reading this page five times.