---
title: "Undoing Changes"
description: "Compare restore, reset, and revert by the repository layer each one changes."
source: https://vizlearn.app/cs/version-control/undoing-changes
---

# Undoing Changes

Undoing changes in Git means choosing which snapshot to copy, which ref to move, or whether to record a new inverse commit. `git restore`, `git reset`, and `git revert` can produce the same file contents while leaving different Index state, branch history, object counts, and recovery pointers behind.

Think of the repository as a desk with three trays: the Working Tree, the Index, and the commit reached by `HEAD -> main`. Restore copies paper between trays. Reset moves the label on the filing cabinet, then may refill one or both trays. Revert files a new correction sheet. The page may read the same afterward; the audit trail does not.

## How undoing changes works

`git restore app.txt` uses the Index as its default source and overwrites the Working Tree copy. It does not move `main`, rewrite the Index, or create an object. With `--staged`, the default source changes to the `HEAD` tree and the destination becomes the Index. Restore is a file-layer copy, not a history operation.

`git reset <target>` first resolves a target commit, records the old branch tip in `ORIG_HEAD`, and moves `main` to the target. The mode controls the remaining copies:

| Mode | Move `main` | Copy target to Index | Copy target to Working Tree | New commit |
| --- | --- | --- | --- | --- |
| `--soft` | yes | no | no | no |
| `--mixed` | yes | yes | no | no |
| `--hard` | yes | yes | yes | no |

That table is the whole reset family. `--soft` is useful when you want to move the branch but keep the old tip's snapshot staged. Mixed reset also replaces the Index. Hard reset goes one boundary farther and overwrites tracked Working Tree bytes. If those bytes were never staged or committed, `ORIG_HEAD` cannot rescue them; it stores a commit ID, not a filesystem backup.

`git revert HEAD` takes the opposite approach to shared history. It compares the selected commit with its parent, derives the inverse change, applies the result to the Index and Working Tree, writes a new commit, and advances `main` to that commit. The bad commit remains an ancestor. Revert does not move backward. It records that moving forward required a correction.

## Undoing changes step by step

The default run starts with an unborn `main` and executes these exact commands:

1. `edit app.txt` writes `mode=safe` only to the Working Tree.
2. `git add app.txt` hashes those bytes into a blob and makes the Index entry point to it.
3. `git commit -m "base"` writes the Index tree, writes root commit C1, then advances `main` to C1. Working Tree, Index, and `HEAD` now all contain `mode=safe`.
4. A second edit changes only the Working Tree to `mode=draft`.
5. `git restore app.txt` resolves its default source as the Index and copies `mode=safe` back into the Working Tree. The draft disappears, but `main` and the Index do not move.
6. Another edit writes `mode=unsafe` to the Working Tree.
7. `git add app.txt` stores the unsafe blob and points the Index at it.
8. `git commit -m "bad-change"` writes C2 with C1 as its parent, then advances `main` to C2. All three file layers now agree on `mode=unsafe`.
9. `git reset --soft HEAD^` resolves C1, writes C2 to `ORIG_HEAD`, and moves `main` back to C1. The Index and Working Tree remain `mode=unsafe`, so the unsafe snapshot is staged relative to the new `HEAD`.
10. `git reset --soft ORIG_HEAD` resolves the saved C2 before replacing `ORIG_HEAD` with C1. It moves `main` forward to C2 and still leaves both file layers alone. This ordering matters; resolving `ORIG_HEAD` after overwriting it would target the wrong commit.
11. `git revert HEAD` verifies that Index and Working Tree match C2, derives `mode=unsafe -> mode=safe` from C2 and C1, and applies that result. The existing C1 tree can be reused, so the operation adds one commit object C3, not a new blob or tree. Finally, `main` advances to C3. C2 remains C3's parent and stays reachable.

The final file says `mode=safe`, exactly as it did at C1. A hard reset to C1 could produce that same text with `main` pointing backward and only two stored commits. Revert instead leaves `main` at a third commit whose parent is the bad one. File content is evidence about one snapshot, not proof of how the repository got there.

## What each undo command writes

The useful question is not "does the file look fixed?" Ask which durable names and snapshots changed.

| Command family | Working Tree | Index | `main` | Object database |
| --- | --- | --- | --- | --- |
| `restore app.txt` | copied from Index | unchanged | unchanged | unchanged |
| `restore --staged app.txt` | unchanged | copied from `HEAD` | unchanged | unchanged |
| `reset --soft` | unchanged | unchanged | moved | unchanged |
| `reset --mixed` | unchanged | copied from target | moved | unchanged |
| `reset --hard` | copied from target | copied from target | moved | unchanged |
| `revert HEAD` | inverse result | inverse result | advanced | one new commit in this trace |

In the one-file engine, hashing or copying file bytes costs O(B) for B bytes. Resolving `HEAD^` is O(1) because commits have one parent and the target is adjacent. Classifying which stored commits remain reachable from `main` costs O(V + E). Those are costs of this browser model, not performance promises about Git's index, packfiles, revision walker, or filesystem.

## A note on simplification: one file, linear history

This lesson uses one local repository, an attached `HEAD -> main`, one regular tracked file, and single-parent commits. It implements whole-file restore, `--soft`/`--mixed`/`--hard` reset, and a clean `git revert HEAD` with deterministic identities and real Git-format SHA-1 object IDs.

It omits pathspec sets, patch mode, deletes, renames, file modes, untracked-file obstruction, detached HEAD, multiple branches, merge commits, `revert -m`, arbitrary older-commit conflicts, the revert sequencer, hooks, signatures, remotes, reflog expiry, garbage collection, and editor or filesystem recovery. Consult the official [`git restore`](https://git-scm.com/docs/git-restore), [`git reset`](https://git-scm.com/docs/git-reset), and [`git revert`](https://git-scm.com/docs/git-revert) documentation before using a destructive command on real work.

## Edge cases

- An unborn `HEAD` has no tree, so `git restore --staged app.txt` has no default source.
- A root commit has no first parent. `HEAD^` therefore fails instead of resolving to an empty tree.
- `ORIG_HEAD` is unset until a successful reset in this model. An unknown or ambiguous object-ID prefix also fails without mutation.
- Restore becomes a no-op when the destination already matches its source. No ref or object changes to make the command look busier.
- Revert refuses when either the Index or Working Tree differs from `HEAD`. This scoped guard prevents a fabricated conflict workflow.
- Hard reset can discard tracked Working Tree bytes that Git has never stored as an object. A commit recovery pointer is not a backup of uncommitted edits.

## Common mistakes

- Saying restore "moves a file." It copies a snapshot into a destination; the source remains where it was.
- Saying reset deletes commits. Reset moves a ref. A displaced commit may still be named by `ORIG_HEAD` or another ref, and object cleanup is a separate concern.
- Treating `ORIG_HEAD` as the reflog. It is one pseudo-ref containing one commit ID. The reflog is a separate history of ref updates and is outside this lesson.
- Saying revert stores a patch object. Commits store trees and parent links. Git computes an inverse change, then records the resulting snapshot in a new commit.
- Running `git reset --hard` because the target commit is recoverable. The target may be recoverable while overwritten, uncommitted Working Tree bytes are not.
