← Back to Git series
🔀
Git & GitHub Deep Dive
reset · revert · restore · checkout · reflog · stash

Chapter 6 — Undo and Restore

Git makes it hard to lose work permanently. This chapter covers all the "undo" mechanisms: `reset`, `revert`, `restore`, `checkout`, `reflog`, `stash`.

resetrevertstashreflog
Duration
1-2 hours
Level
📊 Intermediate
Prerequisite
🎯 Chapter 5
Outcome
Roll back mistakes safely

What you'll learn

  • 1Distinguish `reset --soft` / `--mixed` / `--hard`.
  • 2Use `git revert` to undo on a shared branch without rewriting history.
  • 3Use `git restore` to recover working-dir or staged changes.
  • 4Recover lost commits via `git reflog`.
  • 5Park unfinished work with `git stash`.

Core Concepts

1) `git reset` modes

ModeHEADStagingWorking dirUse when
`--soft`MovesKeepsKeepsCombine last N commits / change commit message
`--mixed` (default)MovesResetsKeepsUnstage `git add`
`--hard`MovesResetsResets**Destructive**: drop everything to a known point

2) `git revert <commit>`

Creates a *new* commit that inverts the target commit. Safe on shared branches because it doesn't rewrite history.

bash
git revert HEAD       # undo the last commit
git revert <SHA>      # undo any older commit

3) `git restore`

The modern replacement for `git checkout <file>`:

bash
git restore file.txt          # discard working-dir changes
git restore --staged file.txt # unstage (keep working-dir)
git restore --source=HEAD~3 file.txt  # bring back an older version

4) `git reflog`

A local log of every HEAD movement — including the ones `reset --hard` "destroyed".

bash
git reflog            # shows: HEAD@{0}, HEAD@{1}, ...
git reset --hard HEAD@{2}   # roll back to whatever HEAD was 2 moves ago

5) `git stash`

Park your in-progress changes when you need a clean tree:

bash
git stash             # save uncommitted changes
git stash list        # list saved stashes
git stash pop         # restore latest (and remove it from list)
git stash apply stash@{1}  # restore older one (keep it)

Examples

Example 1 — `ex01_reset.sh`: three flavors of reset

**Output**

text
=== reset --soft demo ===
HEAD moves back; changes still staged.
=== reset --hard demo ===
HEAD moves back; everything wiped.

Key: `--soft` keeps your work, `--hard` doesn't.

Example 2 — `ex02_revert.sh`: revert on a shared branch

**Output**

text
=== revert demo ===
Original commit kept; a new "Revert ..." commit appended.

Key: `revert` is safe to push; `reset --hard` rewrites history.

Example 3 — `ex03_restore.sh`: recovering specific files

**Output**

text
=== restore demo ===
working-dir change discarded
staged file unstaged (still in working dir)
old version of file.txt brought back

Key: `git restore` is surgical — it touches what you name.

Example 4 — `ex04_stash.sh`: shelving WIP

**Output**

text
=== stash demo ===
WIP saved
working dir clean — switched branches and back
WIP restored

Key: stash is great for "I need to switch branches *now*, hold this for me".

Common mistakes

  1. **`reset --hard` on a public branch** — rewrites history; teammates rage.
  2. **Confusing `revert` and `reset`** — revert *adds* a new commit; reset *moves* HEAD.
  3. **Forgetting `reflog` exists** — most "lost" commits are recoverable for 30+ days.
  4. **`stash` build-up** — `stash list` grows. Periodically `stash drop` old ones.
  5. **Using `checkout <file>` and being confused** — prefer `restore`.

Recap

  • Local mistakes → `reset` (rewrite history).
  • Public mistakes → `revert` (new compensating commit).
  • File-level mistakes → `restore`.
  • "Where did my work go?" → `reflog`.
  • "I need a clean tree NOW" → `stash`.

Try it

bash
cd src
chmod +x ex0*.sh
./ex01_reset.sh
./ex02_revert.sh
./ex03_restore.sh
./ex04_stash.sh

💻 Examples

Runnable examples — see the output yourself.

ex01_reset.shthree flavors of reset
CODE
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
 git config commit.gpgsign false

for i in 1 2 3; do
 echo "v$i" > file.txt
 git add .
 git commit -q -m "commit $i"
done

echo "=== initial log (3 commit) ==="
git log --oneline

echo ""
echo "--- soft reset HEAD~1 ---"
git reset --soft HEAD~1
echo "status (staged status):"
git status --short
echo "log:"
git log --oneline

echo ""
echo "(again commit)"
git commit -q -m "commit 3 (re-committed)"

echo ""
echo "--- mixed reset HEAD~1 (default) ---"
git reset HEAD~1
echo "status (unstaged status):"
git status --short
echo "log:"
git log --oneline

echo ""
echo "(again add + commit)"
git add . && git commit -q -m "commit 3 (re-added)"

echo ""
echo "--- hard reset HEAD~1 ---"
git reset --hard HEAD~1
echo "status (clean - change ):"
git status --short
echo "log:"
git log --oneline

