Matthew Lang avatar

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 commitgit commit --amend
  • Combine a string of “fix typo” commits into the commit they belong to, as you make themgit commit --fixup <sha> + git rebase -i --autosquash
  • Reorder, edit, or drop commits further backgit rebase -i HEAD~N
  • Bring a feature branch up to date with an upstream branch that’s moved on, without a merge commitgit rebase upstream/main
  • Undo something a rebase/amend just didgit reflog