Git & GitHub track

Lesson 06 · Lessons

Undoing Changes

reset, revert, restore, reflog and stash

Lessons6 min readFree lesson

One of the great things about Git is that you can revert your mistakes. reset, revert, restore, reflog, stash — If you use the five tools according to the situation, You can safely handle bad commits, incorrectly staged files, and tasks that require temporary storage.

Learning Objectives

  • Explains the difference between git reset's three modes of soft, mixed, and hard and can be applied to the right situation.
  • You can safely revert commits to a shared branch with git revert.
  • You can cancel changes to the staging/working directory with git restore.
  • You can recover commits lost after reset/checkout with git reflog.
  • You can temporarily save and restore changes in progress with git stash.

Key concepts

1) git reset

HEAD → Reverts commit history.

--soft : Move HEAD + Maintain staging + Maintain working
--mixed : Move HEAD + Cancel staging + Keep working (default)
--hard : Move HEAD + Cancel staging + Cancel walking ⚠️
git reset --soft  HEAD~1    #Only commits are canceled, changes are staged
git reset --mixed HEAD~1    #Commit + Cancel Staging
git reset --hard  HEAD~1    #Cancel all (delete working directory changes)

2) git revert

git revert HEAD         #Create a new commit that reverts the latest commit
git revert abc123       #New commit to revert a specific commit
git revert HEAD~2..HEAD #range revert

Used when the history of a pushed branch needs to be preserved.

3) git restore

git restore file.txt #Cancel working directory changes (no staging impact)
git restore --staged file.txt #Cancel staging (keep working)
git restore --staged --worktree file.txt #cancel both

4) git reflog

git reflog              #HEAD movement history all
git reflog --date=iso   #Includes date
git reset --hard HEAD@{3}   #Restore to previous HEAD position 3 times

5) git stash

git stash               #Temporarily save current changes
git stash push -m "message"
git stash list          #list
git stash pop           #Restore + delete recent stash
git stash apply stash@{1}  #Apply specific stash (do not delete)
git stash drop stash@{0}   #Delete a specific stash
git stash clear         #delete all

See as example

Example 1 — ex01_reset.sh: soft·mixed·hard reset comparison

#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "For practice" && git config user.email "[email protected]"

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

echo "=== Initial state ==="
git log --oneline

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

echo ""
git add . && git commit -q -m "re-commit after soft"

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

Execution result

=== Initial state ===
c3 commit 3 / b2 commit 2 / a1 commit 1

--- soft reset ---
M file.txt ← stay staged
b2 commit 2 / a1 commit 1

--- mixed reset ---
 M file.txt    ← unstaged
b2 commit 2 / a1 commit 1

Key point: --soft only cancels commits, while --hard deletes all changes.


Example 2 — ex02_revert.sh: Safely reverting a pushed commit

#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "For practice" && git config user.email "[email protected]"

echo "normal code" > app.py && git add . && git commit -q -m "feat: good code"
echo "bug code" > app.py && git add . && git commit -q -m "feat: buggy code"
echo "Other features" > other.py && git add . && git commit -q -m "feat: other feature"

echo "=== Log before revert ==="
git log --oneline

BUGGY_HASH=$(git log --oneline | grep "buggy" | awk '{print $1}')
git revert "$BUGGY_HASH" --no-edit

echo ""
echo "=== Log after revert (new revert commit added) ==="
git log --oneline

rm -rf "$REPO"

Execution result

=== Log before revert ===
c3 feat: other feature
b2 feat: buggy code
a1 feat: good code

=== Log after revert ===
d4 Revert "feat: buggy code"
c3 feat: other feature
b2 feat: buggy code
a1 feat: good code

Bottom line: revert does not delete history, but adds the opposite commit.


Example 3 — ex03_restore.sh : Undo changes with restore

#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "For practice" && git config user.email "[email protected]"

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

echo "Edited" > file.txt
git add file.txt

echo "=== status after modification + staging ==="
git status --short

git restore --staged file.txt
echo ""
echo "=== After canceling --staged (unstaged) ==="
git status --short

git restore file.txt
echo ""
echo "=== After canceling the working directory ==="
git status --short
echo "File content: $(cat file.txt)"

rm -rf "$REPO"

Execution result

=== After modification + staging ===
M  file.txt

=== After canceling --staged ===
 M file.txt

=== After canceling the working directory ===
(nothing)
File contents: Original

Key point: restore --staged only cancels staging, and restore only cancels the working directory.


Example 4 — ex04_stash.sh: Temporarily saving work with stash

#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "For practice" && git config user.email "[email protected]"

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

#Urgent bug fix request during work
echo "WIP: New features in development" >> app.py
git stash push -m "WIP: Developing new features"

echo "=== status after stash (clean) ==="
git status --short

echo ""
echo "=== stash list ==="
git stash list

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

#restore original work
git stash pop

echo ""
echo "=== status after stash pop ==="
git status --short
echo "app.py content: $(cat app.py)"

rm -rf "$REPO"

Execution result

=== status after stash ===
(clean)

=== stash list ===
stash@{0}: WIP: Develop new features

=== status after stash pop ===
 M app.py
