Git Amend on Main — 6 Engineers Got Divergent Histories
40 engineers got divergent histories after git amend on main.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- Creates a brand new commit object with a new SHA hash
- Old commit is orphaned, not deleted — recoverable via reflog for 90 days
- Every commit after the amended one would lose its parent — so amend only works on HEAD
--no-edit— keep existing message (the daily workhorse flag)-m 'new message'— replace message inline without opening editor--author='Name— fix wrong Git identity on the commit' --force-with-lease— the amended commit to remote
git commit --amend is a command that modifies the most recent commit in your current branch's history. It doesn't just edit the commit message — it creates an entirely new commit object that replaces the old one, with a different SHA hash. This is why amending is a form of history rewriting: any branch or tag pointing to the original commit becomes stale, and any other developer who pulled that original commit now has a divergent history.
Under the hood, Git stages the current index (or the specified files), combines it with the previous commit's tree, and produces a new commit with the same parent(s). The old commit becomes orphaned and is eventually garbage-collected.
Amending is safe only when the commit hasn't been pushed, or when you are the sole developer on the branch. Once shared, amending forces everyone else to reconcile the divergence — often via git pull --rebase or a forced push. The --force-with-lease flag is a critical safety net: it checks that your remote tracking branch hasn't moved since you last fetched, preventing you from overwriting someone else's work.
Without it, git push --force is a sledgehammer that can destroy colleagues' commits.
For older commits, you need interactive rebase (git rebase -i), which replays commits from a chosen base. Changing a commit in the middle of history rewrites all subsequent commits as well, since each depends on its parent's SHA. This is exponentially more dangerous on shared branches.
The decision tree is simple: if the commit is local and unpushed, amend freely. If it's pushed and others depend on it, don't amend — create a fixup commit instead. Tools like GitHub's "squash and merge" or git revert are safer alternatives for shared history.
Imagine you sealed an envelope with a letter inside, stamped it, and dropped it in the mailbox. Then you realized you forgot to include a second page. git commit --amend is like reaching into the mailbox, pulling out the envelope, adding the missing page, resealing it, and putting it back — with a new seal (new commit hash) that replaces the old one. The old envelope is gone. Nobody who hasn't already picked up the old envelope will ever know it existed.
The critical detail: the new envelope has a different tracking number (SHA hash). If someone already has the old tracking number recorded somewhere — like a teammate who pulled your branch — their records won't match the new envelope. That's why amending pushed commits causes problems: you changed the tracking number after other people wrote it down.
git commit --amend creates a new commit replacing the last one. It fixes typos in messages, adds forgotten files, or corrects the commit author — all without leaving broken commits in the history.
The core trade-off: amend rewrites history. On a local branch, this is invisible and safe. On a shared branch, every teammate's local history diverges from the remote. One ONLY safe way to push an force-push on main in a 40-engineer monorepo caused 6 divergent histories and 2 hours of recovery work.
Common misconceptions: that amend modifies the existing commit (it creates a new one), that --force is fine for personal branches (--force-with-lease is always safer), and that ORIG_HEAD persists indefinitely (it's overwritten by the next dangerous Git operation).
What Is Git Amend — The Fundamentals
git commit --amend replaces the most recent commit with a new one. It's the quickest way to fix a mistake in your last commit without creating a fixup commit.
What amend actually does: 1. Takes the current staging area (or the previous commit's content if nothing is staged). 2. Creates a NEW commit object with a new SHA-1 hash. 3. Replaces the previous commit pointer with the new one. 4. The old commit becomes a dangling object (recoverable via reflog).
Common uses: - git commit --amend --no-edit — add staged files to the last commit without changing the message - git commit --amend -m 'New message' — fix the commit message - git commit --amend --author='Name <email>' — fix the author
The critical rule: Amend creates a NEW SHA. If the commit was already pushed to a shared branch, force-pushing the amend will break every teammate's local history. The one exception is a personal feature branch that nobody else has pulled.
Safer alternatives for pushed commits: - Fix message: git commit --fixup <sha> then git rebase -i --autosquash <base> - Fix files: just create a new commit with the fix - Use git revert for structural changes
# Create a commit with a typo echo 'Important file' > important.txt git add . && git commit -m 'Inporatnt file' # typo! # Fix the message git commit --amend -m 'Important file' # Add a forgotten file to the same commit echo 'Config data' > config.txt git add . && git commit --amend --no-edit # add to previous, keep message # Fix the author git commit --amend --author='Correct Name <correct@email.com>' # See that the SHA changed git log --oneline # Compare with original (check reflog) git reflog | head -3
git log --oneline origin/<branch> to see if your commit is already on the remote before amending.git commit --fixup followed by git rebase -i --autosquash (which a maintainer can do on merge). The --fixup/--autosquash pattern is the structured way to fix commits without force-pushing.--fixup + --autosquash as the safe alternative for pushed commits.Why `git amend` Rewrites History — Not Just Your Last Commit
git commit --amend is a convenience command that replaces the tip of the current branch with a new commit. It does not “edit” the existing commit; it creates a brand new commit object that shares the same parent as the original, then points the branch reference at the new object. The old commit becomes unreachable from the branch — but remains in the object store until garbage collection. This is the core mechanic: amend is a rebase of one commit, not a mutation.
When you amend a commit that has already been pushed to a shared remote, you create a divergent history. Any collaborator who pulled the original commit now has a commit that no longer exists on the remote. Their next git pull will attempt to merge two unrelated histories, producing a merge commit or a conflict. The symptom is not immediate — it surfaces when the next person pushes, often with a cryptic “rejected” or “non-fast-forward” error. The fix requires force-push, which then forces every other team member to reset their local branch.
Use amend only on commits that exist exclusively in your local repository. In practice, that means before you push, or on a private feature branch where you are the sole contributor. On shared branches like main, develop, or any long-lived integration branch, amend is forbidden by convention. The cost of a single amend on main is a coordination tax on every engineer who has to reconcile their local history — often 10–15 minutes of context switching per person, per incident.
How Amend Actually Works Under the Hood
Before using amend, understand what it does to your repository. git commit --amend does NOT modify the existing commit. It creates an entirely new commit object with a new SHA hash, containing your updated message and/or content. The old commit is then orphaned — no branch points to it anymore. Git's garbage collector eventually deletes orphaned commits (default: 90 days), but until then, the old commit still exists in the repository's object database.
This is why amending a pushed commit requires a force push: the remote branch still points to the old commit hash. Your local branch now points to a different commit hash. Normal git push is rejected because the remote's history has a commit your local history doesn't have. You need --force-with-lease to tell the remote 'replace your pointer with mine, but only if nobody else has pushed since I last fetched.'
The parent pointer also changes. The new commit's parent is whatever was before the old commit — not the old commit itself. This means every commit that came after the amended commit (if there were any) would lose its parent and become orphaned. That's why you can only amend the most recent commit directly — amending an older commit would orphan every commit after it.
To amend an older commit, you need interactive rebase, which replays commits and stops at the one you want to modify. That's covered in a later section.
# io.thecodeforge — Git Amend Internals # ───────────────────────────────────────────────────────────── # SCENARIO: Inspecting what amend does to commit objects. # ───────────────────────────────────────────────────────────── # Step 1: Make a commit and record its SHA git add src/payments/RetryConfig.java git commit -m "feat: add retry config" BEFORE_AMEND=$(git rev-parse HEAD) echo "Before amend: $BEFORE_AMEND" # Example output: 9f2c4a1b3d5e7f8a9b0c1d2e3f4a5b6c7d8e9f0a # Step 2: Amend the commit message git commit --amend -m "feat(payment): add PaymentRetryService with retry config" AFTER_AMEND=$(git rev-parse HEAD) echo "After amend: $AFTER_AMEND" # Example output: 3a7b9c1d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2c # Different SHA — it's a completely new commit object. # Step 3: Prove the old commit still exists in the object database echo "Old commit still exists:" git cat-file -t $BEFORE_AMEND # Output: commit — the old commit object is still in the repo. git log --oneline $BEFORE_AMEND -1 # Output: 9f2c4a1 feat: add retry config # The old commit with the old message is still there. # Step 4: Show that no branch points to the old commit echo "Branches containing old commit:" git branch --contains $BEFORE_AMEND # Output: (empty) — no branch references the old commit anymore. # It's orphaned. Git's gc will delete it after 90 days by default. # Step 5: The new commit has a different parent chain echo "New commit's parent:" git log --oneline -2 # The new commit's parent is the commit BEFORE the old commit. # Not the old commit itself — the old commit is bypassed entirely. # ───────────────────────────────────────────────────────────── # RECOVERING THE OLD COMMIT (if you amended by mistake) # ───────────────────────────────────────────────────────────── # ORIG_HEAD points to where HEAD was before the amend. git show ORIG_HEAD --oneline --no-patch # Output: 9f2c4a1 feat: add retry config # This is your escape hatch. # To undo the amend and go back to the old commit: git reset --hard ORIG_HEAD # WARNING: this discards the amended commit. The old commit is restored. # If ORIG_HEAD is gone (you ran other Git commands since the amend): git reflog # Shows every HEAD movement. Find the line with the old commit. # Then: git reset --hard <old-commit-hash>
- New SHA means every reference to the old SHA is now a dangling pointer
- Old commit is recoverable via reflog for 90 days — then garbage-collected permanently
- CI webhooks, deployment trackers, and teammate branches may reference the old SHA
- Understanding this prevents both 'why is my force push rejected' and 'where did my work go'
Amend the Last Commit: Message, Content, and Author
There are three things you can change with amend: the commit message, the commit content (files), and the commit author. You can change one, two, or all three in a single amend command.
For message-only: git commit --amend -m 'new message'. No editor, one line, done.
For content changes: stage the additional files or changes first with git add, then amend. The staged changes get rolled into the existing commit. Use --no-edit to keep the existing message unchanged.
For author correction: git commit --amend --author='Name <email>' --no-edit. This is how you fix commits made with the wrong Git config — common when switching between work and personal machines, or when a CI bot makes commits under the wrong identity.
The most common production pattern: stage a forgotten file, amend with --no-edit. This is the 'oops, I forgot the test file' command that every developer runs multiple times a week.
# io.thecodeforge — Git Amend Patterns # ───────────────────────────────────────────────────────────── # PATTERN 1: Fix commit message (no editor) # ───────────────────────────────────────────────────────────── git commit --amend -m "feat(payment): Add PaymentRetryService with exponential backoff" # Done. New commit with new message. Old commit orphaned. # ───────────────────────────────────────────────────────────── # PATTERN 2: Fix commit message (with editor) # Opens your configured editor (vim, nano, VS Code) with the existing message. # ───────────────────────────────────────────────────────────── git commit --amend # Edit the message, save, close. New commit created. # ───────────────────────────────────────────────────────────── # PATTERN 3: Add forgotten file, keep existing message # The most common 'oops' pattern. # ───────────────────────────────────────────────────────────── git add src/main/java/io/thecodeforge/payment/RetryConfig.java git commit --amend --no-edit # ───────────────────────────────────────────────────────────── # PATTERN 4: Add forgotten file AND change message # ───────────────────────────────────────────────────────────── git add src/main/java/io/thecodeforge/payment/RetryConfig.java git commit --amend -m "feat(payment): Add PaymentRetryService with retry config and tests" # ───────────────────────────────────────────────────────────── # PATTERN 5: Fix author name/email # ───────────────────────────────────────────────────────────── git commit --amend --author="John Doe <john.doe@thecodeforge.io>" --no-edit # Author changed. Commit hash changes. Message and content unchanged. # ───────────────────────────────────────────────────────────── # PATTERN 6: Add DCO sign-off to the last commit # Required by some open-source projects (Linux kernel, CNCF). # ───────────────────────────────────────────────────────────── git commit --amend --signoff --no-edit # Adds 'Signed-off-by: Name <email>' to the commit message. # The sign-off certifies you have the right to submit the code. # ───────────────────────────────────────────────────────────── # PATTERN 7: Remove a file that shouldn't have been committed # ───────────────────────────────────────────────────────────── git rm --cached src/main/resources/application-secrets.yml git commit --amend --no-edit # The file is removed from the commit but NOT from your working directory. # --cached means 'remove from index only, keep the file on disk.' # ───────────────────────────────────────────────────────────── # AFTER AMEND: Push to remote (your own branch only) # ───────────────────────────────────────────────────────────── git push origin feature/payment-retry --force-with-lease # --force-with-lease is SAFER than --force. Explained in next section.
- Stage first, then amend — the order matters. Amend uses whatever is staged.
- --no-edit preserves the message. Without it, your editor opens every time.
- Verify with git diff HEAD~1 HEAD after amending to confirm the file is included.
- This pattern is safe only on branches nobody else has pulled.
alias oops='git add -A && git commit --amend --no-edit'. The risk: git add -A stages ALL changes, not just the forgotten file. If you have unrelated working-directory changes, they get folded into the amend. Always use git add <specific-file> for amends, not git add -A. The author correction pattern (--author) is critical for CI/CD pipelines where bots commit under default identities. If your release process relies on commit authors for changelog generation, a misattributed bot commit can produce incorrect release notes.git add <file> && git commit --amend --no-edit. Always stage the specific file before amending — never use git add -A for amends to avoid accidentally including unrelated changes.--force-with-lease vs --force: The Safety Net You Should Never Skip
After amending a pushed commit, you need to force push. The remote branch still points to the old SHA. Your local branch points to the new SHA. A normal git push is rejected. But there are two force push options, and one of them can destroy your teammates' work.
--force overwrites the remote branch with your local branch, no questions asked. If a teammate pushed commits since your last fetch, --force silently destroys those commits. They're not merged, they're not saved — they're erased from the remote branch.
--force-with-lease does the same overwrite but first checks that the remote branch matches your local tracking reference. If someone else pushed, --force-with-lease rejects the push with an error. It forces you to fetch and review the conflicting commits before proceeding.
There is never a legitimate reason to use --force over --force-with-lease on a branch where anyone else might have pushed. Never.
# io.thecodeforge — --force-with-lease vs --force # ───────────────────────────────────────────────────────────── # WRONG: --force overwrites without checking git push origin feature/payment-retry --force # If a teammate pushed commit X after your last fetch: # Commit X is SILENTLY DESTROYED. No warning. No recovery (except reflog). # The remote branch now points to your amended commit. # Your teammate's work is gone from the branch. # ───────────────────────────────────────────────────────────── # RIGHT: --force-with-lease checks before overwriting git push origin feature/payment-retry --force-with-lease # If the remote has commits your local tracking ref doesn't know about: # error: failed to push some refs to '...' # hint: Updates were rejected because the tip of your current branch is behind # hint: its remote counterpart. Integrate the remote changes before pushing again. # This means: someone else pushed. Fetch and review before force-pushing. # ───────────────────────────────────────────────────────────── # VERIFY SAFETY: Check what the remote has that you don't # ───────────────────────────────────────────────────────────── # Before force-pushing, always fetch and check git fetch origin git log origin/feature/payment-retry..HEAD --oneline # Shows commits YOU have that the remote doesn't (your amend). git log HEAD..origin/feature/payment-retry --oneline # Shows commits the REMOTE has that you don't. # If this is empty, safe to force-push. # If this is NOT empty, someone else pushed. Don't force-push blindly. # ───────────────────────────────────────────────────────────── # MAKE --force-with-lease THE DEFAULT: # ───────────────────────────────────────────────────────────── # Option 1: Alias git config --global alias.pushf 'push --force-with-lease' # Now: git pushf feature/payment-retry # Option 2: Make ALL force pushes use --force-with-lease git config --global push.useForceIfIncludes true # In Git 2.30+, this makes --force behave like --force-with-lease # by requiring the remote ref to match your tracking ref. # ───────────────────────────────────────────────────────────── # WHAT IF --force-with-lease REJECTS YOUR PUSH? # ───────────────────────────────────────────────────────────── # Step 1: Fetch to see what the remote has git fetch origin # Step 2: See what your teammate pushed git log HEAD..origin/feature/payment-retry --oneline # Output: # b7c8d9e Add error handling for PaymentRetryService # f1a2b3c Fix null check in RetryConfig # Step 3: Rebase your amended commit on top of their work git rebase origin/feature/payment-retry # Your amended commit is replayed on top of their two commits. # Step 4: Now force-push is safe (your tracking ref is up to date) git push origin feature/payment-retry --force-with-lease
origin/<branch> tracking reference — not against your local branch. This means the safety net only works if you've fetched recently. If you last fetched 3 hours ago and a teammate pushed 2 hours ago, --force-with-lease will reject. But if you last fetched 3 hours ago, a teammate pushed 2 hours ago, and you then fetched 1 hour ago (updating your tracking ref), --force-with-lease will think it's safe — even though the teammate's commits are now on the remote. The safety is only as good as your last fetch. Always git fetch before --force-with-lease.push.useForceIfIncludes=true (Git 2.30+) to make --force behave like --force-with-lease globally.Amending Older Commits with Interactive Rebase
git commit --amend only works on the most recent commit (HEAD). What if the commit you need to fix is three commits back? That's where git rebase -i (interactive rebase) comes in.
Interactive rebase lets you replay a series of commits and stop at any point to modify them. You mark the commit you want to amend with the 'edit' keyword, Git pauses at that commit, you make your changes, amend the commit, then continue the rebase.
The syntax: git rebase -i HEAD~3 opens an editor showing the last 3 commits. Change 'pick' to 'edit' on the commit you want to modify. Save and close. Git replays commits until it reaches the marked one, then pauses. You stage your changes, run git commit --amend, then run git rebase --continue to replay the remaining commits.
Critical warning: interactive rebase rewrites every commit from the one you modify onward. All those commits get new SHAs. This is fine for local-only branches. For branches that teammates have pulled, this breaks their local history — the same problem as amending a pushed commit, but affecting multiple commits instead of one.
# io.thecodeforge — Amending Older Commits with Interactive Rebase # ───────────────────────────────────────────────────────────── # SCENARIO: You have 5 commits on feature/payment-retry. # Commit #3 (counting from HEAD) has a bug in PaymentService.java. # You need to fix that file IN that commit, not in a new commit. # ───────────────────────────────────────────────────────────── # Step 1: See your recent commits git log --oneline -5 # a1b2c3d (HEAD -> feature/payment-retry) Add integration test # e4f5g6h Add RetryConfig validation # i7j8k9l Add PaymentService.retryPayment() method ← THIS ONE HAS THE BUG # m0n1o2p Add RetryConfig data class # q3r4s5t Add PaymentClient interface # Step 2: Start interactive rebase for the last 5 commits git rebase -i HEAD~5 # Editor opens with: # # pick q3r4s5t Add PaymentClient interface # pick m0n1o2p Add RetryConfig data class # pick i7j8k9l Add PaymentService.retryPayment() method ← change 'pick' to 'edit' # pick e4f5g6h Add RetryConfig validation # pick a1b2c3d Add integration test # # Save and close. # Step 3: Git pauses at commit i7j8k9l git status # Output: interactive rebase in progress; onto abc1234 # Last command done: edit i7j8k9l Add PaymentService.retryPayment() method # You are currently editing a commit. # Step 4: Fix the bug in PaymentService.java echo '// fixed: null check added' >> src/main/java/io/thecodeforge/payment/PaymentService.java # Step 5: Stage the fix and amend the commit git add src/main/java/io/thecodeforge/payment/PaymentService.java git commit --amend --no-edit # The fix is now part of commit i7j8k9l (with a new SHA). # Step 6: Continue the rebase — replay the remaining commits git rebase --continue # Git replays e4f5g6h and a1b2c3d on top of the amended commit. # If there are conflicts during replay, resolve them and run git rebase --continue. # Step 7: Verify the history is clean git log --oneline -5 # Shows all 5 commits with the fix baked into commit #3. # All SHAs are different from before (history was rewritten). # ───────────────────────────────────────────────────────────── # ABORT: If things go wrong during interactive rebase # ───────────────────────────────────────────────────────────── git rebase --abort # Resets everything to the state before you started the rebase. # Safe escape hatch. Use it without hesitation. # ───────────────────────────────────────────────────────────── # PUSH AFTER INTERACTIVE REBASE (your own branch only) # ───────────────────────────────────────────────────────────── git push origin feature/payment-retry --force-with-lease # Required because multiple commits were rewritten.
git rebase --continue. The more commits between your edit point and HEAD, the higher the conflict probability. For complex chains, consider creating a fixup commit instead — it achieves the same clean history without the rebase conflict risk.git commit --fixup with autosquash over interactive rebase.When NOT to Amend: The Decision Tree
Amend is not always the right tool. Here's the decision tree that prevents 95% of amend-related incidents.
Amend is safe when: the commit has NOT been pushed to a shared branch, OR the commit is on a branch only you work on, OR you're about to push for the first time.
Amend is dangerous when: the commit has been pushed to a branch that teammates pull from, OR the commit is on main/develop/release, OR other people have based commits on top of yours, OR a CI pipeline has already run against the old commit.
Use revert instead when: the commit is on a shared branch, OR you want to undo the commit's changes while preserving history, OR the team policy prohibits force-pushing.
Use a new commit instead when: the change is logically separate from the last commit (even if it's small), OR you want to preserve the 'fix' as a visible history entry, OR you're on a shared branch where amend is forbidden.
The mental model: amend is for 'I made a mistake in the last 30 seconds.' If more than 30 seconds have passed — or if anyone else has seen the commit — create a new commit instead.
# io.thecodeforge — When to Amend vs When NOT to Amend # ───────────────────────────────────────────────────────────── # DECISION: Has the commit been pushed to a shared branch? # ───────────────────────────────────────────────────────────── # YES → Do NOT amend. Use one of these instead: # Option A: Create a fixup commit (clean, trackable) git add src/payments/RetryConfig.java git commit --fixup HEAD # Creates a commit with message 'fixup! <original message>' # Later, during merge or rebase, Git auto-squashes fixup commits. # Or: git rebase -i --autosquash (automatically orders fixup commits). # Option B: Revert the commit (undo its changes, preserve history) git revert HEAD # Creates a new commit that undoes the last commit's changes. # Safe to push. No force push required. History is preserved. # Option C: Create a new commit with the fix (simplest) git add src/payments/RetryConfig.java git commit -m "fix(payment): add missing null check in RetryConfig" # New commit, new message, no history rewrite. Safe everywhere. # ───────────────────────────────────────────────────────────── # DECISION: Is the commit on a branch only you use? # ───────────────────────────────────────────────────────────── # YES → Amend freely. Force push with --force-with-lease. git add src/payments/RetryConfig.java git commit --amend --no-edit git push origin feature/payment-retry --force-with-lease # ───────────────────────────────────────────────────────────── # DECISION: Is the commit on main/develop/release? # ───────────────────────────────────────────────────────────── # NEVER amend on main, develop, or release branches. # These branches are pulled by every developer on the team. # Amending here will break everyone's local history. # If you need to fix something on main: git revert <commit-hash> # undo the change safely # or git cherry-pick <fix-commit> # bring a fix from another branch # ───────────────────────────────────────────────────────────── # FIXUP COMMITS: The amend alternative for shared branches # ───────────────────────────────────────────────────────────── # Create a fixup commit that targets a specific older commit git commit --fixup abc1234 # Creates: 'fixup! <original message of abc1234>' # Later, during interactive rebase with autosquash: git rebase -i --autosquash main # Git automatically places the fixup commit next to its target # and marks it as 'fixup' (squash without editing message). # The result: the fix is baked into the original commit. # This is how you 'amend' a commit that's already been pushed # without force-pushing until the final rebase.
- Safe: local-only branch, pre-push, nobody else has pulled
- Dangerous: shared branch, main/develop/release, CI already ran
- Alternative on shared branches: fixup commits (auto-squashed during rebase)
- Alternative for undoing: revert (preserves history, no force push)
git rebase -i --autosquash main before merging the PR. The PR shows the fixup commits (reviewers can see what was fixed), and the final merge has clean, squashed history. This gives you the cleanliness of amend without the danger of force-pushing. The key configuration: git config --global rebase.autoSquash true — this makes --autosquash the default for all interactive rebases.git commit --fixup to create fixup commits that auto-squash during rebase. On main/develop/release, never amend — use git revert to undo changes safely.Undoing a Bad Amend: ORIG_HEAD and Reflog
You amended a commit and immediately realized you made it worse — wrong message, removed a needed file, or amended the wrong commit. How do you undo it?
Git saves ORIG_HEAD before every dangerous operation (amend, rebase, reset). After amending, ORIG_HEAD points to the commit you amended — the one with the original message and content. git reset --hard ORIG_HEAD restores your branch to the pre-amend state.
If you ran other Git commands after the amend (which overwrites ORIG_HEAD), use git reflog. The reflog records every HEAD movement for 90 days. Find the line with the original commit, copy its hash, and reset to it.
The window: 90 days. After that, orphaned commits are garbage-collected and unrecoverable. If you realize you amended the wrong commit two months later, the original may be gone.
# io.thecodeforge — Undoing a Bad Amend # ───────────────────────────────────────────────────────────── # SCENARIO 1: You just amended and want to undo immediately. # ORIG_HEAD is still available. # ───────────────────────────────────────────────────────────── # Check what ORIG_HEAD points to git show ORIG_HEAD --oneline --no-patch # Output: 9f2c4a1 feat: add retry config # This is the commit BEFORE your amend. # Undo the amend — restore the original commit git reset --hard ORIG_HEAD # Output: HEAD is now at 9f2c4a1 feat: add retry config # Your branch is back to the pre-amend state. # The amended commit is now orphaned (recoverable via reflog for 90 days). # ───────────────────────────────────────────────────────────── # SCENARIO 2: You ran other Git commands after amending. # ORIG_HEAD has been overwritten. Use reflog. # ───────────────────────────────────────────────────────────── git reflog # Output: # a1b2c3d HEAD@{0}: commit (amend): feat(payment): add PaymentRetryService ← the amend # 9f2c4a1 HEAD@{1}: commit: feat: add retry config ← THIS IS THE ORIGINAL # 8d1e3f5 HEAD@{2}: checkout: moving from main to feature/payment-retry # Reset to the original commit git reset --hard 9f2c4a1 # Output: HEAD is now at 9f2c4a1 feat: add retry config # ───────────────────────────────────────────────────────────── # SCENARIO 3: You amended, pushed, and now want to undo the push. # ───────────────────────────────────────────────────────────── # Option A: Force-push the original commit git reset --hard ORIG_HEAD # or reflog hash git push origin feature/payment-retry --force-with-lease # Remote now has the original commit. The amended commit is gone from remote. # Option B: Revert the amend (safer if others pulled the amended commit) git revert HEAD # Creates a new commit that undoes the amended commit's changes. # Then create another commit with the correct changes. # No force push required. History is preserved. # ───────────────────────────────────────────────────────────── # SCENARIO 4: You amended multiple times and want a specific version. # ───────────────────────────────────────────────────────────── git reflog # Output: # c3d4e5f HEAD@{0}: commit (amend): feat(payment): third attempt at message # b2c3d4e HEAD@{1}: commit (amend): feat(payment): second attempt at message # a1b2c3d HEAD@{2}: commit (amend): feat(payment): first attempt at message # 9f2c4a1 HEAD@{3}: commit: feat: add retry config ← the original # Pick any version and reset to it git reset --hard HEAD@{2} # Output: HEAD is now at a1b2c3d feat(payment): first attempt at message # You're back to the first amended version.
- ORIG_HEAD is set before amend, rebase, and reset operations
- It's overwritten by the next dangerous operation — not cumulative
- Reflog records every HEAD movement for 90 days (configurable via gc.reflogExpire)
- After 90 days, orphaned commits are garbage-collected and permanently unrecoverable
gc.reflogExpire and gc.reflogExpireUnreachable. For repositories with high commit velocity (monorepos, large teams), the reflog can grow large. The practical issue: git reflog output becomes noisy. Use git reflog --date=relative to see timestamps, and git reflog show <branch> to filter by branch. For critical repositories, consider setting gc.reflogExpire to 180 days. The trade-off: longer retention means more disk usage for the object database, but the recovery window is proportionally longer.Amending Merge Commits
You can amend a merge commit, but it works differently from amending a regular commit. A merge commit has two (or more) parents. git commit --amend on a merge commit lets you change the merge commit message, but it does NOT re-run the merge. The file content stays the same — you're only editing the message.
If you need to change the actual content of a merge commit (add a file that was missed, resolve a conflict differently), you make the changes, stage them, and then amend. The staged changes are folded into the merge commit.
The gotcha: amending a merge commit changes its SHA, which breaks the parent chain of every commit that came after it. This is the same problem as amending any other commit, but amplified because merge commits are often on shared branches (main, develop). Don't amend merge commits on shared branches.
# io.thecodeforge — Amending Merge Commits # ───────────────────────────────────────────────────────────── # SCENARIO: You merged a feature branch into main and the merge # commit message is the default 'Merge branch feature/x into main'. # You want a more descriptive message. # ───────────────────────────────────────────────────────────── # Step 1: Verify the last commit is a merge commit git log --oneline -1 --merges # Output: a1b2c3d Merge branch 'feature/payment-retry' into main # Step 2: Amend the merge commit message git commit --amend -m "Merge feature/payment-retry: Add PaymentRetryService with exponential backoff" # New merge commit created with descriptive message. # The file content is unchanged — only the message changed. # ───────────────────────────────────────────────────────────── # SCENARIO: You need to change content in a merge commit. # ───────────────────────────────────────────────────────────── # Step 1: Make the changes echo '// added during merge fix' >> src/payments/MergeNote.java # Step 2: Stage and amend git add src/payments/MergeNote.java git commit --amend --no-edit # The file is now part of the merge commit. # The merge commit's SHA changes. # ───────────────────────────────────────────────────────────── # WARNING: Amending merge commits on shared branches # ───────────────────────────────────────────────────────────── # If the merge commit is on main and teammates have pulled it: # - Their local main has the old merge commit SHA # - Your amended main has a new merge commit SHA # - Histories have diverged # - Force push required, breaks everyone # Rule: amend merge commits ONLY before anyone else pulls them. # If in doubt, create a follow-up commit instead: git add src/payments/MergeNote.java git commit -m "fix: add missing MergeNote.java from merge"
git merge --no-ff with custom messages. The risk of amending merge commits on main is amplified because merge commits are integration points — they're referenced by CI systems, deployment trackers, and release tooling. Rewriting a merge commit SHA can break all of these simultaneously. The safe alternative: configure your merge tool to produce good messages upfront, rather than amending after the fact.Amend in CI/CD: Re-triggering Builds and Webhook Behavior
When you amend a pushed commit and force-push, most CI systems (GitHub Actions, GitLab CI, Jenkins) detect the push event and trigger a new build. The old build — which was running against the old commit — may still be running. You now have two builds: one for the old commit (which will complete but is irrelevant) and one for the new commit (which is the one you care about).
The problem: if your CI system cancels the old build automatically (GitHub Actions does this for the same branch), you're fine. If it doesn't (some Jenkins configurations), you get two builds running simultaneously, potentially deploying conflicting artifacts.
The other problem: CI webhooks include the commit SHA in their payload. If a downstream system (like a deployment tracker or a notification bot) recorded the old SHA, it now references a commit that no longer exists on the branch. This can cause 'commit not found' errors in downstream systems.
Best practice: if you amend and force-push, check that the CI build triggered for the new SHA. If you're using GitHub Actions, the old build is automatically cancelled. If you're using Jenkins, you may need to manually abort the old build.
# io.thecodeforge — Amend and CI/CD Interaction # ───────────────────────────────────────────────────────────── # SCENARIO: You pushed a commit, CI started building. # You noticed a typo, amended, and force-pushed. # ───────────────────────────────────────────────────────────── # Step 1: Original push triggers CI git push origin feature/payment-retry # CI build #1042 starts — building commit 9f2c4a1 # Step 2: You notice typo, amend, force-push git commit --amend -m "feat(payment): Add PaymentRetryService" git push origin feature/payment-retry --force-with-lease # CI build #1043 starts — building commit a1b2c3d # Build #1042 is still running (for the old commit) # ───────────────────────────────────────────────────────────── # GITHUB ACTIONS: Auto-cancels previous builds on same branch # ───────────────────────────────────────────────────────────── # In your workflow YAML: # concurrency: # group: ${{ github.workflow }}-${{ github.ref }} # cancel-in-progress: true # This ensures only ONE build runs per branch at a time. # When you force-push, the old build is cancelled automatically. # ───────────────────────────────────────────────────────────── # GITLAB CI: Similar behaviour with resource_group # ───────────────────────────────────────────────────────────── # In .gitlab-ci.yml: # deploy: # resource_group: production # script: ./deploy.sh # resource_group ensures only one deploy job runs at a time. # ───────────────────────────────────────────────────────────── # JENKINS: Manual cancellation required # ───────────────────────────────────────────────────────────── # Jenkins does NOT auto-cancel builds on force-push by default. # You need to manually abort the old build (#1042) from the UI. # Or install the 'Discard Old Build' plugin with appropriate config. # ───────────────────────────────────────────────────────────── # VERIFY: Check that CI ran on the correct SHA # ───────────────────────────────────────────────────────────── git rev-parse HEAD # Output: a1b2c3d ← this should match the SHA in your CI build logs # If CI shows 9f2c4a1, it's building the old commit — abort it.
- Force-push triggers a new CI build for the new SHA
- The old build may still be running for the now-orphaned old SHA
- GitHub Actions: configure concurrency groups for auto-cancellation
- Jenkins: manual cancellation required — no default auto-cancel behavior
Git Aliases for Amend: Speed Up Your Daily Workflow
If you amend multiple times a day (you will), setting up aliases saves keystrokes and prevents typos. Here are the aliases I use on every machine.
The most useful: an alias for 'amend with no editor and force-push with lease.' This is the 'oops, forgot a file' one-liner that turns a three-command sequence into one.
# io.thecodeforge — Git Aliases for Amend Workflows # ───────────────────────────────────────────────────────────── # ESSENTIAL ALIASES — add these to your ~/.gitconfig # ───────────────────────────────────────────────────────────── # Amend last commit without opening editor git config --global alias.amend 'commit --amend --no-edit' # Usage: git amend # Equivalent to: git commit --amend --no-edit # Amend last commit WITH editor git config --global alias.amende 'commit --amend' # Usage: git amende # Opens editor to edit the message. # Amend and force-push with lease in one command git config --global alias.amendpush '!git commit --amend --no-edit && git push --force-with-lease' # Usage: git amendpush # Amends the last commit and force-pushes. Use only on your own branch. # Amend with signoff git config --global alias.amendsign 'commit --amend --signoff --no-edit' # Usage: git amendsign # Adds DCO sign-off to the last commit. # Force push with lease (safer alternative to --force) git config --global alias.pushf 'push --force-with-lease' # Usage: git pushf origin feature/my-branch # Undo the last amend (using ORIG_HEAD) git config --global alias.undoamend 'reset --hard ORIG_HEAD' # Usage: git undoamend # WARNING: discards the amended commit. Use immediately after amend. # Show what the last amend changed git config --global alias.amenddiff 'diff ORIG_HEAD HEAD' # Usage: git amenddiff # Shows the diff between the original commit and the amended commit. # Useful for verifying your amend did what you expected. # ───────────────────────────────────────────────────────────── # SHELL ALTERNATIVE — add to ~/.bashrc or ~/.zshrc # ───────────────────────────────────────────────────────────── # One-liner: amend + push (for when you KNOW it's safe) alias gamend='git commit --amend --no-edit && git push --force-with-lease' # Quick message fix: amend with new message and push alias gamendm='git commit --amend && git push --force-with-lease'
- Shows the exact diff between ORIG_HEAD and current HEAD
- Catches the common mistake: amending without staging the forgotten file first
- Verifies the amend targeted the correct commit (not a different one)
- Two-second sanity check that prevents hours of debugging later
amendpush alias combines amend and force-push into one command. This is convenient but dangerous: it removes the pause between amend and push where you might realize the amend was wrong. The safer workflow: amend, verify with amenddiff, then push separately. The amendpush alias should certain nobody else has pulled. only be used on branches where you're absolutely For team-shared branches, never combine amend and push into a single command — the verification step is essential.amend, pushf, undoamend, amenddiff) reduce keystrokes and prevent --force accidents. The amenddiff alias is the most underrated — it verifies your amend did what you expected by showing the diff between the original and amended commits. Use it as a post-amend sanity check.When `git commit --amend` Is Actually the Right Tool
Most devs treat amend like a magic undo button. It's not. It's a surgical tool with exactly three legitimate use cases in production code.
First: you just pushed a broken build and need to hotfix the commit message so CI picks up the correct JIRA ticket. Second: you staged everything except that one config file that's already in .gitignore. Third: you need to strip a hardcoded API key from a commit that hasn't left your local branch yet.
Outside those three scenarios, you're probably making a mess. The rule is simple — if anyone else has based work on that commit, you don't amend. You commit-on-top and squash during code review. The team expects linear history on main. Don't give them a rebus puzzle.
// io.thecodeforge — devops tutorial # Scenario: Forgot to exclude .env from last commit git add .gitignore git commit --amend --no-edit # Verify no sensitive data in reflog git reflog show -3 # Output shows only the amended hash
The Hidden Cost of Amending Shared Branches
Your team's CI/CD pipeline is running on the assumption that commit hashes are immutable. When you force-push an amended commit to a shared branch, every in-flight PR, every cached test result, every deployment tag referencing the old hash becomes garbage.
Here's what actually happens: GitHub strips the old commit from the PR timeline. CircleCI loses the build artifacts. Your QA engineer's local branch now has a merge conflict against thin air. The junior who pulled feature-branch two minutes ago is now working against a ghost commit that only exists in their reflog.
Worst case: your amended commit removes a security patch from the diff, but the old commit still exists in someone's cache. The next deploy picks the wrong version. You've just created a rollback nightmare that takes hours to untangle.
If you absolutely must amend a shared branch — and sometimes you must — coordinate the force-push with everyone who has the branch checked out. Send a Slack message. Wait 60 seconds. Then push with --force-with-lease.
// io.thecodeforge — devops tutorial # Safe amend on shared branch workflow git checkout feature/api-rate-limiting # Amend the last commit message to match PR title git commit --amend -m "feat: add rate limiting to API endpoints" # Force-push only if you've warned the team git push --force-with-lease origin feature/api-rate-limiting # Verify remote state simplified Updating abc1234..def5678 + abc1234...def5678 feature/api-rate-limiting -> feature/api-rate-limiting (forced)
git config --global safety.push --force-with-lease so you never accidentally raw force-push. The 0.5 seconds it saves isn't worth the production outage.Hash Invalidation: Why Amending a Single Commit Breaks Every Descendant
Every Git commit is identified by a SHA-1 hash that depends on the entire snapshot plus its parent hashes. When you amend a commit, Git creates a new hash for that commit. Since each child stores its parent's hash, every subsequent commit must also be rewritten with new hashes. This cascading hash invalidation is the mechanical root of all the pain: pull requests become orphaned, tags point to ghosts, and collaborators see a divergent history. Understanding this immutability constraint is essential before you touch any shared branch. A local amend is cheap; a pushed amend on a topic branch with three downstream commits forces a rebase that invalidates all of them.
// io.thecodeforge — devops tutorial // Demonstrating hash breakage after amend git log --oneline --graph # * a1b2c3d (HEAD -> feature) fix: pagination # * e4f5g6h feat: add search # * i7j8k9l initial git commit --amend -m "fix: pagination edge case" git log --oneline --graph # * m0n1o2p (HEAD) fix: pagination edge case ← new hash # * e4f5g6h feat: add search ← still same hash # * i7j8k9l initial
Branch Protection Policies: When `--force-with-lease` Still Fails
Branch protection rules on GitHub, GitLab, or Bitbucket block direct pushes to main or release branches. But they do not block --force-with-lease on unprotected feature branches — unless the repository enforces linear history or signed commits. The real danger is that protection policies only check the target branch state, not the integrity of rewritten history. A developer can amend a commit, force-push to their feature branch, and CI will rebuild against a different snapshot than what was reviewed. To prevent this, enforce a 'no force push' rule on shared feature branches and require squash-merge instead of amend-before-merge. Status checks and required reviews do not re-run automatically after an amended force-push.
// io.thecodeforge — devops tutorial // GitHub branch protection rule example branches: - name: main protection: required_pull_request_reviews: required_approving_review_count: 2 required_status_checks: strict: true # branch must be up-to-date restrictions: users: [] teams: ["core-devs"] enforce_admins: true
Amend Only Locally: The One Safe Workflow for Unpushed Commits
The only truly safe git commit --amend is the one performed before the commit ever leaves your machine. You can amend the message, add missing files, or fix a typo without any downstream consequences. This is the ideal use case: commit often with 'WIP' messages, then amend to write a clean, atomic commit message right before you push. The danger starts the moment you share the commit — even on a personal branch. If you must amend after push, immediately coordinate with everyone who pulled that branch. Use git reflog to locate the old commit and run git reset --hard <old-hash> if someone needs to revert. For teams, enforce a 'no amend after push' rule and rely on fixup commits during code review instead.
// io.thecodeforge — devops tutorial // Safe local amend workflow git add . git commit -m "WIP: still debugging" # ... test, realize missing file ... git add missing_file.py git commit --amend -m "fix: resolve race condition in scheduler" # still only local — safe to push now git push origin feature-branch
git log --oneline @{u}..HEAD shows exactly one commit and it's yours. Otherwise you're rewriting shared history.Amend on Monorepo Main Branch: 40 Engineers, 6 Divergent Histories
- Never amend on main, develop, or release branches. These are pulled by every developer. Amending here breaks everyone simultaneously.
- git push --force without --force-with-lease destroyed the safety check that would have prevented this. --force-with-lease would have been irrelevant here (nobody else pushed), but the habit of using --force normalizes dangerous behavior.
- Branch protection rules that prevent direct pushes to main would have prevented this entirely. The fix wasn't just technical — it was a process gap.
- The CI webhook referencing the old SHA is an often-overlooked blast radius. Downstream systems that record commit SHAs will break when those SHAs are rewritten.
git fetch origingit reset --hard origin/<branch>git fetch origin && git log HEAD..origin/<branch> --onelinegit rebase origin/<branch> (replay your work on top of remote)git show ORIG_HEAD --oneline --no-patch (verify it's the right commit)git reset --hard ORIG_HEAD (undo the amend)git rev-parse HEAD (get current SHA to compare against CI)Check CI dashboard for running builds on this branchgit status (see which files have conflicts)git rebase --abort (escape hatch — returns to pre-rebase state)| Use Case | Command | Safe on Shared Branch? |
|---|---|---|
| Fix commit message only | git commit --amend -m 'new message' | No — rewrites history |
| Add forgotten file, keep message | git add <file> && git commit --amend --no-edit | No — rewrites history |
| Add forgotten file, change message | git add <file> && git commit --amend -m 'new message' | No — rewrites history |
| Fix author name/email | git commit --amend --author='Name <email>' --no-edit | No — rewrites history |
| Add DCO sign-off | git commit --amend --signoff --no-edit | No — rewrites history |
| Remove a committed file | git rm --cached <file> && git commit --amend --no-edit | No — rewrites history |
| Open editor to rewrite message | git commit --amend | No — rewrites history |
| Amend older commit (3 commits back) | git rebase -i HEAD~3 → change 'pick' to 'edit' | No — rewrites multiple commits |
| Fix a commit on a shared branch | git commit --fixup <target-sha> (squash later) | Yes — creates new commit, no history rewrite |
| Undo a pushed commit's changes | git revert <commit-sha> | Yes — creates new commit, no history rewrite |
| Undo a bad amend | git reset --hard ORIG_HEAD | Local only — doesn't touch remote |
| File | Command / Code | Purpose |
|---|---|---|
| 01_amend_basics.sh | echo 'Important file' > important.txt | What Is Git Amend |
| io | git add src/payments/RetryConfig.java | How Amend Actually Works Under the Hood |
| io | git commit --amend -m "feat(payment): Add PaymentRetryService with exponential b... | Amend the Last Commit |
| io | git push origin feature/payment-retry --force | --force-with-lease vs --force |
| io | git log --oneline -5 | Amending Older Commits with Interactive Rebase |
| io | git add src/payments/RetryConfig.java | When NOT to Amend |
| io | git show ORIG_HEAD --oneline --no-patch | Undoing a Bad Amend |
| io | git log --oneline -1 --merges | Amending Merge Commits |
| io | git push origin feature/payment-retry | Amend in CI/CD |
| io | git config --global alias.amend 'commit --amend --no-edit' | Git Aliases for Amend |
| LegitimateAmend.yml | git add .gitignore | When `git commit --amend` Is Actually the Right Tool |
| CoordinateForcePush.yml | git checkout feature/api-rate-limiting | The Hidden Cost of Amending Shared Branches |
| HashInvalidation.yml | git log --oneline --graph | Hash Invalidation |
| BranchProtection.yml | branches: | Branch Protection Policies |
| LocalOnlyAmend.yml | git add . | Amend Only Locally |
Key takeaways
Interview Questions on This Topic
Frequently Asked Questions
Yes. Amend creates an entirely new commit object with a new SHA hash. The old commit is orphaned — no branch points to it — but it still exists in Git's object database for 90 days and is recoverable via git reflog. This is why amending pushed commits requires a force push: the remote branch still points to the old SHA while your local branch points to the new one.
Use git commit --amend -m 'your new message'. The -m flag provides the message inline so no editor is opened. For a one-character alias, add git config --global alias.amend 'commit --amend --no-edit' to your gitconfig.
Stage the file with git add <filename>, then run git commit --amend --no-edit. The --no-edit flag preserves the existing commit message. This is the most common amend use case — you'll run this command multiple times a week.
--force overwrites the remote branch with your local branch, no questions asked. If a teammate pushed commits since your last fetch, --force silently destroys those commits. --force-with-lease does the same overwrite but first checks that the remote branch matches your local tracking reference (origin/branch-name). If someone else pushed since you last fetched, --force-with-lease rejects the push. Always use --force-with-lease.
Use git rebase -i HEAD~N (where N is the number of commits back). In the editor, change 'pick' to 'edit' on the commit you want to modify. Git pauses at that commit. Make your changes, stage them, run git commit --amend, then run git rebase --continue. This rewrites every commit from the edit point onward — only safe on local-only branches.
If you just amended: git reset --hard ORIG_HEAD restores the original commit. If you ran other Git commands since the amend: git reflog shows every HEAD movement for 90 days — find the original commit hash and reset to it. If you amended and force-pushed: either force-push the original commit (git reset --hard ORIG_HEAD && git push --force-with-lease) or create a revert commit (git revert HEAD) if others have already pulled the amended commit.
Technically yes, but you should never do it. Amending on main rewrites history that every developer on the team has in their local repository. Their next pull will show 'Your branch and 'origin/main' have diverged' and they'll need to reset their local main. Use git revert to undo changes on main, or git commit --fixup to create a fixup commit that gets squashed during the next rebase.
Most CI systems (GitHub Actions, GitLab CI) detect the new push and trigger a new build with the new SHA. The old build may still be running. GitHub Actions auto-cancels the old build if you configure concurrency groups. Jenkins does not auto-cancel — you need to manually abort the old build. Always verify your CI dashboard shows a build for the new SHA, not the old one.
git commit --fixup <target-sha> creates a commit with the message 'fixup! <original message>'. During git rebase -i --autosquash, Git automatically places the fixup commit next to its target and squashes it. Use fixup commits when you can't amend (because the target commit is on a shared branch). The fixup is visible in the PR history, and it gets cleaned up automatically during the final rebase before merge.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Git. Mark it forged?
10 min read · try the examples if you haven't