Learning Objectives
By the end of this module, you will be able to:
- Explain the three modes of
git reset(--soft,--mixed,--hard) and how each affects HEAD, the index, and the working directory - Use
git resetto unstage files, undo local commits, and collapse commit history - Distinguish between
git reset(rewrites history) andgit revert(creates new undo commits) - Revert regular commits and merge commits on shared branches
- Recover "lost" commits after a reset using
git reflog
1. Understanding the Three Areas
Before diving into reset, you need a crystal-clear picture of Git's three areas. Every reset mode affects a different combination of them.
┌──────────────────┐ git add ┌──────────────────┐ git commit ┌──────────────────┐
│ Working Directory │ ──────────→ │ Staging Area │ ──────────→ │ Repository │
│ (your files) │ │ (index) │ │ (HEAD) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
- Working Directory — The actual files on disk that you edit
- Staging Area (Index) — A snapshot of what will go into the next commit; updated by
git add - Repository (HEAD) — The commit history; updated by
git commit
When all three match, git status says "nothing to commit, working tree clean."
2. git reset — The Three Modes
git reset moves the current branch pointer (and HEAD) to a different commit. The three modes control what else gets modified along the way.
The Mental Model
Think of git reset <target> as moving backwards through three checkpoints. Each mode stops at a different point:
git reset --soft <target>
✓ Moves HEAD/branch pointer ← stops here
✗ Index unchanged
✗ Working directory unchanged
git reset --mixed <target> (this is the default)
✓ Moves HEAD/branch pointer
✓ Resets index to match target ← stops here
✗ Working directory unchanged
git reset --hard <target>
✓ Moves HEAD/branch pointer
✓ Resets index to match target
✓ Resets working directory to match target ← DESTRUCTIVE
--soft: Undo the Commit, Keep Everything Staged
git reset --soft HEAD~1This moves HEAD back one commit. The changes from the undone commit remain staged (in the index). Your working directory is untouched.
Before:
A ← B ← C ← HEAD (main)
Index: matches C
Working dir: matches C
After git reset --soft HEAD~1:
A ← B ← HEAD (main)
C (orphaned, but changes still in index)
Index: still matches C (changes are staged)
Working dir: still matches C
Use cases:
- "I want to redo my last commit with a different message"
- "I want to combine the last few commits into one" (squash)
- "I committed too early — I want to add more changes to this commit"
# Redo the last commit with a different message:
git reset --soft HEAD~1
git commit -m "Better message"
# Squash the last 3 commits into one:
git reset --soft HEAD~3
git commit -m "Combined feature"--mixed (Default): Undo the Commit, Unstage the Changes
git reset HEAD~1 # --mixed is the default
git reset --mixed HEAD~1 # Same thing, explicitThis moves HEAD back one commit and resets the index. The changes become unstaged but remain in your working directory.
Before:
A ← B ← C ← HEAD (main)
Index: matches C
Working dir: matches C
After git reset HEAD~1:
A ← B ← HEAD (main)
Index: matches B (reset)
Working dir: still matches C (changes are unstaged)
Use cases:
- "I want to re-stage my changes differently" (split into multiple commits)
- "I want to unstage files I accidentally added"
- Collapsing WIP commits: reset to main, then selectively stage and commit
# Unstage a file you accidentally added:
git reset HEAD path/to/file
# Collapse all commits on a branch into unstaged changes:
git reset main
# Now selectively stage and commit in logical groups--hard: Undo Everything (DESTRUCTIVE)
git reset --hard HEAD~1This moves HEAD back, resets the index, and resets the working directory. All changes from the undone commit are gone. Any uncommitted modifications are also gone.
Before:
A ← B ← C ← HEAD (main)
Index: matches C
Working dir: matches C (+ uncommitted edits)
After git reset --hard HEAD~1:
A ← B ← HEAD (main)
Index: matches B
Working dir: matches B (everything from C and your edits: GONE)
Use cases:
- "I want to completely discard my recent work"
- "I want to match the remote exactly":
git reset --hard origin/main - Restoring to a known state during experimentation
Warning:
--hardis the only reset mode that can cause data loss. Committed changes can be recovered viagit reflog, but uncommitted changes (unstaged edits, untracked files) are gone forever.
Summary Table
| Mode | HEAD | Index | Working Dir | Data Loss? |
|---|---|---|---|---|
--soft | Moves | Unchanged | Unchanged | No |
--mixed (default) | Moves | Reset | Unchanged | No |
--hard | Moves | Reset | Reset | Yes (uncommitted changes lost) |
3. Common git reset Patterns
Unstaging Files
# Unstage a specific file (keep changes in working directory):
git reset HEAD file.txt
# Modern alternative (Git 2.23+):
git restore --staged file.txtThis is --mixed (the default) applied to a single file. HEAD doesn't move — only the index is updated for that file.
Undoing the Last Commit
# Keep changes staged (ready to re-commit):
git reset --soft HEAD~1
# Keep changes unstaged (ready to re-stage differently):
git reset HEAD~1
# Discard changes completely:
git reset --hard HEAD~1Squashing Commits with Reset
Instead of interactive rebase, you can use reset to squash:
# You have: A ← B ← C ← D ← E (5 commits on feature branch)
# You want: A ← F (1 clean commit)
git reset --soft HEAD~4 # Undo last 4 commits, keep changes staged
git commit -m "Add complete feature"
# Or, for more control over staging:
git reset HEAD~4 # Undo last 4 commits, unstage changes
git add model.py
git commit -m "Add data model"
git add controller.py
git commit -m "Add controller"Resetting to Match the Remote
# Discard all local changes and match origin/main exactly:
git fetch origin
git reset --hard origin/mainThis is the "nuclear option." It's what your colleague does when you force-push a rebased branch they had checked out.
Using git reset Without a Target
git reset # Unstage everything (same as git reset HEAD)
git reset --hard # Discard all uncommitted changes (same as git reset --hard HEAD)When no target is specified, reset defaults to HEAD — the current commit. This doesn't move the branch pointer but still resets the index (and working directory for --hard).
4. git commit --amend
Amending is the most common "undo" operation. It replaces the last commit with a new one that includes your additional changes.
# Fix a typo in the last commit message:
git commit --amend -m "Corrected message"
# Add a forgotten file to the last commit:
git add forgotten-file.txt
git commit --amend --no-edit # Keep the same messageWhat Amend Does Behind the Scenes
git commit --amend is equivalent to:
git reset --soft HEAD~1 # Undo last commit, keep changes staged
git commit -c ORIG_HEAD # Recommit with the original messageThe original commit becomes orphaned (new SHA, because the content changed). If you had already pushed, you'll need git push --force-with-lease.
Before amend:
A ← B ← C ← HEAD
After amend (added a file):
A ← B ← C' ← HEAD (new SHA, includes the forgotten file)
↖
C (orphaned, recoverable via reflog)
5. git revert — Safe Undo for Shared History
The Problem with Reset on Shared Branches
git reset rewrites history — it removes commits. If you've already pushed those commits and others have pulled them, resetting causes the same chaos as rebasing shared branches (duplicate commits, broken trust).
The Solution: Revert
git revert creates a new commit that undoes the changes from a previous commit. History is preserved — nothing is rewritten.
git revert <commit-sha>Before:
A ← B ← C ← D ← main
git revert C:
A ← B ← C ← D ← R ← main
↑
"Revert C"
(R undoes the changes introduced by C)
Commit R has the exact opposite diff of commit C. If C added a line, R removes it. If C deleted a file, R recreates it. The important thing: commits B, C, and D remain in history. Nothing is rewritten.
Basic Usage
# Revert a specific commit:
git revert abc1234
# Revert the most recent commit:
git revert HEAD
# Revert without immediately committing (stage the undo):
git revert --no-commit abc1234
# Useful for reverting multiple commits into a single revert commitReverting a Range of Commits
# Revert commits C, D, and E (newest to oldest order):
git revert HEAD~2..HEAD
# Or revert multiple into one commit:
git revert --no-commit HEAD~2..HEAD
git commit -m "Revert features X, Y, and Z"When reverting a range, Git processes them in reverse chronological order (newest first), which minimizes conflicts.
6. Reverting Merge Commits
Merge commits are special — they have two parents. When you revert a merge, Git needs to know which parent's changes to keep. You specify this with the -m flag.
D ← E
/ \
A ← B ← C ←──── M ← main (M is the merge commit)
↑
parent 1 = C (the mainline)
parent 2 = E (the merged branch)
# Revert the merge, keeping mainline (parent 1):
git revert -m 1 <merge-sha>-m 1 means "undo everything that came from the second parent (the merged branch), keeping the first parent's state." This is almost always what you want — it undoes the feature that was merged in.
After git revert -m 1 M:
A ← B ← C ←──── M ← R ← main
↖ ↗
D ← E
R undoes the changes from D and E
Warning: After reverting a merge, you cannot simply re-merge the same branch later. Git thinks those changes were already applied and reverted intentionally. To re-introduce the feature, you must revert the revert:
git revert <revert-sha>.
7. ORIG_HEAD — Your Safety Net
Every time you run a destructive operation (reset, merge, rebase), Git saves the previous HEAD position in a special reference called ORIG_HEAD.
# Oops, that reset was wrong:
git reset --hard ORIG_HEAD
# Check where ORIG_HEAD points:
git rev-parse ORIG_HEADORIG_HEAD only stores the last one — it's overwritten by every operation that sets it. For deeper recovery, use git reflog.
8. Recovery with git reflog
git reflog records every time HEAD moves. It's your safety net for recovering from mistakes.
git reflogabc1234 HEAD@{0}: reset: moving to HEAD~3
def5678 HEAD@{1}: commit: Add tests
ghi9012 HEAD@{2}: commit: Add controller
jkl3456 HEAD@{3}: commit: Add model
mno7890 HEAD@{4}: commit: Initial commit
To recover:
# Go back to before the reset:
git reset --hard def5678
# Or use the reflog syntax:
git reset --hard HEAD@{1}Reflog entries are kept for 90 days by default. After that, they're pruned by garbage collection. Don't wait too long to recover.
The Recovery Flowchart
Lost commits after reset?
├─ Was it --soft or --mixed?
│ └─ Changes are still in working dir/index — just recommit
├─ Was it --hard?
│ ├─ Was the change committed before the reset?
│ │ └─ git reflog → find SHA → git reset --hard <sha>
│ └─ Was the change uncommitted?
│ └─ GONE. No recovery possible.
└─ Not sure?
└─ git reflog → examine the entries → decide
9. Reset vs Revert — When to Use Which
| Scenario | Use | Why |
|---|---|---|
| Undo local commits not yet pushed | git reset | Safe — no one else has the commits |
| Undo a pushed commit on a shared branch | git revert | Preserves history, no force push needed |
| Unstage accidentally staged files | git reset HEAD <file> | Only affects the index |
| Completely discard local changes | git reset --hard | Nuclear option — clears everything |
| Undo a merge that broke production | git revert -m 1 <merge> | Safe for shared branches |
| Squash local WIP commits | git reset --soft HEAD~N | Keeps changes staged for clean recommit |
| Match remote exactly after mishap | git reset --hard origin/main | Colleague's recovery after your force push |
The rule of thumb: If the commits are only local (never pushed), use reset. If others have the commits, use revert.
Command Reference
| Command | Description |
|---|---|
git reset --soft <target> | Move HEAD; keep index and working dir unchanged |
git reset <target> | Move HEAD; reset index; keep working dir (--mixed default) |
git reset --hard <target> | Move HEAD; reset index and working dir (destructive) |
git reset HEAD <file> | Unstage a file (index only, HEAD doesn't move) |
git reset | Unstage everything (same as git reset HEAD) |
git commit --amend | Replace last commit (opens editor for message) |
git commit --amend --no-edit | Replace last commit, keep same message |
git revert <sha> | Create a new commit that undoes <sha> |
git revert --no-commit <sha> | Apply the undo without auto-committing |
git revert -m 1 <merge-sha> | Revert a merge commit (keep parent 1) |
git reflog | Show where HEAD has been |
git reset --hard ORIG_HEAD | Undo the last reset/merge/rebase |
Hands-On Lab: Reset, Revert, and Recover
Setup
mkdir reset-revert-lab && cd reset-revert-lab
git init
echo "line 1: foundation" > app.txt
git add app.txt
git commit -m "Initial commit"
echo "line 2: feature A" >> app.txt
git add app.txt
git commit -m "Add feature A"
echo "line 3: feature B" >> app.txt
git add app.txt
git commit -m "Add feature B"
echo "line 4: feature C" >> app.txt
git add app.txt
git commit -m "Add feature C"git log --onelineCheckpoint: You should see 4 commits. Note the SHAs.
Part A: --soft Reset
Step 1 — Undo the last commit, keeping changes staged
git reset --soft HEAD~1
git statusCheckpoint:
git statusshows "line 4: feature C" as a staged change. The commit is gone fromgit log, but the change is ready to recommit.
Step 2 — Recommit with a different message
git commit -m "Add feature C (improved message)"
git log --onelinePart B: --mixed Reset (Default)
Step 3 — Undo the last 2 commits, unstaging changes
git reset HEAD~2
git statusCheckpoint:
git statusshowsapp.txtas modified (unstaged). The file contains lines 1-4, but only the first 2 commits remain ingit log.
Step 4 — Restage and recommit as a single commit
git add app.txt
git commit -m "Add features B and C together"
git log --onelineCheckpoint: 3 commits. The last one contains both feature B and C changes.
Part C: --hard Reset
Step 5 — Add an uncommitted change, then hard reset
echo "line 5: experimental" >> app.txt
git statusVerify the file has 5 lines.
git reset --hard HEAD~1
cat app.txtCheckpoint: The file has only 2 lines (back to "Add feature A"). The uncommitted "experimental" line AND the committed B+C changes are gone.
Step 6 — Recover the lost commit using reflog
git reflogFind the SHA for "Add features B and C together."
git reset --hard <sha>
cat app.txtCheckpoint: The file is back to 4 lines. Reflog saved you.
Part D: git revert
Step 7 — Revert a specific commit
git log --onelineFind the SHA for "Add feature A" (the second commit). Revert it:
git revert <sha-of-feature-A>Git opens your editor for the revert message. Accept the default.
cat app.txt
git log --onelineCheckpoint: The file no longer contains "line 2: feature A" but still has lines 3 and 4. A new "Revert" commit appears at the top of the log. The original "Add feature A" commit is still in history.
Part E: Reverting a Merge Commit
Step 8 — Create a merge to revert
# Create a branch with changes
git switch -c feature-x
echo "line 5: feature X" >> app.txt
git add app.txt
git commit -m "Add feature X"
# Merge it into main
git switch main
git merge --no-ff feature-x -m "Merge feature-x"
git log --oneline --graphCheckpoint: You see the merge commit with two parents in the graph.
Step 9 — Revert the merge
git revert -m 1 HEADcat app.txt
git log --oneline --graphCheckpoint: "line 5: feature X" is gone. A "Revert" commit sits on top. The merge commit is still in history. The graph shows the full story.
Part F: Unstaging Files
Step 10 — Practice unstaging
echo "secret=abc123" > .env
echo "config=true" > config.txt
git add .env config.txt
git statusBoth files are staged. Unstage the sensitive one:
git reset HEAD .env
git statusCheckpoint:
config.txtis still staged,.envis unstaged (shown in red).
# Clean up - don't commit .env
rm .env
git commit -m "Add config"Cleanup
cd ..
rm -rf reset-revert-labChallenge
Create a repository with 5 commits. Push it to a remote. Then:
- Use
git reset --soft HEAD~2locally, recommit, and force push - Practice recovering the original state from reflog
- Use
git revertto undo a commit in the middle of the history without rewriting any history - Create and merge a feature branch, then revert the merge with
-m 1
Common Pitfalls & Troubleshooting
| Pitfall | Symptom | Fix |
|---|---|---|
Using --hard when you meant --mixed | Uncommitted changes lost | git reflog for committed changes; uncommitted changes are unrecoverable |
| Resetting a shared branch | Colleagues get duplicate commits after pull | Use git revert for shared branches instead |
Forgetting --soft preserves staging | Expected unstaged changes, got staged | Use git reset HEAD (mixed) to unstage |
Reverting a merge without -m | Git error: "commit is a merge but no -m option" | Add -m 1 to specify the mainline parent |
| Re-merging a reverted branch | Git says "Already up to date" | Revert the revert first: git revert <revert-sha> |
Using ORIG_HEAD after multiple operations | Points to wrong commit | Use git reflog for precise navigation |
Confusion between git reset HEAD <file> and git reset HEAD | One unstages a file, the other unstages everything | With a file path: only that file. Without: all staged changes |
| Reset to wrong commit | Branch history looks wrong | git reflog → find correct SHA → git reset --hard <sha> |
Pro Tips
-
Set up a
git undoalias for the most common reset:git config --global alias.undo "reset --soft HEAD~1"Now
git undoremoves the last commit but keeps all changes staged. -
Use
ORIG_HEADimmediately after a mistake:git reset --hard ORIG_HEAD # Undo the last reset/merge/rebase -
Prefer
git restore --stagedovergit reset HEADfor unstaging (Git 2.23+). It's clearer in intent and doesn't have the footgun of accidentally using--hard. -
git revert --no-commitfor batch reverts — Revert multiple commits into a single revert commit:git revert --no-commit HEAD~3..HEAD git commit -m "Revert last 3 commits" -
The
git resetandgit checkoutdistinction:git resetmoves the branch pointer (and HEAD follows)git checkoutmoves HEAD only (branch stays, you get detached HEAD) Both can move backwards, but they move different things.
-
Amend often, push once —
git commit --amend --no-editis safe to use freely on local commits. Only push when you're satisfied with the commit.
Quiz / Self-Assessment
Q1: Explain the difference between git reset --soft HEAD~1, git reset HEAD~1, and git reset --hard HEAD~1.
Answer
All three move HEAD and the branch pointer back one commit. The difference is what else they reset:
--soft: Only moves HEAD. Index and working directory are unchanged. Changes from the undone commit remain staged.--mixed(default, no flag): Moves HEAD and resets the index. Changes from the undone commit are unstaged but remain in the working directory.--hard: Moves HEAD, resets index, and resets working directory. Changes from the undone commit are gone. Any uncommitted changes are also lost.
Q2: You accidentally committed a file with secrets (.env). It's only local (not pushed). What's the fastest way to undo this?
Answer
git reset HEAD~1 # Undo the commit, keep changes unstaged
# Remove .env from staging and add to .gitignore
echo ".env" >> .gitignore
git add .gitignore
git add <other-files> # Re-add everything except .env
git commit -m "Add feature (without secrets)"Or if .env was the only change: git reset --hard HEAD~1 and add .env to .gitignore.
Q3: When should you use git revert instead of git reset?
Answer
Use git revert when the commits have been pushed to a shared branch that others have pulled from. git revert creates a new commit that undoes the changes, preserving history. git reset rewrites history, which causes problems for anyone who already has the old commits. Rule: local-only → reset; shared/pushed → revert.
Q4: Why does reverting a merge commit require the -m flag?
Answer
A merge commit has two parents. When you revert a merge, Git needs to know which parent represents the "mainline" — the branch you want to keep. -m 1 means "undo everything from the second parent (the merged branch)." -m 2 would undo the mainline changes instead. Without -m, Git doesn't know which side to undo and refuses with an error.
Q5: After git reset --hard HEAD~3, can you recover the lost commits?
Answer
Yes, if the changes were committed before the reset. Use git reflog to find the SHA of the commit you want to recover, then git reset --hard <sha>. Reflog entries are kept for 90 days by default. However, any uncommitted changes that were in the working directory at the time of the hard reset are lost permanently.
Q6: What does git reset HEAD file.txt do?
Answer
It unstages file.txt — removes it from the index (staging area) without modifying the working directory or moving HEAD. The file's changes remain in your working directory as unstaged modifications. This is --mixed applied to a single file path.
Q7: What is ORIG_HEAD and when is it useful?
Answer
ORIG_HEAD is a special reference that Git sets before operations that move HEAD drastically (reset, merge, rebase). It stores where HEAD was before the operation. It's useful for quick undo: git reset --hard ORIG_HEAD reverses the last reset/merge/rebase. It only stores one value — it's overwritten by each new operation.
Q8: You reverted a merge commit. Later, you want to re-introduce the same feature. What happens if you try to merge the same branch again?
Answer
Git says "Already up to date" and does nothing. From Git's perspective, those commits were already merged (and then reverted). The solution is to revert the revert: git revert <revert-commit-sha>. This re-introduces the original changes, and then you can merge new commits from the feature branch.
Q9: What is the equivalent of git commit --amend using git reset?
Answer
git reset --soft HEAD~1 # Undo commit, keep changes staged
git add <any-new-files> # Stage additional changes
git commit -m "new message" # Create a new commitThis is exactly what --amend does behind the scenes, except --amend also supports -c ORIG_HEAD to pre-fill the original message.
Q10: True or false: git reset --hard can lose uncommitted changes that are not recoverable by any Git command.
Answer
True. git reset --hard resets the working directory to match the target commit. Any changes that were not committed (and not stashed) are lost permanently. git reflog only tracks committed states. If you modified a file but never ran git add or git commit, those modifications cannot be recovered after a hard reset.