app.py content: initial code
WIP: New features in development

Key point: With stash, you can temporarily save it, work on another branch, and then come back and restore it.


Seeing from a different perspective

command Everyday metaphor Use after push
reset --hard Permanently delete files ❌ Absolutely not
reset --soft Erase what you wrote on paper with an eraser ❌ Only before sharing
revert Reverse and overwrite edits ✅ Safety
restore Recall last save —
stash Put it in a drawer for a moment —

Common Mistakes

  1. reset --hard on the pushed branch — The team member's history is messed up and a forced push becomes necessary.
  2. Confusion between reset and revert — Always use revert in shared branches.
  3. Stash and Forget — Check periodically with git stash list and delete unnecessary ones.
  4. No recovery after restore — If restore unstaged changes are made, recovery is not possible even with reflog.
  5. reflog period expiration — Reflog entries are deleted after 90 days by default, so they must be recovered quickly.

Cleanup

  • reset --soft: Cancel only commits, --mixed: Cancel staging as well, --hard: Cancel all.
  • The pushed commit is reverted to revert — the history is preserved.
  • Cancel staging with restore --staged and cancel working directory change with restore.
  • You can also recover accidentally deleted commits with reflog.
  • stash temporarily stores changes without committing and restores them later.

Try it yourself

cd 06_Cancel-and_Revert/src
chmod +x *.sh
./ex01_reset.sh
./ex02_revert.sh
./ex03_restore.sh
./ex04_stash.sh

Application:

  1. Make 3 commits with git reset --hard HEAD~3 and then restore them with git reflog.
  2. Try separating the stash into a new branch directly with git stash branch <branch name>.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/ex01_reset.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    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 commits) ==="
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 "(commit again)"
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 "(add + commit again)"
git add . && git commit -q -m "commit 3 (re-added)"

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

rm -rf "$REPO"
src/ex02_revert.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    git config commit.gpgsign false

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

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

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

echo "=== Log before revert ==="
git log --oneline

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

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

echo ""
echo "=== Log after revert (new commit added, history preserved) ==="
git log --oneline

echo ""
echo "=== app.py after revert (restore to before bug) ==="
cat app.py

rm -rf "$REPO"
src/ex03_restore.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    git config commit.gpgsign false

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

#Edit + Staging
echo "What has been modified" > file.txt
git add file.txt

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

#Cancel staging only (keep working directory)
git restore --staged file.txt
echo ""
echo "[2] After restore --staged (unstaged, keep contents)"
git status --short
echo "File content: $(cat file.txt)"

#Also cancel the working directory
git restore file.txt
echo ""
echo "[3] After restore (clean, restore to original)"
git status --short
echo "File content: $(cat file.txt)"

echo ""
echo "=== Example where restore cannot be applied to a new file ==="
echo "new file" > newfile.txt
git restore newfile.txt 2>&1 || echo "→ Untracked files cannot be restored (delete with rm)"

rm -rf "$REPO"
src/ex04_stash.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    git config commit.gpgsign false

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

#New features in development (uncommitted)
echo "WIP: Working on new features" >> app.py
echo ""
echo "[1] Working status"
git status --short

#Request for urgent fix → stash
git stash push -m "WIP: Developing new features"
echo ""
echo "[2] Status (clean) after stash"
git status --short

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

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

echo ""
echo "[4] Log after merging hotfix"
git log --oneline

#restore original work
git stash pop
echo ""
echo "[5] Status after stash pop"
git status --short
echo "app.py contents:"
cat app.py

rm -rf "$REPO"

Assignment

Problem 1

git reset Write a script to directly compare the three modes.

  • File name: hw01.sh
  • Requirements:
  1. Initialize the temporary storage and commit a.txt, b.txt, and c.txt respectively (3 commits in total).
  2. Undo the last commit with git reset --soft HEAD~1 and print git status. (Expectation: c.txt is staged)
  3. Cancel one more step with git reset HEAD~1 and output git status. (Expectation: b.txt and c.txt are unstaged)
  4. Commit again to git add . && git commit -m "re-commit b and c".
  5. Cancel with git reset --hard HEAD~1 and print git status and ls. (Expectation: clean status, no files b.txt c.txt)

Problem 2

Implement scenarios to temporarily save and restore your work with git stash.

  • File name: hw02.sh
  • Requirements:
  1. Initialize temporary storage and commit main.py.
  2. Add “WIP task” to main.py and save it as git stash push -m "WIP".
  3. Create a fix/patch branch, add patch.py and commit.
  4. Return to main and merge fix/patch.
  5. Restore the work to git stash pop and print the contents of main.py.
  6. Confirm that git stash list is empty.

Check the correct answer

After writing it yourself, compare it with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

hw01.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    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 files:"
ls

rm -rf "$REPO"
hw02.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    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 "Change status: $(git status --short | wc -l) after stash (should be 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 "Log after merge:"
git log --oneline

git stash pop

echo ""
echo "After stash pop main.py:"
cat main.py

echo ""
echo "stash list (must be empty):"
git stash list

rm -rf "$REPO"

Learn this lesson with a mentor

Work through the Git & GitHub track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now