-
Apply the golden rule first — is this branch shared? Rewriting changes every downstream SHA; anyone who pulled the old history gets a divergent tree and can re-push the secret you deleted.
| Branch state | Rewrite? | How to push |
|---|
| Local-only, never pushed | Yes, freely | normal git push (first push) |
| Pushed, only you pull it (personal feature branch) | Yes | git push --force-with-lease |
Shared / main / release / others have pulled | No — coordinate first | announce → everyone stops → rewrite → --force-with-lease → everyone re-clones or git reset --hard origin/<branch> |
Default: rewrite only local/unshared branches. For main, the answer is almost always "don't" — the only exception is purging a secret, and then only with team sign-off.
-
Record the safety net before touching anything. Copy the current tip so you can undo: git rev-parse HEAD, and confirm git status is clean — stash or commit dirty work first; rebase refuses to start otherwise. The reflog (git reflog) keeps the old tip ~90 days regardless, but a written-down SHA is faster.
-
Interactive rebase for squash/reword/reorder/drop/edit. Rebase the commits since the base, not your whole history:
git rebase -i origin/main
In the todo editor, set the verb on each line (top = oldest), and reorder by moving whole lines:
| Verb | Effect |
|---|
pick | keep as-is |
reword | keep changes, edit the message |
edit | stop here to amend content or split (step 4) |
squash | fold into the commit above, combine both messages |
fixup | fold into the commit above, discard this message (use for "oops typo" commits) |
drop | delete the commit entirely (or just delete the line) |
Save and close. Resolve any conflicts (→ resolve-merge-rebase-conflict), then git rebase --continue. Abort cleanly at any point with git rebase --abort — it restores the pre-rebase tip exactly.
-
Split one commit into several. Mark it edit in the rebase todo; when rebase stops on it:
git reset HEAD^
git add -p
git commit -m "first logical change"
git add -p && git commit -m "second logical change"
git rebase --continue
git reset HEAD^ (mixed) keeps your work in the tree; never --hard here or you lose it.
-
Amend only the last commit (no rebase needed): git commit --amend (edit message + fold staged changes), or git commit --amend --no-edit to silently add forgotten files. If already pushed to your own branch, follow with git push --force-with-lease.
-
Purge a file/secret across ALL history — git filter-repo (preferred, BFG fallback). git filter-branch is deprecated and slow; do not use it.
pip install git-filter-repo
git filter-repo --path config/secrets.yml --invert-paths
printf 'AKIAIOSFODNN7EXAMPLE==>REDACTED\n' > replacements.txt
git filter-repo --replace-text replacements.txt
BFG alternative for big blobs: bfg --delete-files '*.zip' or bfg --replace-text replacements.txt, then git reflog expire --expire=now --all && git gc --prune=now --aggressive. filter-repo runs that cleanup for you and removes the origin remote on purpose — re-add it before pushing.
-
ROTATE the leaked secret — rewriting history does NOT un-leak it. Anyone who cloned, any fork, any CI cache, and GitHub's own unreachable-commit cache still hold it. The history scrub is step 1 of 2; the real fix is: revoke + reissue the key/token/password at the provider (then → secrets-management). Treat the credential as compromised the moment it was pushed.
-
Force-push with --force-with-lease, never bare --force.
git push --force-with-lease origin <branch>
--force-with-lease refuses the push if the remote moved since your last fetch — it catches the case where a teammate pushed in the meantime, which bare --force would silently obliterate. For a full-history purge you must push every ref: git push --force-with-lease --all && git push --force-with-lease --tags.
Done = intended commits are exactly reshaped with zero unintended drops (verified against the pre-rewrite SHA/reflog), the secret is absent from every ref and every history blob, the leaked credential is rotated and dead at the provider, and the push used --force-with-lease on a branch that was either unshared or coordinated.