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.

18 min 7 sections

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?

SituationCommandDestroys data?
Throw away unstaged edits to a filegit restore <file>Yes, those edits
Unstage a file but keep the editsgit restore --staged <file>No
Fix the message or contents of your last, unpushed commitgit commit --amendNo (old commit stays in reflog)
Remove recent local commits, keep the changesgit reset --soft or git resetNo
Remove recent local commits and their changesgit reset --hardYes, uncommitted work
Undo a commit that has been pushedgit revert <commit>No
Delete untracked filesgit cleanYes

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:

text
4b90e2a Add app
591aa31 Add feature A
2eb3405 Add feature B   ← main ← HEAD

Discarding 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:

bash
echo "oops" >> app.js
git status --short
git restore app.js
git status --short
text
 M app.js

The 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:

bash
git add app.js
git restore --staged app.js

The 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:

bash
git restore --source=591aa31 app.js     # or --source=HEAD~1

Finally, git restore only affects tracked files. To remove untracked files, use git clean, always with a dry run first:

bash
git clean -n     # list what would be removed
git clean -fd    # really remove untracked files (-f) and directories (-d)
text
Would remove draft.txt

Fixing the last commit with --amend

Forgot a file, or made a typo in the message? If you have not pushed yet, amend:

bash
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
text
[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:

text
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:

ModeBranch moves?IndexWorking treeUse it to
--softYesUnchangedUnchangedRecommit the same changes differently
--mixed (default)YesReset to match the commitUnchangedRe-stage changes selectively
--hardYesResetResetThrow everything away

Each mode goes one area further than the previous one. Here they are on the example history, undoing "Add feature B":

bash
git reset --soft HEAD~1
git status
text
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   app.js

The 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:

bash
git reset HEAD~1
text
Unstaged changes after reset:
M	app.js

With --hard, the branch, index and working tree all snap back:

bash
git reset --hard HEAD~1
git log --oneline
text
HEAD is now at 591aa31 Add feature A
591aa31 Add feature A
4b90e2a Add app
text
Before:  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:

bash
git reflog -3
text
591aa31 HEAD@{0}: reset: moving to HEAD~1
2eb3405 HEAD@{1}: commit: Add feature B
591aa31 HEAD@{2}: commit: Add feature A

To 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.

Interactivereset --hard and the reflogOpen in Labs
Loading interactive…

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:

bash
git revert HEAD

Git 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.)

text
[main 526dd50] Revert "Add feature B"
 Date: Sun Oct 11 09:20:41 2026 +0000
 1 file changed, 1 deletion(-)
text
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:

bash
git revert 591aa31             # revert any commit, not just the latest
git revert --no-commit HEAD~2..HEAD   # revert the last two commits as one change

You 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:

bash
git revert -m 1 1024d74

Without -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:

  1. Append a line junk, stage it, and get back to a clean state in two separate steps (unstage, then discard).
  2. Change the message of three to third commit.
  3. Remove third commit from the branch but keep its change staged. Recommit it.
  4. Pretend the branch is pushed: undo two without rewriting history. Predict whether this will conflict.
  5. Use the reflog to restore the branch to how it was right after step 2.
Show solution
bash
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~1

Step 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.)

bash
# 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, one

Key takeaways

  • Decide where the mistake lives — working tree, index, local commit or shared commit — before picking a command.
  • git restore discards working-tree edits; git restore --staged unstages without losing anything.
  • git commit --amend and git reset replace history, so use them only on commits you have not shared.
  • reset --soft keeps changes staged, --mixed keeps them unstaged, --hard discards them, including uncommitted work.
  • git revert undoes a pushed commit by adding an inverse commit; merges need -m 1.
  • Dropped commits can be recovered through the reflog or ORIG_HEAD for weeks; uncommitted changes cannot.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.You committed with a typo in the message and have not pushed. What is the simplest fix?

  2. 2.After git reset --soft HEAD~2, where are the changes from the two removed commits?

  3. 3.A bad commit was pushed to main yesterday and teammates have pulled it. How should you undo it?

  4. 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.