Rewriting History
Date: 2026-08-17
Changing commits that already exist. Safe and useful on work only you have; destructive on anything shared — and the one case where you must rewrite shared history, a leaked secret, is also the case where rewriting doesn’t actually fix the problem.
Rewriting history replaces existing commits with new ones. Because a commit’s hash covers its contents and its parent, changing anything changes every hash after it — Git Model.
The tools
git commit --amend # replace the last commit
git rebase -i HEAD~5 # edit the last 5
git reset --soft HEAD~1 # undo the commit,
# keep the changes staged
git filter-repo # rewrite the whole historyInteractive rebase is the workhorse:
pick a1b2c3 Add basket validation
squash d4e5f6 fix typo
squash 7g8h9i fix typo again
reword j0k1l2 Add delivery estimate
drop m3n4o5 Debug logging| Command | Effect |
|---|---|
pick | Keep as-is |
reword | Change the message |
edit | Stop to amend the contents |
squash | Combine into the previous, keeping both messages |
fixup | Combine, discarding this message |
drop | Remove entirely |
fixup is the one to use most. Commit work-in-progress freely, then fold the “fix typo” commits away before opening a pull request — Commit Hygiene.
The autosquash workflow
git commit --fixup a1b2c3 # marks it
git rebase -i --autosquash HEAD~5Git positions the fixup commits automatically against the commits they reference. It removes the manual reordering, which is where interactive rebase goes wrong.
The boundary
Rewrite freely: anything you haven’t pushed.
Never rewrite: anything anyone else has pulled.
you force-push a rewritten branch
→ their clone still has the old commits
→ their next pull sees two histories
with the same changes
→ duplicate commits, or a merge that
reconciles nothing
→ someone force-pushes back, and
work is lost
If you must force-push a shared branch, use the safer form:
git push --force-with-lease--force-with-lease refuses if someone else has pushed since you last fetched, which turns silent data loss into an error. There is no reason to use plain --force on a branch anyone else touches.
The secret-leak case
The most common reason to rewrite shared history, and the one where it matters least:
1 ROTATE THE CREDENTIAL ← first, always
2 then rewrite history if you like
Rewriting does not un-leak a secret. Every existing clone, fork, CI cache, and any automated scanner watching public repositories already has it. The rewrite is hygiene; the rotation is the fix — Secrets Management.
Recovering from a bad rewrite
The reflog records every position HEAD has held, including ones no branch points at:
git reflog
# a1b2c3 HEAD@{0}: rebase finished
# 9f8e7d HEAD@{1}: rebase start
# 4c5b6a HEAD@{2}: commit: the work you lost
git reset --hard HEAD@{2}Almost nothing is unrecoverable within the reflog’s retention, which defaults to weeks. The reflog is local, so it can’t recover from someone else’s force-push — but your own mistakes are reversible.
Where it’s genuinely worth doing
- Tidying a branch before review, so the reviewer reads a sequence of clear steps rather than your working process — Code Review
- Splitting a commit that did two unrelated things, which restores bisectability — Git Bisect
- Fixing a message that references the wrong ticket
- Removing a large accidental file before it’s shared, which otherwise stays in history forever
Where it isn’t
- On
main, ever - To hide mistakes. History showing a bug being introduced and fixed is more useful than history pretending it never happened
- To fabricate an idealised sequence that never occurred. There’s a real line between tidying and fiction, and the test is whether the resulting commits each actually work