Guide14 min read

How to Undo Almost Anything in Git

Git has an answer for nearly every mistake. The hard part is knowing which command to reach for while your heart rate is elevated. Here is the triage, organised by what went wrong.

How to Undo Almost Anything in Git | Gitoryx

The moment you realise you have done something wrong in Git is a bad moment to start reading documentation. reset has three flags that do different things, revert sounds like it should undo a commit but works on a different level, restore and checkout overlap, and half the advice on the internet suggests a force push that will make things considerably worse.

So this is a triage list rather than a reference. Find the sentence that matches what happened and the command is underneath it.

The framing that helps: Git very rarely destroys anything. Commits stay in the object database long after they stop being reachable, and the reflog records where your branch pointers have been. The exception, and it is the one that actually costs people work, is changes that were never committed. Everything below is recoverable. Uncommitted work is not.

#Start here

Two questions decide almost every case, and asking them in this order saves you from most of the bad outcomes.

Is the work committed? If yes, it is recoverable no matter what you do next, so you can afford to experiment. If no, stop and run git stash before trying anything, because the commands that fix committed mistakes are frequently the same commands that destroy uncommitted ones.

Has it been pushed to a branch other people use? If no, you can rewrite history freely: reset, amend and rebase are all available. If yes, rewriting is off the table and git revert is the answer. Everything in this post follows from that fork.

SituationNot pushedPushed to a shared branch
Bad commit messagecommit --amendLeave it, or revert and recommit
Unwanted last commitreset HEAD~1revert HEAD
Unwanted old commitrebase -i with droprevert <sha>
Wrong branchreset + cherry-pickrevert there, commit here
Several bad commitsreset HEAD~Nrevert <old>..<new>

#A realistic walk through all four

Say you are on feature/checkout and things go sideways over the course of an afternoon. Here is how reset, revert, checkout and reflog each earn their place in the same session, rather than in isolation:

  1. You commit a pricing fix, then notice the commit message is wrong and nothing is pushed yet. That is git commit --amend, not one of the four below, but it sets the scene: still private, still cheap to change.
  2. Two commits later you realise the second-to-last one duplicated logic that already exists elsewhere. It is still unpushed, so you git reset HEAD~2 to uncommit both, drop the duplicate, and recommit the rest. Reset is for rewriting private history.
  3. You push, a teammate pulls, and only then do you spot that one of those commits reintroduced a bug. Reset is off the table now, the commit is public, so you git revert <sha> instead. Revert is for undoing public history without moving it.
  4. While fixing that, you want to see what src/pricing.ts looked like three commits back, just to compare, without touching your commit history at all: git checkout HEAD~3 -- src/pricing.ts. Checkout (or restore --source) is for pulling an old version of a file into the present, nothing more.
  5. Then you fat-finger a git reset --hard on the wrong branch and watch two hours of work vanish from git log. It is not gone, it is just unreferenced: git reflog shows the commit, and git reset --hard HEAD@{1} gets you back. Reflog is for finding commits that a branch pointer no longer points at.

Four different tools, four different questions: rewrite my own history (reset), cancel public history (revert), borrow an old file version (checkout/restore), or find something the branch pointer left behind (reflog). The official git-reset, git-revert, and git-reflog references cover every flag if you want the exhaustive version.

#I staged a file I did not mean to stage

Nothing is committed. You just want it out of the index.

# Modern form.
git restore --staged src/secrets.ts

# Older form, still works everywhere.
git reset HEAD src/secrets.ts

The file goes back to being modified-but-unstaged. Its contents are untouched. To unstage everything, drop the path.

If you want to discard the changes entirely rather than just unstage them, that is a different and destructive operation:

git restore src/secrets.ts     # discards working tree changes, unrecoverable

Read that one twice before running it. There is no reflog for uncommitted content.

#I want to throw away all my local changes

Nothing committed, you just want the working tree to match HEAD again.