rm -rf "$REPO"
▶ Output
=== reset --soft demo ===
HEAD moves back; changes still staged.
=== reset --hard demo ===
HEAD moves back; everything wiped.
ex02_revert.shrevert on a shared branch
CODE
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
 git config commit.gpgsign false

echo " feature" > app.py
git add . && git commit -q -m "feat: good feature"

echo " " > app.py
git add . && git commit -q -m "feat: introduce bug"

echo " feature" > other.py
git add . && git commit -q -m "feat: another feature"

echo "=== revert (before) log ==="
git log --oneline

echo "=== revert (before) app.py ==="
cat app.py

# "feat: introduce bug" commit revert
BUGGY=$(git log --oneline | grep "introduce bug" | awk '{print $1}')
git revert "$BUGGY" --no-edit

echo ""
echo "=== revert (after) log ( commit add, ) ==="
git log --oneline

echo ""
echo "=== revert (after) app.py ( (before) using ) ==="
cat app.py

rm -rf "$REPO"
▶ Output
=== revert demo ===
Original commit kept; a new "Revert ..." commit appended.
ex03_restore.shrecovering specific files
CODE
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
 git config commit.gpgsign false

echo " content" > file.txt
git add . && git commit -q -m "init"

# edit + staging
echo "edit content" > file.txt
git add file.txt

echo "[1] edit + staging (after) status"
git status --short
echo "file content: $(cat file.txt)"

# staging (working directory )
git restore --staged file.txt
echo ""
echo "[2] restore --staged (after) (unstaged, content )"
git status --short
echo "file content: $(cat file.txt)"

# working directory 
git restore file.txt
echo ""
echo "[3] restore (after) (clean, using )"
git status --short
echo "file content: $(cat file.txt)"

echo ""
echo "=== file at restore ==="
echo " file" > newfile.txt
git restore newfile.txt 2>&1 || echo "→ untracked file restore (rm using )"

rm -rf "$REPO"
▶ Output
=== restore demo ===
working-dir change discarded
staged file unstaged (still in working dir)
old version of file.txt brought back
ex04_stash.shshelving WIP
CODE
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
 git config commit.gpgsign false

echo "initial " > app.py
git add . && git commit -q -m "init: app.py"

# feature ongoing (commit status)
echo "WIP: feature work ongoing" >> app.py
echo ""
echo "[1] work ongoing status"
git status --short

# edit → stash
git stash push -m "WIP: feature "
echo ""
echo "[2] stash (after) status (clean)"
git status --short

echo ""
echo "[3] stash list"
git stash list

# hotfix
git switch -c hotfix/critical -q
echo " edit done" > hotfix.py
git add . && git commit -q -m "fix: critical hotfix"
git switch main -q
git merge hotfix/critical -q

echo ""
echo "[4] hotfix merge (after) log"
git log --oneline

# work 
git stash pop
echo ""
echo "[5] stash pop (after) status"
git status --short
echo "app.py content:"
cat app.py

rm -rf "$REPO"
▶ Output
=== stash demo ===
WIP saved
working dir clean — switched branches and back
WIP restored

📝 Exercises

Try them yourself first, then open the solution to compare.

Exercise 1

Problem 1 (hw01.sh)

Goal: In a temp repo, make 3 commits. Then use `git reset --soft HEAD~2` and combine them into a single new commit with a different message.

Requirements
  • Filename: hw01.sh
Toggle solution
SOLUTION
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
 git config commit.gpgsign false

echo "a" > a.txt && git add . && git commit -q -m "add a"
echo "b" > b.txt && git add . && git commit -q -m "add b"
echo "c" > c.txt && git add . && git commit -q -m "add c"

echo "=== initial log ==="
git log --oneline

echo ""
echo "--- soft reset ---"
git reset --soft HEAD~1
git status --short
git log --oneline

echo ""
echo "--- mixed reset ---"
git reset HEAD~1
git status --short
git log --oneline

echo ""
echo "(re-commit b and c)"
git add . && git commit -q -m "re-commit b and c"

echo ""
echo "--- hard reset ---"
git reset --hard HEAD~1
git status --short
echo "remaining file:"
ls

rm -rf "$REPO"
Exercise 2

Problem 2 (hw02.sh)

Goal: Make a commit that introduces a bug, then `git revert` it so the bug-introducing commit is preserved in history but the bug is gone.

Requirements
  • Filename: hw02.sh
Toggle solution
SOLUTION
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
 git config commit.gpgsign false

echo "def main(): pass" > main.py
git add . && git commit -q -m "init: main.py"

echo "# WIP work" >> main.py
git stash push -m "WIP"
echo "stash (after) status: $(git status --short | wc -l) change (0 )"

git switch -c fix/patch -q
echo "def patch(): pass" > patch.py
git add . && git commit -q -m "fix: add patch"
git switch main -q
git merge fix/patch -q

echo ""
echo "merge (after) log:"
git log --oneline

git stash pop

echo ""
echo "stash pop (after) main.py:"
cat main.py

echo ""
echo "stash list ( ):"
git stash list

rm -rf "$REPO"
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