Skip to content

Undoing things in Git safely: restore, reset and revert

By · Git & GitHub · 4 min read · Published

Git can undo almost anything, but it offers several commands that sound alike and behave very differently. Pick the wrong one and you either fail to undo the mistake or create a bigger one, such as rewriting history your teammates already pulled. This guide explains the three areas Git tracks, then matches common mistakes to the safe command for each, and finishes with how to recover when something seems lost.

Three places your work lives

Every undo command makes more sense once you know which of these it changes:

  • Working tree: the files on disk that you edit.
  • Index (staging area): a snapshot of what the next commit will contain. git add copies changes from the working tree into the index.
  • HEAD: a pointer to the current commit, usually through the current branch. git commit turns the index into a new commit and moves the branch forward.

The modern commands map cleanly onto these areas: git restore changes files in the working tree or index, git reset moves the branch pointer, and git revert adds a new commit. Older tutorials use git checkout for several of these jobs; restore and switch were added in Git 2.23 to split that overloaded command.

Undo changes that are not committed yet

Discard edits to a file

git restore src/app.js

This replaces the file in the working tree with the version in the index. The uncommitted edits are gone for good, since Git never stored them, so be sure first.

Unstage a file but keep the edits

git restore --staged src/app.js

This copies the file from HEAD back into the index. Your working tree is untouched.

Put work aside temporarily

git stash push -m "half-done refactor"
git stash pop

Stashing saves your uncommitted changes and cleans the working tree, so you can switch branches. Add -u to include untracked files.

Remove untracked files

git clean -n     # preview what would be deleted
git clean -fd    # delete untracked files and folders

Always run the preview first. Untracked files are not in Git, so they cannot be recovered from it.

Undo commits that are only on your machine

If you have not pushed, you can freely rewrite history, because nobody else has the commits.

Fix the last commit

git commit --amend -m "Better message"
git add forgotten-file.js && git commit --amend --no-edit

Amending replaces the last commit with a new one that includes the current index and, optionally, a new message.

Uncommit but keep the changes

git reset --soft HEAD~1    # changes stay staged
git reset HEAD~1           # changes stay in the working tree, unstaged (--mixed)

HEAD~1 means "one commit before HEAD". Use HEAD~3 to undo three commits. The branch pointer moves back; your files keep the changes so you can recommit them differently.

Throw commits away completely

git reset --hard HEAD~1

This moves the branch back and overwrites the index and working tree. Any uncommitted changes are destroyed. The discarded commits are still recoverable from the reflog for a while, as described below, but uncommitted work is not.

Combine several commits into one

git reset --soft HEAD~3
git commit -m "Add login page"

This is the simplest way to squash the last few commits. Interactive rebase, git rebase -i HEAD~3, gives finer control, such as reordering or editing individual commits.

Undo commits that are already pushed

Once others may have pulled your commits, rewriting them causes trouble: their history no longer matches the remote, and the next pull produces confusing merges or duplicated commits. On shared branches like main, add a new commit that reverses the change instead:

git revert a1b2c3d

git revert creates a new commit whose changes are the exact opposite of the given commit. History stays intact, everyone can pull normally, and the revert itself can be reverted later if needed. Reverting a merge commit requires telling Git which parent to keep, usually with -m 1.

When you must rewrite a pushed branch

On your own feature branch, rewriting after pushing is common, for example after rebasing onto main or squashing before review. Push with:

git push --force-with-lease

Unlike --force, this refuses to overwrite the remote branch if someone else pushed to it since your last fetch, so you cannot silently delete a colleague's commits.

Quick decision table

SituationCommandRewrites history?
Discard uncommitted edits to a filegit restore <file>No, but edits are lost
Unstage a filegit restore --staged <file>No
Fix the last unpushed commitgit commit --amendYes
Uncommit, keep changesgit reset --soft HEAD~1Yes
Delete unpushed commitsgit reset --hard HEAD~1Yes
Undo a pushed commitgit revert <hash>No

Recovering "lost" commits with the reflog

Git records every position HEAD has pointed to in the reflog, including commits you reset away or that disappeared in a bad rebase:

git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~1
# 9f8e7d6 HEAD@{1}: commit: Add payment webhook
git branch rescue 9f8e7d6

Find the entry from before the mistake, then create a branch at it or reset back to it. Reflog entries are kept for about 90 days by default, and they are local to your clone. The reflog cannot recover changes that were never committed, which is a good argument for committing early and often on feature branches.

Habits that make undo easy

  • Commit small, focused changes. They are easier to revert individually.
  • Create a backup branch before risky operations: git branch backup-before-rebase.
  • Run git status before any reset, restore or clean.
  • Protect shared branches on your Git host so force pushes are rejected.

If you do not remember the exact syntax, the Git Command Generator builds these commands from a description of what you want to do, with a warning on each one that rewrites history or deletes work.