Rebase and the Rules of Rewriting

What rebase actually does to your commits, the interactive todo list in full, and why force-with-lease is the only force-push you should ever type.

intermediate 22 min lesson hands-on task included

Rebase is the command people are warned about and then use daily. The warnings are correct and the daily use is also correct — the distinction is entirely about whether anyone else has the commits you are rewriting.


Topic 1: What Rebase Does

SAME WORK. TWO HISTORIES. MERGE — preserves what happened A B M F1 F2 Non-linear, honest, and noisy on a busy repo. REBASE — replays as if written later A B F1' F2' Linear and easy to read… …but F1' and F2' are NEW commits with NEW hashes. THE GOLDEN RULE Never rebase commits other people have pulled. Their history and yours no longer share objects, and the next merge duplicates every commit. THE SAFE FORCE-PUSH git push --force-with-lease Refuses if the remote moved since your last fetch — so you cannot silently delete a colleague's commit. INTERACTIVE REBASE IS THE SAME MACHINE WITH A TODO LIST pick · reword (message) · edit (stop here) · squash (merge + edit msg) · fixup (merge, drop msg) · drop · reorder lines git commit --fixup=<sha> then git rebase -i --autosquash — writes the todo list for you.
Same work, two shapes. The detail that matters is at the bottom of the right-hand panel: F1′ and F2′ are new objects. Rebase does not move commits, it replays them as copies.

git rebase main takes each commit unique to your branch, and re-applies it on top of main as a new commit. Same changes, same author, same message — new parent, new committer date, new hash.

before:    A ─ B ─ C          main
                    ╲
                     D ─ E    feature

git switch feature && git rebase main
# main has advanced to F meanwhile:

after:     A ─ B ─ C ─ F              main
                        ╲
                         D' ─ E'      feature   (new hashes)

D and E still exist as objects, now unreferenced, recoverable from the reflog. D' and E' are copies replayed onto F.

Rebase vs merge, honestly:

MergeRebase
HistoryWhat actually happenedWhat it would look like if written last
ShapeBranching graphLinear
HashesPreservedRewritten
Safe on shared branchesYesNo
ConflictsOnce, at the mergePotentially once per commit
git bisect, git revertSlightly harder on a mergeSimpler on linear history

Neither is correct in general. The workable convention most teams land on: rebase your own unpublished branch onto main, merge the finished branch into main. You get a readable branch history and an honest record of integration.


Topic 2: Conflicts During a Rebase

A rebase applies commits one at a time, so a conflict can occur at each one — and the “ours”/“theirs” labels are inverted relative to a merge, because Git is replaying your commit onto their branch:

  • ours = the branch you are rebasing onto (usually main)
  • theirs = the commit of yours being replayed
# ...conflict during rebase...
git status                 # says "interactive rebase in progress"
# fix the files
git add <files>
git rebase --continue

git rebase --skip          # drop this commit entirely — think first
git rebase --abort         # back to exactly where you started

--abort is always available until the rebase finishes. --skip discards the commit being applied, which is occasionally right (its change is already upstream) and is otherwise how work disappears silently.

Two options that remove most rebase friction:

git config --global rebase.autoStash true    # stash and restore dirty files around it
git config --global rerere.enabled true      # do not resolve the same conflict twice

Topic 3: Interactive Rebase — The Whole Verb List

git rebase -i HEAD~5
pick   a1b2c3  add retry logic
reword 9f8e7d  fix tpyo in error message      ← edit the message
squash 3c4d5e  actually fix the retry         ← fold into previous, merge messages
fixup  7a8b9c  address review comment         ← fold into previous, discard message
edit   2e4f6a  refactor client                ← stop here so I can change it
drop   1f2e3d  debug printf                   ← remove entirely

# reorder by moving lines · delete a line = drop it
VerbEffect
pickKeep as is
rewordKeep the change, edit the message
editPause here; amend the commit, or split it
squashCombine into the previous commit, open a combined message
fixupCombine into the previous commit, discard this message
dropRemove the commit
execRun a shell command at this point
breakStop here for no reason other than to look around

