Run git reset --hard on a commit your team already pulled and you’ve just made your history disagree with theirs. Run git revert on a typo you haven’t pushed and you’ve added a pointless commit to the log. Git has four undo commands, restore, reset, commit --amend and revert, and they are not interchangeable. Pick the wrong one and it either does nothing or does far more than you wanted.

What decides it is where the change lives right now: in your working tree, in the staging area, in a commit only you have, or in a commit you already pushed.

Where a change lives in Git: working tree, index, history

Every undo command operates on one or more of three layers:

  • Working tree: the files on disk, as you’re editing them.
  • Index (the staging area): the snapshot you’ve built with git add, waiting to become the next commit.
  • History: the commits already recorded, reachable from HEAD.

A change starts in the working tree, moves to the index on git add, and moves to history on git commit. Undoing it means walking it back through the same layers. The further it has gone, the more deliberate the command.

1
2
working tree  --add-->  index  --commit-->  history
   restore              restore --staged     reset / revert / amend

How to discard unstaged changes with git restore

You’ve edited config.py, broken it, and want Git’s copy back:

1
git restore config.py

This overwrites the file in your working tree with the version in the index, which is the same as HEAD unless you’ve staged something. The edit is gone. It isn’t stashed, and no Git command will bring it back. To discard every unstaged change at once:

1
git restore .

git restore replaces the older git checkout -- <file>. Both still work, but restore exists so that “give me this file back” no longer shares a command with “switch branches”.

How to unstage a file without losing your edits

You ran git add on three files, then noticed one of them doesn’t belong in this commit. The edit itself is fine:

1
git restore --staged migration.sql

migration.sql leaves the index and goes back to “modified, not staged”. The file on disk is untouched. This is the same as the older git reset HEAD -- migration.sql, and it’s the one place where reset and restore overlap: reset with a pathspec only touches the index, which is all restore --staged does.

How to undo a commit with git reset: soft, mixed, and hard

git restore can’t touch a commit. For that you need git reset, and the flag controls how much it undoes:

FlagWorking treeIndexHistory (HEAD)
--softunchangedunchangedmoved
--mixed (default)unchangedreset to match targetmoved
--hardreset to match targetreset to match targetmoved

All three move the branch pointer. The difference is how much of your work follows it backward.

--soft rewinds the commits but leaves their changes staged. That makes it the quickest way to squash the last three commits into one:

1
2
git reset --soft HEAD~3
git commit -m "Add payment retry logic"

--mixed is what you get with a plain git reset HEAD~1. It rewinds the commit and drops its changes back into the working tree as unstaged edits, so you can split them into smaller commits or throw part of them away.

--hard discards the diff entirely. The working tree ends up as if the commit never happened:

1
git reset --hard HEAD~1

There’s no prompt. The commits you rewound past can still be found through git reflog (see below), but any uncommitted edits in your working tree at that moment are gone for good, so check git status first. And never run it on commits someone else has already pulled: moving the branch backward on a shared branch leaves your history and theirs disagreeing about what happened, the same problem behind the golden rule of rebasing.

How to fix the last commit with git commit –amend

If the mistake is in the most recent commit, a reset is overkill. --amend replaces the tip commit instead of rewinding past it:

1
git commit --amend -m "Fix payment retry logic"

Stage more changes first and they fold into the same commit, on the same parent:

1
2
git add retry.py
git commit --amend --no-edit

The amended commit is a new object with a new hash, just like a rebased one. So the rule is the same as for rebase: amend only what you haven’t pushed. Amend a commit that’s already on a shared branch and everyone who pulled it has to reconcile two commits that claim to be the same change.

How to undo a commit that’s already been pushed with git revert

Once a commit is on a shared branch, reset and --amend are off the table. Rewriting history other people have fetched is how you get diverged branches and duplicated commits. git revert takes the other route: it leaves the bad commit where it is and adds a new one that does the opposite.

1
git revert a1b2c3d

Git opens an editor for the revert commit’s message, then records it:

1
2
3
Revert "Add aggressive cache invalidation"

This reverts commit a1b2c3d4e5f6789a1b2c3d4e5f6789a1b2c3d4.

History now holds both the original commit and its inverse. Nothing was rewritten, so a plain git pull on every other clone picks up the fix. To revert a range as a single commit:

1
2
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert the last three deploy changes"

--no-commit (-n) applies the inverse of each commit to the working tree and index without committing, so three reverts land as one commit instead of three. If a revert conflicts with changes made since, Git stops the way it does mid-merge: resolve the conflict markers, git add the file, then run git revert --continue.

So “undo a push” means: revert the commit, then push the revert. Don’t force-push rewritten history to a branch other people pull from.

How to recover from a bad reset with git reflog

After reset --hard, the commits disappear from git log. They still exist. git reflog lists every position HEAD has held on your machine, including the one right before the reset:

1
git reflog
1
2
f3a9c21 HEAD@{0}: reset: moving to HEAD~1
7d4e1b8 HEAD@{1}: commit: Add payment retry logic

git reset --hard HEAD@{1} puts you back on 7d4e1b8. How long reflog entries live, and why the reflog is local-only, is covered in the recovery section of Git Rebase vs Merge. It works the same whichever command moved HEAD.

The reflog gets you back to where you were. It won’t tell you which older commit was the last good one. For that, git bisect narrows hundreds of commits down to the one that introduced the bug.

Git reset vs revert vs restore: which to use when

The change is…Command
Edited, not staged, and you want it gonegit restore <file>
Staged, and you want it unstaged (edit kept)git restore --staged <file>
Committed, unpushed, and it’s the last commitgit commit --amend
Committed, unpushed, and you want to rewind furthergit reset --soft / --mixed / --hard HEAD~n
Committed and already pushed or sharedgit revert <commit>
Gone after a reset --hard you regretgit reflog + git reset --hard HEAD@{n}

For anything already committed, one question picks the row: has anyone else seen this commit? If not, reset or amend and move on. If they have, revert. It costs one extra commit in the log, and none of your teammates has to untangle a history that no longer matches theirs.