Team training · Block 3 of 6
Rewriting local history
Still yours alone. Still safe.
Set the boundary immediately: everything in this block operates on commits nobody else has seen. The constraint on rewriting arrives in Block 4, once "shared" means something concrete.
Callback to Block 2: they now know nothing is really lost. That confidence is the prerequisite for this block — rebase is only frightening if you believe mistakes are permanent.
~41 minutes.
Nothing here edits a commit
Commits are named by their contents. Change anything and you have a different commit .
Every command in this block creates new commits and moves a branch label onto them. The originals are orphaned, not modified.
Which means "rewriting history" is a misleading phrase — and also why the old commits are still in your reflog.
Direct callback to the hash slide in Block 1. If that landed, this is a two-second confirmation. If it didn't, this is the second chance.
Worth being blunt: the phrase "rewrite history" makes people imagine git going back and altering something. It never does. It builds replacements and re-points the label.
Practical consequence they'll appreciate: a botched rebase is recoverable exactly like a botched reset, because the originals are sitting unreferenced with reflog entries pointing at them.
3.1
amend
~7 minutes. Quick win, immediately useful, and a gentle first taste of rewriting.
Fixing the commit you just made
$ git commit -m "Add tax calclation" # typo
[main c584509] Add tax calclation
$ git commit --amend -m "Add tax calculation"
[main 9f2ab41] Add tax calculation
Different hash. c584509 still exists, unreferenced — 9f2ab41 is a new commit that replaced it on the branch.
Point at the two hashes side by side before advancing. Ask what changed. Then reveal.
Also amends content, not just the message: stage a forgotten file, then git commit --amend --no-edit folds it into the previous commit keeping the message.
The warning: only amend commits you have not pushed. It's a rewrite, and Block 4 explains why that matters. For now the rule is "if it's pushed, don't".
3.2
rebase
~16 minutes. The most feared command in git and the one most improved by seeing the mechanism.
Replaying commits onto a new base
main
feature
copies — new hashes
originals, orphaned
feature
Rebase doesn't move your commits. It copies them onto a new starting point.
Ask first: given that commits are immutable and named by content, what must rebase actually be doing? Some people work it out — the answer is forced by Block 1.
Advance 1: the copies appear on top of main, in amber, with new hashes. Advance 2: the originals grey out as orphaned.
The word "rebase" is accurate: same changes, different base. Every copied commit has a different parent, therefore different content, therefore a different hash.
This is why rebasing a shared branch causes pain — colleagues hold the originals, which no longer exist on your branch.
Doing it
$ git switch feature
$ git rebase main
Successfully rebased and updated refs/heads/feature.
Read it as: "take my commits, and replay them as if I'd started from main instead."
Conflicts can surface per replayed commit . Resolve, then git rebase --continue. Or --abort to put everything back.
The direction confuses everyone: you stand on the branch being moved and name the branch you're moving onto. Say it out loud twice.
--abort is the reassurance people need. At any point mid-rebase, abort returns you exactly to where you started. Demo it — starting a rebase, hitting a conflict, aborting, and showing the graph unchanged does more for confidence than any explanation.
Conflicts arriving one commit at a time surprises people who expect a single conflict like a merge. Mention it before they meet it.
Merge or rebase?
merge main into your branch
Adds a merge commit. Nothing is rewritten. Safe even if the branch is shared.
rebase onto main
Linear history, no merge commit. Rewrites — so only while the branch is yours alone.
On our setup, trunk only ever receives squashed commits — so merge commits on a feature branch never reach main. Both options are fine.
Deliberately not framed as a holy war. On a squash-merge team the usual argument is moot: whatever shape your branch has, trunk gets one commit.
That's genuinely liberating and worth stating plainly — it means merging main into your branch is a completely legitimate choice, and it removes force push from the normal workflow, because you never rewrite anything.
Our rule, previewed here and confirmed in Block 5: rebase while it's yours, merge once it's shared.
3.3
Interactive rebase
~14 minutes. The history-tidying tool. Reframed for our squash-merge context.
A todo list you can edit
$ git rebase -i HEAD~4
pick 91ac5d3 Add discount helper
pick e91e334 wip
pick c584509 wip2
pick f6c6987 fix typo
# p, pick = use commit
# r, reword = use commit, edit the message
# s, squash = meld into previous, combine messages
# f, fixup = meld into previous, discard message
# d, drop = remove commit
Squashing everything back into one? Skip the todo list entirely: git reset --soft <ancestor> moves the branch back but leaves every change staged — then a single commit rebuilds it. <ancestor> is where you branched from: main, or HEAD~4.
Git opens core.editor with this file. You edit the verbs, save, close, and git replays according to your instructions. That's the entire mechanism.
This is where an unconfigured editor bites hardest — vim, mid-rebase, is the classic panic. Confirm everyone set core.editor in pre-flight.
Order is oldest at top, which is the reverse of git log. Say it; people get it backwards constantly.
The reset --soft escape hatch is worth demoing right after: if the goal is simply "collapse this whole branch into one commit", interactive rebase is overkill. git reset --soft main leaves every change staged on top of main's tip, and one commit rebuilds the branch as a single clean commit. No editor, no per-commit replay. Reserve interactive rebase for when you want to keep some structure — reorder, reword, drop specific commits.
Collapsing the mess
before
pick 91ac5d3 Add discount helper
pick e91e334 wip
pick c584509 wip2
pick f6c6987 fix typo
after editing
pick 91ac5d3 Add discount helper
squash e91e334 wip
squash c584509 wip2
fixup f6c6987 fix typo
Four commits become one. squash lets you edit the combined message; fixup silently discards its own.
Demo it end to end. The visible collapse from four log lines to one is the payoff.
Practical guidance: use fixup for commits whose messages are worthless ("wip", "fix typo") and squash where the message contributes something. Most real cleanups are mostly fixups.
Mention --abort works here too, same as a plain rebase.
Why bother, given squash merge?
Trunk gets one commit either way. So this is not about keeping main tidy.
It's about the reviewer. A branch of four coherent commits can be reviewed step by step. A branch of eleven "wip" commits can only be reviewed as one enormous diff.
Polish, not hygiene. Worth doing on a large PR; skip it on a two-line fix.
Be honest about this. Standard git training justifies interactive rebase as protecting trunk history, and on our setup that's demonstrably false — the squash already handles it. Someone will notice, so say it first.
The real justification is review quality, which ties straight to commandments 2 and 11. A reviewer who can walk your reasoning in steps gives better feedback than one facing a wall of changes.
Proportionality matters: don't let this become a ritual applied to every branch. Big or subtle PR, worth the ten minutes. Small one, just merge it.
Where we got to
Want to Use
Fix the message or content of the last commit commit --amend
Move your branch on top of current main rebase main
Combine, reorder, or drop commits rebase -i
Undo any of the above rebase --abort, or reflog
All of it is safe while the branch is yours alone. Block 4 adds the one constraint that matters.
Close on the boundary. Everything in this block is safe precisely because nobody else holds these commits.
Tease Block 4 concretely: we're about to introduce other people's copies of the same history, and every rewrite in this block becomes dangerous the moment someone else has the original. That's the whole reason the shared-history rule exists.