DaleSchool

Undoing Changes

Beginner25min

Learning Objectives

  • Discard file changes with git restore
  • Understand the difference between git reset --soft, --mixed, and --hard
  • Safely revert public commits with git revert
  • Recover lost commits with git reflog

Working Code

Example 1: Discarding file changes with git restore

When you accidentally break a file:

git init
echo "Original content" > file.txt
git add file.txt
git commit -m "feat: add file"

# Accidentally overwrite an important file
echo "Wrong content overwrites everything" > file.txt
cat file.txt

Output:

Wrong content overwrites everything
# Restore to the last committed state
git restore file.txt
cat file.txt

Output:

Original content

git restore discards working directory changes. This cannot be undone.

Example 2: Unstaging

echo "Modified content" > file.txt
git add file.txt
git status

Output:

Changes to be committed:
    modified: file.txt
# Unstage only (keep working directory changes)
git restore --staged file.txt
git status

Output:

Changes not staged for commit:
    modified: file.txt

The file content is preserved; only the staging is undone.

Example 3: Reverting a specific commit with git revert

git init
echo "v1" > version.txt && git add version.txt && git commit -m "feat: v1"
echo "v2" >> version.txt && git add version.txt && git commit -m "feat: v2"
echo "v3" >> version.txt && git add version.txt && git commit -m "feat: v3"

git log --oneline

Output:

c3d4e5f feat: v3
b2c3d4e feat: v2
a1b2c3d feat: v1

Undo the v2 commit's changes (as a new commit):

git revert b2c3d4e

Save and close the editor. Output:

[main d5e6f7g] Revert "feat: v2"
 1 file changed, 1 deletion(-)
cat version.txt

Output:

v1
v3

The v2 changes are undone, but history is preserved.

Try It Yourself

git reset: three ways to undo commits

git init
echo "a" > file.txt && git add file.txt && git commit -m "Commit A"
echo "b" >> file.txt && git add file.txt && git commit -m "Commit B"
echo "c" >> file.txt && git add file.txt && git commit -m "Commit C"

git log --oneline

Output:

c3d4e5f Commit C
b2c3d4e Commit B
a1b2c3d Commit A

--soft: Undo the commit, keep changes staged

git reset --soft HEAD~1
git log --oneline

Output:

b2c3d4e Commit B
a1b2c3d Commit A
git status

Output:

Changes to be committed:
    modified: file.txt    <- Commit C's changes remain staged

Useful when you just want to rewrite the commit message.

--mixed: Undo commit + unstage, keep files (default)

git reset HEAD~1    # --mixed is the default
git log --oneline
git status

Output:

a1b2c3d Commit A

Changes not staged for commit:
    modified: file.txt    <- Changes remain in working directory

Use this when you want to re-stage files differently.

--hard: Undo commit + unstage + discard file changes

git reset --hard HEAD~1
git log --oneline
cat file.txt

Output:

a1b2c3d Commit A

a

Commits B and C are completely gone. Use this with extreme caution.

git reset comparison

| Command | Commit | Staging | Working Directory | | --------- | ------ | ------- | ----------------- | | --soft | Undone | Kept | Kept | | --mixed | Undone | Undone | Kept | | --hard | Undone | Undone | Undone |

git reflog: your safety net

When you lose commits via git reset --hard or other mistakes:

# View the log of all HEAD movements
git reflog

Output:

a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1
c3d4e5f HEAD@{1}: commit: Commit C
b2c3d4e HEAD@{2}: commit: Commit B
a1b2c3d HEAD@{3}: commit: Commit A

Recover a lost commit:

git reset --hard c3d4e5f   # or HEAD@{1}
git log --oneline

Output:

c3d4e5f Commit C  <- Recovered!
b2c3d4e Commit B
a1b2c3d Commit A

reflog is Git's local record of every HEAD movement. Even commits "deleted" by git reset --hard can be recovered.

"Why?" -- When to use each command

| Scenario | Correct Command | | ------------------------------------ | ------------------------------- | | Discard file changes (before commit) | git restore filename | | Unstage a file | git restore --staged filename | | Fix the last commit message | git commit --amend | | Undo unpushed commits | git reset | | Undo pushed commits | git revert | | Recover accidentally lost commits | git reflog |

The key difference: reset vs revert

reset:  A -> B -> C -> (D removed)
            ^
           HEAD

revert: A -> B -> C -> D -> D' (inverse of D)
                             ^
                            HEAD

Always use revert for pushed code. Using reset to delete pushed commits causes history conflicts with teammates.

Common Mistakes

Mistake 1: Using --hard recklessly

# Warning! Cannot be undone easily
git reset --hard HEAD~3   # All changes from 3 commits are gone

# Check what you'll lose first
git diff HEAD~3 HEAD      # See what will be removed
git reset --hard HEAD~3   # Then decide

Mistake 2: Using reset on pushed commits

git push origin main   # Already pushed
git reset --hard HEAD~1   # Delete commit locally
git push origin main   # Error!
# error: failed to push some refs

# Force push (causes problems for teammates)
git push --force   # Never do this on shared branches

Mistake 3: Conflict during revert

git revert b2c3d4e   # Conflict may occur
# CONFLICT: ...

# Resolve the conflict, then
git add filename
git revert --continue

Deep Dive

git restore vs git checkout (legacy approach)

Before Git 2.23, checkout was used to restore files:

# Legacy approach
git checkout -- file.txt          # Restore working directory
git checkout HEAD -- file.txt     # Restore from a specific commit

# Modern approach (clearer intent)
git restore file.txt              # Restore working directory
git restore --staged file.txt     # Unstage
git restore --source=HEAD~2 file.txt  # Restore from 2 commits ago
Restoring a specific file from an older commit
# Restore a file from 5 commits ago
git restore --source=HEAD~5 important-file.txt

# Restore from a specific commit hash
git restore --source=a1b2c3d config.yaml

# The restored file is in your working directory; review and commit
git diff config.yaml
git add config.yaml
git commit -m "revert: restore config.yaml to previous version"
git bisect: finding the bug-introducing commit

When you don't know which commit introduced a bug:

git bisect start
git bisect bad HEAD          # Current state: has the bug
git bisect good a1b2c3d     # This commit was working fine

# Git checks out a middle commit
# Test and report the result
git bisect good   # This commit is fine
git bisect bad    # This commit has the bug

# After a few rounds, Git identifies the culprit
# When done, return to normal
git bisect reset
  1. Modify a file and restore it with git restore.
  2. Stage a file and unstage it with git restore --staged.
  3. Create 3 commits and test git reset --soft HEAD~1, git reset --mixed HEAD~1, and git reset --hard HEAD~1 separately. Compare the differences.
  4. Delete a commit with git reset --hard HEAD~1, then find and recover it with git reflog.
  5. Undo a commit with git revert and check the history with git log --oneline.

Q1. How do you safely undo a commit that has already been pushed and shared with teammates?

  • A) git reset --hard HEAD~1 then git push --force
  • B) git revert commit-hash
  • C) git reset --soft HEAD~1 then git push
  • D) git branch -D and recreate it