exec is the underrated one. It runs a command after each commit is applied — which lets you verify that every commit in a series builds, not just the final one:

git rebase -i --exec 'make test' HEAD~10

That is how you find the commit in the middle of your branch that does not compile, before a bisect finds it for you in six months.

Splitting a commit that does two things:

git rebase -i HEAD~3      # mark the commit 'edit'
git reset HEAD~           # undo the commit, keep the changes unstaged
git add -p                # stage the first logical piece
git commit -m "first thing"
git add -p && git commit -m "second thing"
git rebase --continue

The --fixup workflow, which is what interactive rebase looks like in a healthy team:

git commit --fixup=a1b2c3          # "this fixes commit a1b2c3"
# ...more review, more fixups...
git rebase -i --autosquash main    # todo list is written for you

Reviewers see each change as it arrives; main receives a clean series. No hand-editing of todo lists, no risk of reordering something wrongly.


Topic 4: The Golden Rule, Stated Precisely

Do not rewrite commits that other people have based work on.

The usual phrasing — “never rebase a public branch” — is close but imprecise. What matters is not publication, it is whether someone else’s history contains those commits. Rebasing your own feature branch that only you have, and then force-pushing it, is normal and safe even though it is “public” on the forge.

What breaks when the rule is broken: your colleague’s local branch still has the old commits. Their next git pull sees two lineages containing the same changes, merges them, and now the history has every commit twice — with conflicts, because the same change applies twice.

Recovery, if it happens to you:

git fetch origin
git reset --hard origin/feature      # discard your local copy — if it has no unique work
# If you DO have unique work:
git rebase --onto origin/feature <old-upstream-tip> feature

The second command is the one worth knowing: --onto replays only your unique commits onto the rewritten branch, which is exactly the surgery this situation calls for.


Topic 5: Force-Push Without Destroying Anything

git push --force-with-lease

Plain --force says “make the remote match me, whatever is there”. If a colleague pushed in the meantime, their commit is gone from the branch — recoverable, but only if someone notices.

--force-with-lease refuses unless the remote is where you last saw it. Someone else pushed? The push is rejected and you go and look. It is a one-word difference with an outsized effect, and it belongs in muscle memory and in your aliases.

One subtlety worth knowing: --force-with-lease compares against your remote-tracking ref, so a background git fetch (some IDEs do this automatically) can update that ref and defeat the check. The precise form removes the ambiguity:

git push --force-with-lease=feature:9f8e7d6   # only if origin/feature is still exactly this

And the modern belt-and-braces option, which additionally verifies the ref still exists:

git push --force-if-includes --force-with-lease

Topic 6: Cherry-Pick and Revert Interact With Rewriting

Both create new commits, and both matter here because they are how you move a change without moving a branch.

git cherry-pick 9f8e7d              # apply that commit here, as a new commit
git cherry-pick 9f8e7d^..3c4d5e     # a range
git cherry-pick -x 9f8e7d           # record "cherry picked from commit …"
git cherry-pick -n 9f8e7d           # apply, do not commit — for editing first

-x is worth using every time you cherry-pick between long-lived branches: it writes the source hash into the message, so six months later somebody can tell that the hotfix on release/2026.08 came from main.

The duplicate-commit problem: cherry-picking a commit and then also merging the branch it came from gives Git two commits with different hashes and identical changes. Usually it copes — git log --cherry-mark --left-right and git patch-id exist precisely to identify equivalent commits — but conflicts here are confusing and common. The rule of thumb: cherry-pick out of a branch you will later merge only when you have a reason, and prefer merging the fix branch into both targets.

Try it yourself: make three commits, note their hashes, then git rebase -i HEAD~3 and merely reword the oldest message. Compare all three hashes afterwards. All three changed — including the two you did not touch. That single observation explains every rule in this lesson.

Common mistake: rebasing a branch that has already been reviewed, mid-review, and force-pushing. Every review comment is now attached to commits that no longer exist, the reviewer’s “what changed since I last looked” view is destroyed, and they have to start over. Add fixup commits during review; squash them at the end, once, with --autosquash.