Engineering
Demystifying git rebase
Rebase scares people because the usual one-line explanation
Rebase scares people because the usual one-line explanation, "it moves your commits onto a new base," is wrong in a way that matters. Rebase doesn't move anything. It copies your commits into brand-new commits with different hashes, then abandons the originals. Once you see it as copy-and-replay rather than move, every confusing thing about rebase, including why it's dangerous on shared branches and why the reflog can always save you, falls into place.
What happens
Say you branched feature off main, did some work, and meanwhile main got new commits from other people:
A---B---C feature
/
D---E---F---G main
Your feature commits (A, B, C) are sitting on top of E, an old version of main. You'd like them on top of G instead, where main is now. So from feature you run:
git rebase main
Here's the result:
A'--B'--C' feature
/
D---E---F---G main
Look closely at the labels. The commits are A', B', C', not A, B, C. Git took the changes from each of your original commits and applied them, one at a time, on top of G, creating a new commit for each. Same diffs, same messages, different parents, and therefore different hashes. A, B, and C still exist in the object database for now, but nothing points to them anymore. They're orphaned.
That's the entire mechanism. Rebase replays a sequence of patches onto a new base and gives you new commits. The word "move" hides the one fact you most need to know, which is that the commits are not the same objects afterward.
Why the new hashes matter
A commit's hash is computed from its content and its parent. Change the parent and you change the hash, even if the diff is identical. This single fact explains the two things people find mysterious about rebase.
First, it's why rebased history is linear and clean:
gitGraph
commit id: "D"
commit id: "E"
commit id: "F"
commit id: "G"
commit id: "A'"
commit id: "B'"
commit id: "C'"
No fork, no merge commit, just one straight line that reads as though you'd done your work after everyone else's, in order. That readability is the whole appeal.
Second, it's why rebase is dangerous on shared branches, which is the next part and the one rule you can't break.
The one rule: don't rebase shared history
If anyone else has based work on the commits you're about to rebase, rewriting them is a problem. You're replacing A, B, C with A', B', C', and your teammate's repo still references the originals. When they next pull, git sees two divergent histories that contain the same changes twice, and the result is duplicated commits, confusing conflicts, and a bad afternoon for everyone.
The rule that keeps you safe is simple: rebase commits that live only on your machine, and never rebase commits you've already shared and others have built on. A feature branch only you touch, rebase it freely to keep it current. The team's main, never. When you do rewrite a branch you've already pushed, you have to force-push, and you should use the safe form:
git push --force-with-lease
--force-with-lease refuses the push if the remote moved since you last fetched, so you can't clobber a commit a teammate pushed while you were rebasing. Plain --force has no such guard. Treat the lease version as the only one you type.
Interactive rebase, where rebase earns its reputation
The everyday power of rebase isn't moving a branch, it's cleaning up your own messy history before review. Interactive rebase lets you reorder, combine, edit, and drop commits:
git rebase -i HEAD~5
That opens an editor listing your last five commits, each with a verb in front:
pick a1b2c3 Add login form
pick d4e5f6 fix typo
pick 7890ab fix typo again
pick cdef01 Wire up validation
pick 234567 oops forgot a file
Change the verbs and git replays accordingly. The two you'll use constantly:
squash(ors) folds a commit into the one above it and lets you combine the messages.fixup(orf) does the same but discards the squashed commit's message, which is perfect for those "fix typo" and "forgot a file" commits that should never have been separate.
Rewrite that list so the typo fixes fold into the real commits, and five scattered commits become two clean ones that tell an honest story. This is what people mean when they say they rebase before opening a pull request. They're not moving branches around, they're editing their history into something a reviewer can follow.
You can't really lose work
The thing that makes rebase safe to learn is that the originals don't vanish. Remember A, B, C becoming orphaned, not deleted. The reflog still knows where they were:
git reflog
git reset --hard <sha-before-the-rebase>
The reflog records every position HEAD held, including the spot right before you started the rebase. If a rebase goes wrong, find that line, reset to it, and you're back exactly where you started as if nothing happened. Git keeps those orphaned commits around for weeks before garbage collection touches them, which is a long time to change your mind.
So the demystified version is this. Rebase copies your commits onto a new base as new objects with new hashes, which makes history linear and which is exactly why you must not do it to commits other people share. Use it to keep a private branch current and to tidy your own commits before review. And when it bites you, the reflog has your back. Once "move" becomes "copy and replay," rebase stops being scary and starts being the tool you reach for to make your history mean something.