Step 05 of 10
Undoing changes
Choose correctly between restore, commit --amend, reset and revert to undo anything from an unsaved edit to a pushed commit.
Everyone makes mistakes in Git: a file committed by accident, a broken commit pushed to main, an hour of experiments you want to throw away. Git can undo almost anything, but it offers several tools that look similar and behave very differently. This lesson gives you a decision guide, then works through each tool — restore, commit --amend, reset and revert — with the three areas in mind, so you always know what will be kept and what will be destroyed.
Choose the right tool
The first question is always: where is the mistake? In the working tree, in the index, in a local commit, or in a commit other people already have?
| Situation | Command | Destroys data? |
|---|---|---|
| Throw away unstaged edits to a file | git restore <file> | Yes, those edits |
| Unstage a file but keep the edits | git restore --staged <file> | No |
| Fix the message or contents of your last, unpushed commit | git commit --amend | No (old commit stays in reflog) |
| Remove recent local commits, keep the changes | git reset --soft or git reset | No |
| Remove recent local commits and their changes | git reset --hard | Yes, uncommitted work |
| Undo a commit that has been pushed | git revert <commit> | No |
| Delete untracked files | git clean | Yes |
The dividing line between the top and bottom halves of that table is the most important rule in this lesson:
Let's work through each tool using a small project whose history looks like this:
4b90e2a Add app
591aa31 Add feature A
2eb3405 Add feature B ← main ← HEADDiscarding and unstaging with git restore
git restore copies file content into the working tree or index from somewhere else. With no options, it overwrites a working-tree file with the version in the index — that is, it discards your unstaged edits:
echo "oops" >> app.js
git status --short
git restore app.js
git status --short M app.jsThe second status prints nothing: the edit is gone. Git never saw that content, so there is no way to get it back. Check git diff before you restore.
To unstage a file while keeping your edits on disk, restore the index from HEAD:
git add app.js
git restore --staged app.jsThe file moves from "Changes to be committed" back to "Changes not staged for commit". Nothing is lost. To do both at once — unstage and discard — use git restore --staged --worktree app.js.
You can also pull a file out of any commit, which is handy when you have broken one file and want an older version back without touching the rest of the project:
git restore --source=591aa31 app.js # or --source=HEAD~1Finally, git restore only affects tracked files. To remove untracked files, use git clean, always with a dry run first:
git clean -n # list what would be removed
git clean -fd # really remove untracked files (-f) and directories (-d)Would remove draft.txtFixing the last commit with --amend
Forgot a file, or made a typo in the message? If you have not pushed yet, amend:
git add forgotten-test.js
git commit --amend --no-edit # add staged changes, keep the message
git commit --amend -m "Add feature B with tests" # or replace the message[main 7f3d1c8] Add feature B with tests
Date: Sun Oct 11 09:14:02 2026 +0000
2 files changed, 12 insertions(+)Amending does not edit the old commit — commits are immutable. It builds a new commit from the current index with the same parent, and moves the branch to it:
Before: A ── B ── C ← main
After: A ── B ── C' ← main (C is no longer on any branch)That is why the ID changes, and why amending a commit someone else has already pulled causes trouble: their history contains C, yours contains C'.
Moving a branch back with git reset
git reset <commit> moves the current branch pointer to <commit>. The commits after it drop off the branch. The mode decides what happens to the index and working tree:
| Mode | Branch moves? | Index | Working tree | Use it to |
|---|---|---|---|---|
--soft | Yes | Unchanged | Unchanged | Recommit the same changes differently |
--mixed (default) | Yes | Reset to match the commit | Unchanged | Re-stage changes selectively |
--hard | Yes | Reset | Reset | Throw everything away |
Each mode goes one area further than the previous one. Here they are on the example history, undoing "Add feature B":
git reset --soft HEAD~1
git statusOn branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: app.jsThe commit is gone from the branch, but its changes are still staged — ready to be committed again, perhaps split differently or with a better message. A common use is squashing several messy local commits into one: git reset --soft HEAD~3 followed by a single git commit.
With the default --mixed mode, the changes stay on disk but are unstaged:
git reset HEAD~1Unstaged changes after reset:
M app.jsWith --hard, the branch, index and working tree all snap back:
git reset --hard HEAD~1
git log --onelineHEAD is now at 591aa31 Add feature A
591aa31 Add feature A
4b90e2a Add appBefore: 4b90e2a ── 591aa31 ── 2eb3405 ← main
After: 4b90e2a ── 591aa31 ← main
2eb3405 (unreachable, kept in the reflog)The safety net
Committed work dropped by reset or --amend is not deleted immediately. Git records every movement of HEAD in the reflog:
git reflog -3591aa31 HEAD@{0}: reset: moving to HEAD~1
2eb3405 HEAD@{1}: commit: Add feature B
591aa31 HEAD@{2}: commit: Add feature ATo undo the reset, reset back to the old position: git reset --hard HEAD@{1} (or git reset --hard 2eb3405). Right after a reset or merge, Git also keeps the previous position in ORIG_HEAD, so git reset --hard ORIG_HEAD works too. Unreachable commits are eventually garbage-collected (after about 30 days by default), so the reflog is a safety net, not an archive. It gets fuller treatment in the power tools lesson.
Try the whole round trip on the example history: reset, look at the log and the reflog, then reset back.
reset --hard only moves main. The dropped commit is still there, faded because no branch reaches it, and git log no longer shows it. The reflog does, so HEAD@{1} names it and a second reset restores the branch. Make a new commit after the first reset and the old one stays stranded.
Undoing shared history with git revert
Once a commit is on a shared branch, you should not remove it. Instead, git revert creates a new commit that applies the exact inverse of the old one:
git revert HEADGit opens your editor with a message like Revert "Add feature B" and a line This reverts commit 2eb3405.... Add why you are reverting, save, and close. (--no-edit accepts the default message.)
[main 526dd50] Revert "Add feature B"
Date: Sun Oct 11 09:20:41 2026 +0000
1 file changed, 1 deletion(-)Before: A ── B ── C ← main, origin/main
After: A ── B ── C ── C⁻¹ ← main (C⁻¹ undoes C)History only grows, so teammates can pull normally. Some useful variations:
git revert 591aa31 # revert any commit, not just the latest
git revert --no-commit HEAD~2..HEAD # revert the last two commits as one changeYou can revert an old commit even if later commits exist; if those later commits changed the same lines, you will get a conflict to resolve, exactly as in a merge.
Reverting a merge commit
A merge commit has two parents, so Git needs to know which side to keep. Use -m 1 to say "keep the first parent's line" — the branch that was merged into, usually main:
git revert -m 1 1024d74Without -m, Git stops with error: commit 1024d74... is a merge but no -m option was given.
One trap: after reverting a merge, Git still considers that branch's commits merged. If the feature is fixed later and you merge the branch again, only the new commits come in. To bring the original changes back, revert the revert commit first.
Practice
Create a repository with three commits (one, two, three, each appending a line to log.txt). Then:
- Append a line
junk, stage it, and get back to a clean state in two separate steps (unstage, then discard). - Change the message of
threetothird commit. - Remove
third commitfrom the branch but keep its change staged. Recommit it. - Pretend the branch is pushed: undo
twowithout rewriting history. Predict whether this will conflict. - Use the reflog to restore the branch to how it was right after step 2.
Show solution
git init undo-lab && cd undo-lab
for n in one two three; do echo "$n" >> log.txt; git add log.txt; git commit -m "$n"; done
# 1. unstage, then discard
echo junk >> log.txt
git add log.txt
git restore --staged log.txt # back to "not staged"
git restore log.txt # edit discarded
git status # clean
# 2. reword the last commit
git commit --amend -m "third commit"
# 3. drop it but keep the change staged, then recommit
git reset --soft HEAD~1
git status # log.txt staged
git commit -m "third commit"
# 4. revert "two" safely
git revert HEAD~1Step 4 conflicts. Reverting two means removing the line two, but the later commit added three right next to it, so Git cannot apply the inverse cleanly — adjacent edits are treated as overlapping. Resolve it by editing log.txt to contain one and three, then git add log.txt and git revert --continue. (Or run git revert --abort to back out.)
# 5. find the position after step 2 and go back
git reflog # look for "commit (amend): third commit"
git reset --hard HEAD@{N} # N = the number shown on that line
git log --oneline # third commit, two, oneKey takeaways
- Decide where the mistake lives — working tree, index, local commit or shared commit — before picking a command.
git restorediscards working-tree edits;git restore --stagedunstages without losing anything.git commit --amendandgit resetreplace history, so use them only on commits you have not shared.reset --softkeeps changes staged,--mixedkeeps them unstaged,--harddiscards them, including uncommitted work.git revertundoes a pushed commit by adding an inverse commit; merges need-m 1.- Dropped commits can be recovered through the reflog or
ORIG_HEADfor weeks; uncommitted changes cannot.
Check yourself
Quiz
0/4 correct · 4 left
1.You committed with a typo in the message and have not pushed. What is the simplest fix?
2.After git reset --soft HEAD~2, where are the changes from the two removed commits?
3.A bad commit was pushed to main yesterday and teammates have pulled it. How should you undo it?
4.Which of these can the reflog NOT help you recover?
Concepts in this lesson
One-page notes on each idea, for review or a quick lookup later.
Finished reading?
Mark it complete to fill in your roadmap.