git restore .                 # tracked files back to HEAD
git clean -fd                 # delete untracked files and directories

git clean deletes files Git has never seen, which includes anything in .gitignore only if you add -x. Preview it first, always:

git clean -nd                 # dry run: list what would be deleted

The number of times a git clean -fdx has removed someone's .env file is not small.

#I committed a file that should never have been committed

A .env, a key, a 400 MB build artefact. Two different problems depending on whether it has been pushed.

Not pushed, and it was the last commit:

git rm --cached .env
echo ".env" >> .gitignore
git commit --amend --no-edit

Already pushed: the file is in the remote's history and removing it from the tip changes nothing. Anyone can still check out the old commit and read it. Rewriting the history to remove it entirely requires git filter-repo and a coordinated force push, and every contributor has to reclone.

#I wrote a bad commit message

Only the most recent commit, and only if you have not pushed:

git commit --amend

Your editor opens with the existing message. Change it, save, done. To skip the editor:

git commit --amend -m "🐛 reject expired tokens on refresh"

Amending creates a new commit with a new hash. That is fine locally and a problem if the old one is already on a remote that other people track. For messages further back in the history, reword during an interactive rebase is the tool.

#I forgot to include a file in the last commit

Same command, different use.

git add src/config.ts
git commit --amend --no-edit

--no-edit keeps the existing message. The file joins the previous commit as if it had been there all along.

#I committed to the wrong branch

The commit belongs on feature/billing and it is sitting on main. Nothing is pushed.

# 1. Move the commit onto the right branch.
git checkout feature/billing
git cherry-pick main

# 2. Remove it from the wrong one.
git checkout main
git reset --hard HEAD~1

If it was several commits, cherry-pick a range: git cherry-pick main~3..main.

There is a shorter version when the branch you want does not exist yet, because a new branch simply inherits the commits:

git branch feature/billing     # new branch keeps the commits
git reset --hard HEAD~1        # main goes back

The cherry-pick tutorial covers what to do when the commit does not apply cleanly on the target branch.

#I made a few commits I want to undo, and nothing is pushed

This is git reset, and the flag is the entire decision.

FlagCommitsStaged changesWorking tree
--softRemovedKept, stagedKept
--mixed (default)RemovedUnstagedKept
--hardRemovedDiscardedDiscarded
git reset --soft HEAD~3    # squash three commits into one: recommit now
git reset HEAD~3           # keep the work, re-split it however you like
git reset --hard HEAD~3    # throw the work away entirely

Soft is the one to reach for when you want to redo the commits differently. Mixed is the one for when you want to reorganise which changes go where. Hard is the one to be careful with, and the only one that can cost you anything.

#I already pushed the bad commit

Stop reaching for reset. On a branch anyone else has pulled, rewriting history means their clone now points at commits that no longer exist on the remote, and the next thing that happens is a merge that duplicates everything.

Use revert instead:

git revert 8f3a91c

Git creates a new commit that applies the inverse of 8f3a91c. History is longer, not shorter, and everyone who pulls just gets one more commit. To undo several, pass a range:

git revert --no-commit 8f3a91c..4a91f3d
git commit -m "⏪️ revert the token normalisation series"

Reverting a merge commit needs the -m flag to say which parent to treat as the mainline, which is the one detail people trip over:

git revert -m 1 <merge-sha>

-m 1 means "keep the branch we merged into". The git revert tutorial goes through the consequences, including why re-merging that branch later needs care.

#I need to undo a revert

It happens: you reverted something, then decided the revert was the mistake. A revert is an ordinary commit, so you revert it.

git revert <sha-of-the-revert>

Git will happily do this and the history will read ⏪️ revert X followed by ⏪️ revert "revert X", which is ugly but honest. The alternative, cherry-picking the original commit back, produces a cleaner log and loses the record that anything was ever reverted. On a shared branch the honest version is usually better, because the next person to look at this will want to know it happened.

