Editing Git Commit History Safely
Four commands change what your history looks like, and they are routinely confused because their effects overlap. Choosing wrongly is how people lose work or disrupt colleagues.
| Command | What it does | Rewrites history? |
|---|---|---|
git commit --amend | Replaces the most recent commit | Yes |
git rebase -i | Replaces a range of commits | Yes |
git revert | Adds a new commit undoing an old one | No |
git reset | Moves the branch ref to a different commit | Yes, in effect |
The first, second and fourth produce new commit IDs and require a force push if the branch was shared. The third does not — which is precisely why it is the right answer on a shared branch.
The decision, first
Section titled “The decision, first”Two questions settle almost every case.
1. Has anyone else got these commits?
If no → rewriting is fine. Use amend, rebase -i or reset.
If yes → do not rewrite. Use revert.
2. What exactly do you want to change?
| Goal | Tool |
|---|---|
| Fix the message of the last commit | git commit --amend |
| Add a forgotten file to the last commit | git commit --amend |
| Fix the message of an older commit | git rebase -i with reword |
| Change the content of an older commit | git rebase -i with edit |
| Combine commits | git rebase -i with squash/fixup |
| Remove a commit from a private branch | git rebase -i with drop |
| Undo a commit that is already shared | git revert |
| Undo the last few local commits, keep the changes | git reset --soft |
| Undo the last few local commits, discard everything | git reset --hard ⚠ |
git commit --amend
Section titled “git commit --amend”The smallest rewrite: it replaces the most recent commit.
git commit --amendWhat it doesCreates a new commit from the current index plus whatever the previous commit contained, then moves the branch to it. The previous commit is replaced, not modified.
Why we run itIt is the quickest way to correct the commit you just made — a typo in the message, a file you forgot to stage.
Expected resultA commit summary line. The commit ID will differ from the one you just made.
Common forms:
git commit --amend # edit the messagegit commit --amend -m "Better message" # replace the message inlinegit commit --amend --no-edit # keep the message, take newly staged changesgit commit --amend --author="Name <email>" # correct authorshipThe --no-edit form is the one to remember: stage the forgotten file, amend, done.
git add forgotten-file.pygit commit --amend --no-editgit rebase -i for older commits
Section titled “git rebase -i for older commits”--amend only reaches the most recent commit. For anything further back, interactive rebase:
git rebase -i <commit>^Appending ^ names the parent of the commit you want to change, which is where the range must start.
| Todo verb | Use for |
|---|---|
reword | Change the message only |
edit | Change the content — Git stops so you can amend |
squash / fixup | Combine with the previous commit |
drop | Remove it |
Remember that changing a commit rewrites every commit after it, because each one’s parent changed. Interactive Rebase covers the mechanics.
git revert
Section titled “git revert”Revert does not rewrite anything. It creates a new commit whose changes are the inverse of an old one.
git revert a1b2c3dWhat it doesComputes the reverse of the named commit's changes and commits them as a new commit on top of your branch.
Why we run itIt undoes the effect of a commit while leaving history intact, so it is safe on branches other people have.
Expected resultA new commit named Revert "…". The original commit is still present in the history.
[main 7f8e9d0] Revert "Add experimental caching" 1 file changed, 12 deletions(-)Both commits now exist: the original and its reversal. That is a feature — the history records that something was tried and withdrawn, which is often useful information.
Reverting a merge commit needs -m to say which parent’s line to keep:
git revert -m 1 <merge-commit>Merge Commits covers the consequences, including the trap that re-merging a reverted branch restores nothing.
git reset
Section titled “git reset”Reset moves the current branch to point at a different commit. What happens to your files depends entirely on the mode.
| Mode | Branch ref | Index | Working tree |
|---|---|---|---|
--soft | Moves | Untouched | Untouched |
--mixed (default) | Moves | Reset to target | Untouched |
--hard | Moves | Reset to target | Overwritten |
--soft: undo commits, keep everything staged
Section titled “--soft: undo commits, keep everything staged”git reset --soft HEAD~3The last three commits are no longer on the branch; all their changes are staged, ready to be recommitted differently. This is the cleanest way to collapse several commits into one.
--mixed: undo commits, keep changes unstaged
Section titled “--mixed: undo commits, keep changes unstaged”git reset HEAD~1The commit is undone and its changes are in your working tree, unstaged. This is the standard way to split a commit: undo it, then stage and commit the pieces separately.
--hard: undo commits and discard the changes
Section titled “--hard: undo commits and discard the changes”git reset --hard HEAD~1Amend, rebase, revert or reset?
Section titled “Amend, rebase, revert or reset?”The same goal — “undo my last commit” — has four defensible answers depending on circumstances.
| Situation | Command | Why |
|---|---|---|
| Not pushed, want to fix the message | git commit --amend | Smallest change |
| Not pushed, want to redo it entirely | git reset --soft HEAD~1 | Changes stay staged |
| Not pushed, want it gone completely | git reset --hard HEAD~1 ⚠ | Destructive |
| Pushed, branch is yours alone | git reset --hard HEAD~1 then force-with-lease | Acceptable if nobody has it |
| Pushed, others have it | git revert HEAD | No rewrite, no coordination |
On main | git revert HEAD | Always |
The pattern: rewrite privately, revert publicly.
Splitting a commit
Section titled “Splitting a commit”A commit that does two unrelated things is a common target for editing. The technique combines interactive rebase with a mixed reset.
-
Mark it
editin the todo list:Terminal window git rebase -i <commit>^ -
When Git stops, undo the commit but keep its changes:
Terminal window git reset HEAD^No flag means
--mixed: the branch moves back, the changes become unstaged, nothing is lost. -
Commit the pieces separately.
git add -pis invaluable if both changes live in the same file — it walks through each hunk asking whether to stage it:Terminal window git add -p parser.pygit commit -m "Add parser"git add validate.pygit commit -m "Add validation" -
Continue:
Terminal window git rebase --continue
Changing author or date across many commits
Section titled “Changing author or date across many commits”Occasionally a whole branch was committed with the wrong identity — a misconfigured user.email on a new
machine, say.
For the most recent commit:
git commit --amend --author="Correct Name <correct@example.com>" --no-editFor a range, use the interactive rebase exec mechanism to amend each one:
git rebase -i --exec "git commit --amend --reset-author --no-edit" main--reset-author sets both the author and the author date to the current identity and time. If you want to
keep the original dates, omit it and pass --author= explicitly instead.
Checking what you changed
Section titled “Checking what you changed”After any history edit, two commands confirm you changed the history and not the code.
git diff backup-branch HEADEmpty output means the final tree is identical — the edit was purely structural. Any output means the content changed, which is only correct if you meant it to.
git range-diff main..backup-branch main..HEADgit range-diff compares two series of commits rather than two trees. It attempts to pair up commits
that correspond across the rewrite and reports what happened to each — which is exactly the right view
after a rebase, and far more informative than diffing the endpoints.
1: b2e9ccb = 1: b2e9ccb Add parser2: f6c2619 < -: ------- Add validation-: ------- > 2: a9a992b Add validation| Marker | Meaning |
|---|---|
= | The commit is unchanged |
! | Paired with a commit in the other range, with the differences shown below |
< | Present only in the first range — dropped by the rewrite |
> | Present only in the second range — new in the rewrite |
Pairing is heuristic. Two versions of a commit are matched when they are similar enough; when a commit
changed substantially, range-diff reports it as one dropped and one added rather than as a modification,
as in the output above.
Either way this is the best available review of your own rewrite before you force-push it: it makes dropped commits obvious, which is precisely the mistake that is hardest to notice afterwards.
Removing something sensitive
Section titled “Removing something sensitive”A special case that deserves its own treatment, because the intuitive answers are wrong.
If a credential, key or personal data is committed, reverting is not sufficient. The revert adds a commit removing the file; the original commit still contains it, and anyone with a clone still has it.
The response, in order:
-
Rotate the credential immediately. This is the only step that genuinely fixes the problem. Assume it is compromised from the moment it was pushed.
-
Remove it from the working tree and add an ignore rule so it cannot recur.
-
Rewrite the history only if you also control every clone and mirror.
git filter-repois the maintained tool for this; the oldergit filter-branchis slow and error-prone. -
Force-push and tell everyone to re-clone. Anyone who pulls instead of re-cloning may reintroduce the old objects.
Recovery
Section titled “Recovery”Everything in this lesson is recoverable except uncommitted changes destroyed by --hard.
The reflog
Section titled “The reflog”git reflog04a2fa1 HEAD@{0}: reset: moving to HEAD~1f1b6bc0 HEAD@{1}: commit: Add experimental caching0f16212 HEAD@{2}: commit: Main workEvery entry is a position your branch held. Restore any of them:
git reset --hard HEAD@{1}You can also reflog a specific branch rather than HEAD:
git reflog show featureRecovering a commit with no ref
Section titled “Recovering a commit with no ref”If a commit is not in the reflog either — an orphan from a rewrite done in another clone, for example —
git fsck can find it:
git fsck --lost-founddangling commit a1d07c2f8e9b0c3d4e5f60718293a4b5c6d7e8f9Inspect and rescue:
git show a1d07c2git branch recovered a1d07c2Force pushing
Section titled “Force pushing”Any rewrite of a pushed branch requires overriding the remote’s protection.
git push --force-with-leaseThe lease compares the remote’s current position against your remote-tracking ref, refusing if they differ — so a colleague’s push cannot be silently destroyed.
What rewriting does to everything else
Section titled “What rewriting does to everything else”A history edit is not confined to your repository. Four things react to it.
Open pull requests. The pull request follows the branch, so a force push updates it — the diff and commit list refresh. What does not survive cleanly are review comments anchored to specific lines of specific commits: when those commits cease to exist, comments are typically marked outdated and detached from their context. Reviewers then have to work out whether their point was addressed.
CI. A force push is a new head, so pipelines re-run from scratch. Cached results keyed by commit SHA miss entirely. On a repository with a slow pipeline, rebasing repeatedly during review is expensive in build minutes as well as goodwill.
Anything referencing a commit ID. Tickets, chat messages, changelog entries, deployment records and
git bisect sessions all point at objects that are no longer on the branch. The objects still exist until
garbage collection, so the links do not immediately break — they simply cease to be reachable from any
branch, which is subtly worse because nothing announces it.
Other people’s branches. Anyone who branched from your commits now has a branch whose base is orphaned.
They need git rebase --onto to re-parent it, which is straightforward but requires them to know it
happened.
Common mistakes
Section titled “Common mistakes”Rewriting shared history. The recurring theme. Rewrite privately, revert publicly.
Using --hard when you meant --soft. One discards the work; the other keeps it staged. Read the flag
before pressing Enter.
Reverting a leaked secret and considering it handled. Rotate it.
Amending a pushed commit without warning anyone. It is a rewrite like any other.
Using bare --force. Use --force-with-lease, ideally with --force-if-includes.
Assuming reflog covers everything. It covers commits. Uncommitted changes destroyed by --hard are
gone.
Rewriting during review. Comments are anchored to commits that will cease to exist.
Reaching for filter-repo to tidy history. Rewriting an entire repository’s history for cosmetic
reasons imposes a re-clone on everyone. Reserve it for genuine necessity.
Mental Model
Section titled “Mental Model”There are two ways to change what history says.
Rewriting replaces commits with new ones. The old versions become unreferenced but still exist, which is why recovery works and why everyone else’s copy breaks.
Reverting adds a commit that undoes an earlier one. Nothing is replaced, nobody’s copy breaks, and the record shows both what was done and that it was undone.
Rewriting is for history only you have seen. Reverting is for history you have shared.
What You Learned
Section titled “What You Learned”--amend,rebase -iandresetrewrite history;revertdoes not.--amendreaches only the last commit;rebase -ireaches any commit, rewriting everything after it.reset --softkeeps changes staged,--mixedkeeps them unstaged,--harddestroys them.- Uncommitted changes lost to
--hardare not in the reflog and are unrecoverable. - Reverting is the correct tool on any shared branch, including
main. - A committed secret must be rotated; rewriting history is cleanup, not remediation.
git reflogandgit fsck --lost-foundrecover almost anything, within the reflog’s expiry window.--force-with-leasecan be defeated by fetching first;--force-if-includescloses that gap.
Try It Yourself
Section titled “Try It Yourself”Practise each tool, and one recovery, in a disposable repository.
- Create a repository with three commits.
- Amend: change the last commit’s message with
git commit --amend -m "Reworded". Compare the ID before and after. - Soft reset:
git reset --soft HEAD~2, thengit status. Where are the changes? Recommit them as one commit. - Revert:
git revert HEAD --no-edit. Confirmgit lognow shows both the commit and its reversal. - Reword an old commit:
git rebase -i HEAD~3, mark the oldestreword. Note that every later ID changes too. - Recovery drill. Note the current ID, then run
git reset --hard HEAD~2. Confirm the commits are gone fromgit log. - Run
git reflog, find the pre-reset entry, and restore withgit reset --hard HEAD@{1}. - Finally, run
git fsck --lost-foundand see whether anything is dangling.
Step 7 is the one to internalise. Doing it once deliberately, in a repository that does not matter, makes it a reflex rather than a panic later.
Next Lesson
Section titled “Next Lesson”You now know how to reshape history. The next lesson steps back to the strategic question: rebase or merge, as an integration policy.