TechEarl

How to Undo (Almost) Anything in Git

A by-situation map for undoing things in git: discard a change, unstage a file, undo a commit, undo a pushed commit safely, delete untracked files, and recover with reflog. Modern restore and switch, not just checkout and reset.

Ishan Karunaratne⏱️ 10 min readUpdated
Share thisCopied
Undo anything in git by situation: restore unstaged changes, unstage a file, undo a commit, revert a pushed commit, clean untracked files, and recover with reflog.

Almost every "undo" in git comes down to one question: what state is the change in right now? Once you know that, the command is obvious. The hard part is that git has historically overloaded checkout and reset to mean half a dozen unrelated things, so the same word undoes a file edit, switches a branch, and throws away a commit. Since git 2.23 (2019) there is a cleaner split: git restore for files, git switch for branches. The rest still works, but the modern commands say what they do.

Here is the whole map in one table. Pick your row, run the command, read the section below for the nuance.

SituationCommand
Discard unstaged edits to a filegit restore path/to/file
Unstage a staged file (keep the edits)git restore --staged path/to/file
Reword the last commit messagegit commit --amend
Undo the last commit, keep the changesgit reset --soft HEAD~1
Undo the last commit, throw the changes awaygit reset --hard HEAD~1
Undo a commit you have already pushedgit revert <sha>
Delete untracked files and foldersgit clean -fd (preview with -nd first)
Recover something you think you lostgit reflog

Discard unstaged changes to a file

You edited a file, did not stage it, and want the file back to its last committed state:

bash
git restore path/to/file

This is the modern replacement for git checkout -- file. The old form still works, but checkout is so overloaded that it was easy to fat-finger a branch name and lose your edits. git restore only ever touches files, so there is no ambiguity. To discard every unstaged change in the working tree at once:

bash
git restore .

There is no undo for this. The edits were never committed, so once they are gone, they are gone. If you are not sure you want to lose the work, stash it instead with git stash, which tucks the changes away where you can get them back later.

Unstage a file

You ran git add on something you did not mean to stage. You want it out of the index but you want to keep the edits:

bash
git restore --staged path/to/file

That replaces the old git reset HEAD file. The file goes back to "modified but not staged"; your actual changes are untouched. To unstage everything:

bash
git restore --staged .

A common point of confusion: if a file was only ever modified (never staged), you do not need this step at all. Just git restore file and move on. The --staged form is only for pulling something back out of the index.

Reword the last commit message

You committed with a typo in the message, or "wip" when you meant something real:

bash
git commit --amend

That opens your editor on the last commit's message. To set it in one line without the editor:

bash
git commit --amend -m "A clearer commit message"

--amend replaces the previous commit entirely, which means it gets a new SHA. That is fine while the commit is local. Do not amend a commit you have already pushed to a shared branch: the new SHA diverges from what everyone else has, and you are back to force-push territory. See git force push safely for what that costs and how to do it without clobbering a teammate.

Undo the last commit

Two versions, and the difference matters a lot. Both move the branch pointer back one commit; what they do with the changes is the whole question.

Keep the changes (they land back in your working tree, staged):

bash
git reset --soft HEAD~1

This is the everyday "I committed too early" or "I committed to the wrong branch" undo. The commit is gone, but every line you wrote is right there, staged and ready to re-commit (or move to another branch). Nothing is at risk.

Throw the changes away entirely:

bash
git reset --hard HEAD~1

--hard is destructive. It deletes the commit and the changes in it from your working tree. There is no confirmation prompt. Run it only when you are certain you want those edits gone. If you are not certain, use --soft (or --mixed, the default, which keeps the changes unstaged) and you keep your safety net.

HEAD~1 means "one commit before HEAD." HEAD~2 undoes the last two, and so on. The --soft / --hard distinction applies the same way at any depth.

Undo a commit you have already pushed

This is the one that catches people. git reset rewrites history, which is fine on a commit that only exists on your machine and a disaster on a commit other people have already pulled. The safe tool for a pushed commit is revert:

bash
git revert <sha>

git revert does not erase anything. It creates a new commit that is the exact inverse of the one you name, undoing its changes while leaving the original in the history. Because it only adds a commit, you push it normally, no force-push, no rewritten history, nothing breaks for anyone who already pulled. That audit trail ("this change was reversed, here is when and why") is a feature, not noise, on a shared branch.

To find the SHA, run git log --oneline and copy the short hash. To revert the most recent commit specifically:

bash
git revert HEAD

Reverting a merge commit needs the -m parent flag, which is its own small topic; the git revert docs cover the mainline-parent rule. The rule of thumb is simple: local commit, use reset; pushed commit, use revert.

Remove untracked files

git restore and git reset only touch files git already knows about. Brand-new files that have never been staged are untracked, and the tool for those is git clean. It deletes files off your disk, so always preview first:

bash
git clean -nd

The -n is a dry run: it lists exactly what would be deleted and changes nothing. Read that list. When it is what you expect, run it for real:

bash
git clean -fd

-f is "force" (git refuses to delete without it, by design), and -d includes untracked directories, not just files. This is the command you want for "get rid of all the junk I created but never committed."

A flag worth getting right, because it is a common and damaging mix-up: -fX (uppercase X) does not mean "only untracked files." Uppercase -X removes only the files git is ignoring (the build artifacts and caches in your .gitignore), and leaves your real untracked files in place. Lowercase -x is more aggressive still: it removes untracked and ignored files together. For the ordinary "clean up untracked stuff" case, you want plain git clean -fd, not -fX. When in doubt, git clean -nd shows you the truth before anything is deleted.

You want to deleteCommand
Untracked files and foldersgit clean -fd
Only ignored files (build output, caches)git clean -fdX
Untracked and ignored, everythinggit clean -fdx

When you think you lost something: reflog

Here is the reassuring part. Almost nothing in git is truly gone, even after a reset --hard or a bad rebase. Git keeps a private log of everywhere HEAD has pointed, and that log is reflog:

bash
git reflog

You get a list like abc1234 HEAD@{0}: reset: moving to HEAD~1, one line per move, most recent first. Find the SHA from before the move you regret, and you can get back to it:

bash
git reset --hard abc1234

Or, if you only want the lost commit back as a branch without disturbing where you are now:

bash
git switch -c recovered-work abc1234

reflog is local and only goes back so far (entries expire, ninety days by default for reachable commits), but in the moment right after a mistake it is almost always there. Before you panic about a --hard or an amend that ate the wrong thing, check git reflog. The thing you lost is usually one SHA away.

A note on the old commands

If you learned git years ago, the muscle memory is git checkout -- file and git reset HEAD file. Those still work and are not deprecated; nothing about your existing scripts breaks. But git restore (files) and git switch (branches) were added precisely because checkout did too many jobs, and the new pair is what I reach for now and what I would teach someone starting today. If you are still getting comfortable with the basics, git for beginners walks through the staging model that makes all of this make sense.

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

Tagsgitgit undogit restoregit resetgit revertgit refloggit cleanversion control

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to List the Files Changed in Git

List the files changed in git: working tree, staged, the last commit, between branches, or since N commits. Then pipe the list straight into a linter so you only check what changed.

How to Undo the Last Git Commit

Undo your last Git commit without losing work. When to use amend, reset --soft, reset --mixed, reset --hard, and revert, plus the rule for commits you already pushed.

How to Discard Local Changes in Git

Discard local changes in Git with restore, checkout, and clean. How to throw away edits to tracked files, delete untracked files, and reset to the last commit.