#I merged something I did not mean to merge

If the merge is not pushed, reset past it:

git reset --hard ORIG_HEAD

ORIG_HEAD is written by every merge, so this works immediately after one. If you have run something else since, find the pre-merge commit in git reflog instead.

If the merge is pushed, revert it with the mainline flag:

git revert -m 1 <merge-sha>

The consequence to know about: once you revert a merge, Git considers that branch merged already. Merging it again brings in nothing, because the merge base has not moved. To genuinely re-merge it later you have to revert the revert first.

#I pushed to the wrong remote or the wrong branch

Pushed to main instead of your feature branch, and you have the access to fix it.

# Put the commits where they belong.
git checkout -b feature/billing origin/main
git push -u origin feature/billing

# Move main back to where it was.
git push origin <sha-before-your-commits>:main --force-with-lease

The second line only works if the branch is not protected, and on any repository with more than two people it will be, correctly. In that case the answer is git revert on main and a normal pull request for the real change.

Pushed to the wrong remote entirely, which usually means a fork you did not intend:

git push otherremote --delete feature/billing

#I lost a stash

Stashes are commits too, and git stash drop or a pop that hit a conflict can leave you thinking one is gone. It usually is not.

# Stash entries are reflog entries on refs/stash.
git reflog stash

# If the stash ref itself is gone, look for dangling commits.
git fsck --no-reflog | awk '/dangling commit/ {print $3}'

Inspect the candidates with git show <sha>, and when you find the right one:

git stash apply <sha>

This is the one recovery on this page that people give up on before trying, because a dropped stash feels final. It is not, for the same 30 to 90 day window as everything else.

#I ran git reset --hard and lost commits

The commits are still there. Git moved a pointer.

git reflog

Find the entry from just before the reset, verify it, and go back:

git show HEAD@{1}
git reset --hard HEAD@{1}

If you would rather not move your current branch, create a new one at that point and look around first:

git branch recovered HEAD@{1}

This works for rebases too. Git writes the pre-operation state to ORIG_HEAD, so immediately after a rebase you regret, git reset --hard ORIG_HEAD is the one-liner. The full set of recovery patterns is in the reflog guide.

#I deleted a branch that was not merged

git reflog | grep <branch-name>

Look for the line recording when you last left that branch. The hash on that line is the branch tip. Recreate it:

git branch feature/invoices 4a91f3d

If nothing turns up, because the branch was never checked out in this clone, git fsck --lost-found --no-reflogs will list unreferenced commits. Noisier, but it finds them.

#I need to undo one specific commit from the middle of the history

Not the last one. One from six commits ago, on a branch that is already shared.

git revert 8f3a91c

Revert works on any commit, not just the tip. If the changes since then touched the same lines, you will get a conflict, which you resolve the same way as any other and then git revert --continue.

The alternative, an interactive rebase with drop, rewrites everything from that commit forward. Correct on a private branch, wrong on a shared one.

#I want the file back as it was two commits ago

Not the whole tree, one file.

git restore --source=HEAD~2 src/pricing.ts

# Older equivalent.
git checkout HEAD~2 -- src/pricing.ts

See the git-checkout reference for the full set of forms this command takes, including switching branches, which is the part restore and switch now cover separately.

The file appears in your working tree in its old state, staged and ready to commit as a normal change. Nothing about the history moves.

#I amended a commit that was already pushed

You ran git commit --amend out of habit, and the original is on the remote. Your local branch and the remote have now diverged by one commit each, and git status says you are "1 ahead, 1 behind".

If nobody has pulled, force-push and it never happened:

git push --force-with-lease

If someone has pulled, do not. Get your original commit back and add the fix as a new one instead:

git reset --hard origin/feature/billing   # back to the pushed state

Your amended version is not lost, it is in the reflog, so if the amendment contained real work rather than a message fix, recover it first with git stash or by noting the SHA from git reflog before resetting.

