# Undoing changes

> Choose correctly between restore, commit --amend, reset and revert to undo anything from an unsaved edit to a pushed commit.

Git & Collaboration roadmap · Everyday workflow · 18 min read · https://levelcode.org/roadmaps/git/undoing-changes

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:

> [!IMPORTANT]
> **Rewrite only what you have not shared.** Amending and resetting replace commits with new ones. That is fine for commits that exist only on your machine. For commits already pushed to a shared branch, use `git revert`, which adds a new commit instead of changing history others have built on.

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
```

> [!NOTE]
> Before `git restore` existed (Git 2.23), the same jobs were done with `git checkout -- app.js` (discard edits) and `git reset HEAD app.js` (unstage). You will still see these in older answers and scripts; they behave the same way.

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

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

```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)
```

> [!WARNING]
> `git reset --hard` also wipes **uncommitted** changes in tracked files, and those were never stored in Git, so nothing can bring them back. Run `git status` first, and commit or stash anything you might want.

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

> **Interactive: reset --hard and the reflog.** 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. Try it: https://levelcode.org/labs/git-graph

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

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

## Quiz

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

   A. git revert HEAD
   B. git reset --hard HEAD~1
   C. git restore --staged .
   D. git commit --amend -m "Correct message"

   **Answer: D.** --amend replaces the last commit with a new one that has the corrected message. That is safe because nobody else has the old commit. Revert would add a second commit; reset --hard would throw the work away.

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

   A. Staged in the index, and present in the working tree
   B. Deleted entirely
   C. In the working tree but unstaged
   D. Stashed automatically

   **Answer: A.** --soft only moves the branch. The index and working tree keep the content of the old tip, so the removed commits' changes show as staged, ready to recommit.

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

   A. git reset --hard HEAD~1 then git push --force
   B. git revert <commit> and push the new commit
   C. git commit --amend and push
   D. Delete the branch on the remote and push again

   **Answer: B.** Revert adds a new commit that undoes the bad one, so shared history is not rewritten and teammates can pull normally. Resetting and force-pushing would break everyone who already has the commit.

4. Which of these can the reflog NOT help you recover?

   A. A commit removed by git reset --hard
   B. The commit replaced by git commit --amend
   C. Uncommitted edits discarded by git restore
   D. A branch tip before a rebase

   **Answer: C.** The reflog records where HEAD and branches pointed, so any commit you once had is findable. Edits that were never committed (or staged) were never stored in Git, so restoring over them is permanent.
