Rebase: Reshaping History
This chapter covers rebase: linearizing history, squashing work-in-progress commits, and reordering what you shipped.
What Rebase Does
git rebase takes a sequence of commits and replays them on top of a different base. Same changes, different parents.
Before:
A --- B --- X --- Y (main)
\
C --- D (feature/login)
After git rebase main on feature/login:
A --- B --- X --- Y (main)
\
C' --- D' (feature/login)
C' and D' are new commits with the same content as C and D but different parents (and different hashes). The originals are still in the reflog but no branch points at them.
Why Rebase
Two reasons.
Keep history linear. Merges create forks in the graph. Rebases make history read like a list. Some teams care; some don't. Chapter 10 covers the tradeoffs.
Clean up your work before sharing. You made five "WIP", "fix", "oops" commits while figuring out a feature. Before opening a PR, you rebase interactively to collapse them into one or two meaningful commits. The review is cleaner. The permanent history is readable.
Basic Rebase
Update a feature branch to the latest main:
git switch feature/login
git fetch origin
git rebase origin/main
If there are no conflicts, git moves your commits onto the new base and you're done. If there are, git stops at the first conflict; see Chapter 7.
During a conflict:
git rebase --continue # after fixing and staging
git rebase --abort # give up, go back to before the rebase
git rebase --skip # drop the current commit
--abort is safe. It puts the branch exactly where it was before git rebase started. Use it whenever a rebase gets hairy.
Interactive Rebase
The serious tool. git rebase -i <base> opens your editor with a list of commits:
git rebase -i HEAD~5
pick 3f1a22c first stab at login form
pick 4a5b6c7 wip
pick 7d8e9f0 fix typo
pick 9b2f11a WIP
pick a1b2c3d finally done
Edit this list before saving. Each line is a command:
pick use the commit as-is
reword use the commit, but let me edit the message
edit stop for edits to the commit's contents
squash meld into the previous commit, edit combined message
fixup meld into the previous commit, discard this commit's message
drop remove the commit
exec run a shell command here (e.g. exec npm test)
Rewritten example:
pick 3f1a22c first stab at login form
fixup 4a5b6c7
fixup 7d8e9f0
fixup 9b2f11a
reword a1b2c3d Add login form and validation
Save and close. Git replays the commits in the new order, collapsing the fixups into the first pick, and opening the editor for the reworded message. Result: one commit with a clear message, the history you actually wanted.
Squashing a Branch
Before opening a PR, you might want your whole branch as a single commit:
git rebase -i main
Mark the first commit pick and every other commit fixup:
pick 3f1a22c initial login work
fixup 4a5b6c7
fixup 7d8e9f0
fixup a1b2c3d
One commit, clean message, ready to share.
A shortcut, if you don't care about the intermediate messages at all:
git reset --soft main
git commit -m "Add login form and validation"
That softly resets to main (keeping your changes staged) and creates one commit with everything.
Editing a Commit
Mark a commit edit during interactive rebase. Git pauses there:
# edit the files, then
git add .
git commit --amend
git rebase --continue
You can also split a commit into two:
pick 3f1a22c big messy commit
Mark it edit. When git pauses:
git reset HEAD~ # undo the commit, keep changes in the working tree
git add path/one
git commit -m "part one"
git add path/two
git commit -m "part two"
git rebase --continue
One messy commit becomes two clean ones.
exec for Per-Commit Tests
Run a command after every commit during a rebase:
git rebase -i --exec "npm test" HEAD~5
Git applies each commit, then runs npm test. If tests fail, it stops so you can fix the commit. Great for guaranteeing every commit builds, which keeps git bisect useful later.
The Golden Rule
Don't rebase commits that others may have based work on.
If you rebase commits you've pushed to a shared branch and force-push, collaborators who pulled those commits now have diverged history. Sorting it out is painful.
Safe to rebase:
- Local commits that have never been pushed.
- Commits on a branch only you are using.
- Commits on a PR branch your team has agreed to squash-merge (nobody's building on it).
Unsafe:
- Anything on
main. - Shared long-lived branches like
developorrelease/*.
When in doubt, ask before force-pushing.
Rebase onto Another Branch
git rebase --onto is the hidden gem. Replay a range of commits on an arbitrary base.
You branched off feature/login by accident, then realized your work should have been off main:
A --- B (main)
\
L1 --- L2 (feature/login)
\
X --- Y (feature/login-extras)
You want X and Y on main, not on feature/login:
git rebase --onto main feature/login feature/login-extras
That's "take commits from feature/login to feature/login-extras, and replay them starting from main". Result:
A --- B (main)
\
L1 --- L2 (feature/login)
\
X' --- Y' (feature/login-extras)
--onto reads like an arcane incantation, but it's just "pick this range and put it on that base".
Common Pitfalls
Rebasing a public branch and force-pushing. Breaks collaborators. Use squash-merge in the PR instead, or coordinate the force-push.
Rebasing with uncommitted changes. Git refuses. Stash or commit first.
Losing work during a rebase. If something goes sideways, git rebase --abort sends you back. If you finished the rebase and then realized it was wrong, the reflog has the old tip: git reflog to find the hash, git reset --hard <hash> to go back (Chapter 8).
Editing the pick list wrong. Deleting a pick line drops the commit. If you meant to keep it, abort and start over.
Endless conflict loops. If the same conflict keeps appearing during a long rebase, consider git rebase --abort and merging instead. A merge solves the conflict once; a rebase solves it at every intermediate commit.
Next Steps
Continue to 07-conflicts.md to handle the merges that don't resolve themselves.