#I have no idea what I did

Two commands, in this order.

git status      # what is the state right now
git reflog      # what happened to get here

git status will usually tell you outright that you are mid-rebase, mid-merge, or mid-cherry-pick, and it prints the command to abort. If you are in the middle of something:

git rebase --abort
git merge --abort
git cherry-pick --abort
git revert --abort

All four restore the state from before the operation started. None of them leave partial work behind. Aborting is genuinely safe, and knowing that removes most of the anxiety around trying an operation in the first place.

If git status says nothing unusual, the question becomes where your branch is relative to the remote:

git status -sb                        # ahead/behind in one line
git log --oneline --graph -20 --all   # the last 20 commits, all branches
git diff origin/main...HEAD --stat    # what your branch actually changes

That third command is the one that settles most confusion. The three-dot form compares against the merge base, so it shows your changes without including everything that landed on main since you branched. If the output is not what you expected, the problem is usually that you branched from the wrong place rather than anything you did afterwards.

#The five things Git genuinely cannot undo

Worth knowing, because they are the only cases where speed matters.

Uncommitted changes destroyed by reset --hard, restore or checkout. Never committed means never stored. Nothing to recover.

Untracked files deleted by git clean. Git never knew about them. Your editor's local history or your filesystem snapshots are the only hope.

Anything pruned by garbage collection. Unreachable objects survive 30 days by default and 90 if the reflog still references them. git gc --prune=now removes them immediately, which is why you should not run it while you are still working out whether something is lost.

A secret that has been pushed. You can rewrite the history, but you cannot un-see it. Rotate the credential.

Someone else's history. The reflog is local. If a colleague force-pushed over your commits and you never fetched them, your repository has no record and theirs does.

Everything else on this page is recoverable, usually in under a minute.

#Undoing with fewer commands

Every command above is one you can type. The problem is not that they are hard, it is that choosing between them requires you to know what state you are in, and the state is exactly what is unclear in the moment you need to undo something.

Gitoryx tracks the last operation that moved HEAD and offers a single undo for it. Before acting, it checks whether the commits have been pushed and whether your working tree is dirty, which are the two conditions that turn a routine undo into a second problem. For anything more complicated, the reflog viewer shows every past position with its commit, its diff and its place in the graph, so you pick a state you can see rather than an index into a text list.

#The two habits that make all of this unnecessary

Commit more often than feels warranted. A commit is the boundary of what Git can recover. Work that is committed is safe in a way that work in your editor is not, and a messy branch can be cleaned up in ten minutes with interactive rebase before anyone sees it.

And stash before anything destructive. git stash takes half a second and converts "my uncommitted changes are gone" into "my uncommitted changes are in the stash list". That single habit covers the only category of loss on this page that has no recovery path.

Frequently Asked Questions

What is the difference between git revert and git reset?

`git reset` moves the branch pointer backwards, so the commits stop being part of the branch. `git revert` leaves history alone and adds a new commit that applies the inverse changes. Reset rewrites, revert appends. On a branch other people have pulled, revert is the only safe option.

How do I undo the last commit but keep my changes?

`git reset --soft HEAD~1` removes the commit and leaves everything staged, ready to recommit. `git reset HEAD~1` (mixed, the default) removes the commit and leaves the changes in your working tree, unstaged. Use soft when you only want to fix the message or add one more file, mixed when you want to re-split the work.

Can I undo a commit I already pushed?

Yes, with `git revert <sha>`, which adds an inverse commit and is safe on shared branches. Rewriting the pushed history with reset and a force push is possible but breaks the history for anyone who already pulled, so keep it to branches only you are working on.

I ran git reset --hard and lost work. Can I get it back?

If it was committed, almost certainly. `git reflog` lists everywhere HEAD has been, and `git reset --hard HEAD@{1}` returns you to the state just before the reset. If the work was never committed, the reflog cannot help, because it only records commits.

All Blog Posts