Rewriting History: Amend, Reset, Revert, Reflog
This chapter covers amending, reverting, resetting, and how to recover from the commands that sounded destructive but weren't.
git commit --amend
Fix the last commit. Either the message, the files, or both.
Change the message only:
git commit --amend -m "the new message"
Add files you forgot:
git add forgotten_file.ts
git commit --amend --no-edit # reuse the existing message
--amend creates a new commit with the same parent as the one it replaces, then moves the branch pointer to it. The old commit is still reachable via the reflog (see below).
Rule: only amend a commit you haven't pushed, or one nobody else has pulled. Amending shared commits breaks collaborators for the same reasons as rebasing them.
git reset: Move the Branch Pointer
git reset moves the current branch to a different commit. It has three modes.
git reset --soft <commit> # move branch, keep index, keep working tree
git reset --mixed <commit> # move branch, reset index, keep working tree (default)
git reset --hard <commit> # move branch, reset index, reset working tree
Pictured:
Before: branch → D You: git reset --hard B
After: branch → B (commits C and D are now unreachable by branch)
--soft: Uncommit But Keep Changes
Undo the last commit, keep everything staged:
git reset --soft HEAD~1
The commit is gone (from the branch). The changes are exactly where they were when you ran git commit. Edit and re-commit.
--mixed: Uncommit and Unstage
Undo the last commit, keep changes in the working tree but unstage them:
git reset HEAD~1 # --mixed is the default
Useful when you want to regroup the staging.
--hard: Obliterate
Undo everything up to and including the given commit:
git reset --hard HEAD~1
The commit is gone, changes in the working tree are gone, everything since HEAD~1 is gone. Be sure. This is the command that deletes uncommitted work. The reflog can still recover commits; it cannot recover uncommitted changes.
Resetting a Single File
A common pattern that looks like a reset is actually git restore:
git restore path/to/file # discard working tree changes
git restore --staged path/to/file # unstage
git restore --source HEAD~3 path/to/file # restore from 3 commits ago
Prefer git restore for file-level operations. It's clearer.
git revert: Undo With a New Commit
git revert creates a new commit that undoes the changes of an earlier commit. History is preserved; you just have one more commit that cancels out the bad one.
git revert <sha> # revert a single commit
git revert HEAD # revert the last commit
git revert a1b2c3d..e4f5g6h # revert a range
Git creates a new commit with the message "Revert ...". Edit as needed.
Revert is the safe answer when the bad commit is already shared. Don't reset --hard and force-push; revert.
Reverting a Merge Commit
Merge commits need the -m flag to say which parent to revert against:
git revert -m 1 <merge-commit-sha>
-m 1 means "keep the first parent's history". Usually what you want.
Reverting a merge can make re-merging the branch awkward. Think before doing it.
git reflog: The Safety Net
The reflog records every change to every ref. Commits you thought were lost are almost always in the reflog for 90 days (default).
git reflog
9b2f11a HEAD@{0}: reset: moving to HEAD~1
3f1a22c HEAD@{1}: commit: add login form
4d2c1b1 HEAD@{2}: commit: fix typo
...
To get back to a commit the reflog remembers:
git reset --hard HEAD@{1}
# or by hash
git reset --hard 3f1a22c
Or make a branch at that point:
git switch -c recovered 3f1a22c
Branch-Specific Reflog
Every branch has its own reflog:
git reflog feature/login
Useful when you need the history of one specific branch pointer, not of HEAD.
Reflog Is Local
The reflog lives in .git/logs/. It's not in any remote. If you clone fresh, there's no reflog to rescue you. Commit important recoveries explicitly into a branch.
cherry-pick: Grab a Commit From Somewhere Else
git cherry-pick <sha> applies the changes of a specific commit onto your current branch, creating a new commit.
git switch main
git cherry-pick a1b2c3d
Useful for backporting a bugfix to a release branch, or grabbing one commit from a branch you don't want to merge wholesale.
Range:
git cherry-pick a1b2c3d..e4f5g6h # exclusive of a1b2c3d
git cherry-pick a1b2c3d^..e4f5g6h # inclusive
Conflicts work the same as in merge or rebase.
git clean: Remove Untracked Files
Not a history rewrite, but adjacent. Removes untracked files from the working tree.
git clean -n # dry run (show what would be deleted)
git clean -f # actually delete
git clean -fd # include untracked directories
git clean -fx # include ignored files (danger)
Always run -n first. git clean -fx will nuke your .env if it's gitignored.
History Rewriting With filter-repo
For large rewrites (removing a file from all history, stripping secrets, changing an email), use git-filter-repo:
# Install separately (not part of git):
# pip install git-filter-repo
git filter-repo --path-glob 'secrets/*' --invert-paths
git filter-repo --email-callback 'return email.replace(b"@old.com", b"@new.com")'
git filter-repo replaces the older git filter-branch, which is slow and easy to misuse.
Two warnings:
- filter-repo rewrites every commit. All hashes change. Everyone else has to re-clone or reset hard.
- If you're removing a secret, the secret is probably still cached on GitHub and any clone. Rotate the secret. filter-repo is damage control, not magic.
Common Pitfalls
git reset --hard on work you hadn't pushed. Unstaged changes are gone for good. Commit early when in doubt.
Force-push after reset on a shared branch. Same as rebasing shared history. Use revert instead.
Forgetting the reflog. "I lost all my work" is almost always wrong. Check git reflog first. You probably have 90 days.
Amending a commit that's been pushed. Forces force-push and breaks collaborators. If the commit is shared, use a follow-up commit instead.
Committing filter-repo'd history without coordinating. The entire team's clones become worthless. Announce before running it.
Next Steps
Continue to 09-internals.md to see how all of this is just files.