Git: Undoing Changes
Git gives you multiple ways to undo things depending on where in the workflow the mistake occurred — in the working tree, in the staging area, in the last commit, or deeper in history. Knowing which tool to reach for (and what the consequences are) is critical. Some undos are safe and reversible; others rewrite history and can cause problems if others have seen those commits.
Commands Covered
git revert · git reset · git restore · git clean
Undo Decision Tree
| Where is the mistake? | Command | Safe to use on pushed commits? |
|---|---|---|
| Working tree (unsaved edit) | git restore <file> | Yes — local only |
| Staging area (git add'd but not committed) | git restore --staged <file> | Yes — local only |
| Last commit (not yet pushed) | git commit --amend | No — rewrites history |
| Last commit (already pushed) | git revert HEAD | Yes — creates a new commit |
| Multiple old commits (not pushed) | git reset | No — rewrites history |
| Multiple old commits (pushed) | git revert each commit | Yes — creates new commits |
| Untracked files (not committed) | git clean | Yes — local only; irreversible |
git revert
Creates a new commit that undoes the changes from a previous commit. The original commit stays in history — nothing is erased. This is the safe way to undo pushed commits.
# Revert the most recent commit $ git revert HEAD # Revert a specific commit (by hash) $ git revert abc1234 # Revert without auto-committing (stage the reversal for review) $ git revert --no-commit abc1234 $ git revert -n abc1234 # Then: git commit -m "Revert the foo feature" # Revert a range of commits (reverting oldest first is usually safer) $ git revert abc1234..def5678 # This reverts all commits between abc1234 and def5678 (exclusive of abc1234) # Revert a merge commit (must specify which parent to revert to) $ git revert -m 1 abc1234 # -m 1 = keep the first parent (main branch side)
git reset
Moves the current branch pointer backward to an earlier commit. Has three modes controlling what happens to the changes between the old and new HEAD.
# Soft reset — move HEAD back, keep changes staged $ git reset --soft HEAD~1 # The last commit is undone; files are staged as they were just before commit. # Useful: you committed too soon, want to restructure the commit. # Mixed reset (default) — move HEAD back, unstage changes, keep in working tree $ git reset HEAD~1 $ git reset --mixed HEAD~1 # The last commit is undone; changes are in your working tree but unstaged. # Useful: undo a commit and re-stage things differently. # Hard reset — move HEAD back, discard all changes completely $ git reset --hard HEAD~1 # WARNING: changes are GONE — not in staging area, not in working tree. # (Can be recovered with git reflog if you're quick) # Reset to a specific commit $ git reset --hard abc1234 # Unstage a file (without touching HEAD) — same as git restore --staged $ git reset HEAD myfile.c $ git reset -- myfile.c # Unstage everything (keep changes in working tree) $ git reset HEAD
--soft vs --mixed vs --hard
| Flag | HEAD moves? | Index (staged)? | Working tree? |
|---|---|---|---|
--soft | Yes | Unchanged (still staged) | Unchanged |
--mixed (default) | Yes | Reset (unstaged) | Unchanged |
--hard | Yes | Reset | Reset (changes lost) |
# Recovering from a bad hard reset using reflog:
$ git reflog
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: The commit I accidentally removed
$ git reset --hard def5678 # restore to before the reset
# (The reflog keeps entries for ~90 days by default)
git restore
(Covered more fully in the Staging article — key points for undoing:)
# Discard changes in a file (revert to last committed state) $ git restore myfile.c # This is destructive — unsaved changes are gone # Unstage a file (keep changes in working tree) $ git restore --staged myfile.c # Restore a file to the state it was in 3 commits ago $ git restore --source=HEAD~3 myfile.c
git clean
Removes untracked files from the working tree. Useful after a build that left artifacts everywhere, or to reset to a clean state. Irreversible — files are deleted, not moved to trash.
# Show what would be deleted (dry run — always do this first) $ git clean -n $ git clean --dry-run # Delete untracked files (requires -f, a safety measure) $ git clean -f # Delete untracked files AND untracked directories $ git clean -fd # Delete untracked files including ignored files (e.g. build output) $ git clean -fx # Delete everything untracked including directories and ignored files $ git clean -fdx # WARNING: this is the nuclear option — use git clean -n first # Interactive mode (confirm each file) $ git clean -i $ git clean --interactive # Clean only a specific directory $ git clean -f src/
Amending the Last Commit
# Fix the message of the last commit $ git commit --amend -m "Better message" # Add a forgotten file to the last commit $ git add forgotten-file.txt $ git commit --amend --no-edit # reuse existing commit message # Amend the author $ git commit --amend --author="New Name <new@email.com>" # WARNING: amend creates a new commit object — the old commit SHA is gone. # If you've already pushed, you'll need --force-with-lease to push. # Don't amend shared commits that others have already pulled.
dispelled