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.