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
reset --hardon the pushed branch — The team member's history is messed up and a forced push becomes necessary.- Confusion between
resetandrevert— Always userevertin shared branches. - Stash and Forget — Check periodically with
git stash listand delete unnecessary ones. - No recovery after
restore— Ifrestoreunstaged changes are made, recovery is not possible even with reflog. - 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 --stagedand cancel working directory change withrestore. - You can also recover accidentally deleted commits with
reflog. stashtemporarily 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:
- Make 3 commits with
git reset --hard HEAD~3and then restore them withgit reflog. - Try separating the stash into a new branch directly with
git stash branch <branch name>.