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
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:
| Merge | Rebase | |
|---|---|---|
| History | What actually happened | What it would look like if written last |
| Shape | Branching graph | Linear |
| Hashes | Preserved | Rewritten |
| Safe on shared branches | Yes | No |
| Conflicts | Once, at the merge | Potentially once per commit |
git bisect, git revert | Slightly harder on a merge | Simpler 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
| Verb | Effect |
|---|---|
pick | Keep as is |
reword | Keep the change, edit the message |
edit | Pause here; amend the commit, or split it |
squash | Combine into the previous commit, open a combined message |
fixup | Combine into the previous commit, discard this message |
drop | Remove the commit |
exec | Run a shell command at this point |
break | Stop 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.