Git History Rewriting
Amending, squashing, and rebasing — how to clean up local history, and how to not lose work while doing it. Everything here is safe on commits nobody else has pulled; treat pushed, shared commits as append-only unless you know everyone downstream is on board.
Amending the last commit
git commit --amend # edit the message
git commit --amend --no-edit # stage more changes, keep the message
git commit --amend --author="Name <email>" # fix the author
--amend doesn’t edit the commit in place — it creates a new commit and moves the branch pointer to it. The old commit still exists (briefly, until garbage collected) and is recoverable via git reflog if you amend the wrong thing.
Interactive rebase
git rebase -i HEAD~4 # rewrite the last 4 commits
Opens $EDITOR with one line per commit, oldest first. Change the leading word to control what happens to each:
| Command | Effect |
|---|---|
pick |
keep as-is |
reword (r) |
keep the changes, edit the message |
edit (e) |
stop here so you can amend (change content, split the commit) |
squash (s) |
meld into the commit above, combine both messages |
fixup (f) |
meld into the commit above, discard this commit’s message |
drop (d) |
remove the commit entirely |
Squashing a cleanup trail before merging:
pick 07c5abd Add user authentication
s de9b1eb Fix typo
f 3e7ee36 Remove debug log
Result: one commit, Add user authentication, since fixup throws away its message and squash prompts to combine 07c5abd’s and de9b1eb’s.
--autosquash skips the manual reordering: commit with git commit --fixup <sha> (or --squash <sha>) as you go, then git rebase -i --autosquash <base> moves each fixup/squash commit next to its target and marks it automatically. Good default when cleaning up a branch before opening a PR.
Rebasing onto another branch
git fetch upstream
git checkout feature
git rebase upstream/main
# on conflict: resolve files, git add, git rebase --continue
# or bail out entirely: git rebase --abort
# or skip a commit that's now a no-op: git rebase --skip
git push --force-with-lease origin feature
This replays feature’s commits one at a time on top of upstream/main’s current tip, giving linear history instead of a merge commit — but it changes every replayed commit’s SHA. Never rebase a branch other people have already pulled from without warning them; their local history diverges from yours and merging the two back together is painful.
Force pushing safely
Rewriting history means the remote branch’s old commits are gone from your local view — pushing requires overwriting what’s there, which a normal git push refuses to do.
git push --force origin feature # overwrites remote unconditionally
git push --force-with-lease origin feature # overwrites only if remote matches what you last fetched
--force-with-lease is the one to reach for by default. If a teammate pushed to feature since your last fetch, --force-with-lease fails instead of silently discarding their commits — --force would overwrite them without warning.
Recovering from a bad rebase
git reflog records every position HEAD has pointed to, including commits a rebase or amend just orphaned:
git reflog # find the SHA from before the rebase
git reset --hard HEAD@{5} # or the SHA directly
Entries expire (default 90 days for reachable commits, 30 for unreachable) but for anything recent this is the escape hatch when an interactive rebase goes wrong — nothing is actually lost until it’s garbage collected.
Quick reference: which tool for which job
- Fix the message or add a forgotten file to the last commit →
git commit --amend - Combine a string of “fix typo” commits into the commit they belong to, as you make them →
git commit --fixup <sha>+git rebase -i --autosquash - Reorder, edit, or drop commits further back →
git rebase -i HEAD~N - Bring a feature branch up to date with an upstream branch that’s moved on, without a merge commit →
git rebase upstream/main - Undo something a rebase/amend just did →
git reflog