Tags: web-dev concept

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 history

Interactive 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
CommandEffect
pickKeep as-is
rewordChange the message
editStop to amend the contents
squashCombine into the previous, keeping both messages
fixupCombine, discarding this message
dropRemove 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~5

Git 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