Git Rebase Explained: When to Rebase and When to Merge
A deeper look at git rebase — the mechanics, interactive rebase with squash/fixup/edit/drop, the golden rule of published branches, conflict resolution, and rerere for repeated conflicts.
What you'll learn
- ✓The mechanics of how rebase replays commits, with hashes that change
- ✓Interactive rebase actions: pick, reword, edit, squash, fixup, drop
- ✓The golden rule — never rebase commits that have been shared
- ✓How to resolve conflicts during a rebase and continue or abort
- ✓Using rerere to skip repeated conflict resolutions
Prerequisites
- •You have used both merge and rebase at least once — see Merge vs Rebase
- •You are comfortable resolving merge conflicts
Merge keeps history honest; rebase keeps history readable. Once you have used both, the questions stop being “what does each one do” and start being “when do I use which, and how do I get out of it when something goes wrong.” This post is the practical answer.
Mechanics: what rebase actually does
Suppose you have this:
E---F (feature)
/
A---B---C---D (main)
Running git rebase main from feature does the following, step by step:
- Git identifies the commits unique to
feature— here,EandF. - Git temporarily stores them and resets
featureto point atmain’s tip (D). - Git replays
EontoDas a brand-new commitE'with a new hash but the same change. - Git replays
FontoE'asF'. - If a step conflicts, Git pauses and asks you to resolve.
The result:
E'---F' (feature)
/
A---B---C---D (main)
The new commits E' and F' have different hashes from E and F. The content of the changes is the same; the parent commit is different, so the SHA is different. This is the entire reason for the golden rule below.
Merge vs rebase mechanics, side by side
A quick comparison:
| Operation | Creates new commits? | Changes hashes? | Shape of history |
|---|---|---|---|
merge (fast-forward) | No | No | Linear |
merge (true merge) | Yes — one merge commit | No (for existing commits) | Diamond |
rebase | Yes — replays all | Yes (new hashes) | Linear |
The merge commit in a true merge has two parents; rebased commits have one. That single fact drives most of the trade-offs.
Interactive rebase
The everyday usefulness of rebase is not “put my branch on top of main” — most teams handle that with a pull or a merge. The killer feature is interactive rebase, which lets you rewrite a sequence of commits before sharing them.
git rebase -i HEAD~5
Git opens an editor showing the last five commits, oldest at the top:
pick a1b2c3d Add login form
pick e4f5g6h fix typo
pick i7j8k9l fix typo again
pick m1n2o3p Wire up auth context
pick q4r5s6t WIP — remember to remove console.log
You change the action on each line and save. Git replays the list using your instructions.
The actions
pick— keep the commit as-is. The default.reword(orr) — keep the commit but open the editor to change its message.edit(ore) — pause at this commit so you can amend it (change files, split it, etc.).squash(ors) — combine this commit into the previous one. Git opens an editor to compose a combined message.fixup(orf) — likesquash, but discard this commit’s message and keep the previous one’s. The right choice for “fix typo” follow-ups.drop(ord) — delete the commit entirely. Same effect as deleting the line.
So the example above becomes:
pick a1b2c3d Add login form
fixup e4f5g6h fix typo
fixup i7j8k9l fix typo again
pick m1n2o3p Wire up auth context
drop q4r5s6t WIP — remember to remove console.log
Three commits become two, the typos disappear into the original commit, and the WIP commit is gone. The branch now reads like a story you wrote on purpose.
Reordering
You can also reorder the lines in the editor and Git will replay them in the new order. Useful when two commits are conceptually backwards. Be aware that reordering can introduce conflicts if the commits touch related code.
Try it yourself. In a practice repo, make four commits on a feature branch: “Add header”, “fix typo in header”, “Add footer”, “fix typo in footer”. Run git rebase -i HEAD~4, change the typo commits to fixup, and watch the history collapse to two clean commits.
The golden rule
The single rule you must internalise:
Never rebase commits that you have already shared.
When you rebase, the commit hashes change. If a teammate already pulled your old commits and started building on them, their history now diverges from yours. The next pull/push produces duplicates and ugly merges that take time and trust to untangle.
Practical applications:
- Rebasing your local, unpushed commits to tidy them up — safe.
- Rebasing your private feature branch onto the latest
main— safe. - Rebasing a shared
mainor any branch other people are committing to — never. - Rebasing a branch you have already pushed but only you work on — technically safe, but you will have to force-push, which is its own can of worms (see below).
If you must update a shared branch, merge instead — it is non-destructive.
Force-pushing safely
If you rebase a branch you have already pushed, the remote will reject a normal git push because your history no longer matches. You have to force-push. The safe form is:
git push --force-with-lease
--force-with-lease only overwrites the remote if it is at the commit you last fetched — that is, if no one else pushed to it in the meantime. If someone did, the push fails and you can investigate. Plain git push --force skips this check and will silently clobber your teammate’s work.
Habit: use --force-with-lease always. Treat --force as a hostile command.
Resolving conflicts during a rebase
When rebase hits a conflict, it stops mid-replay:
CONFLICT (content): Merge conflict in src/auth.js
error: could not apply <hash>... Wire up auth context
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
The workflow is the same shape as a merge conflict, but you may resolve the same area multiple times if successive commits touch it. That is normal — each replayed commit is applied independently.
The three options at any pause:
git rebase --continue # after fixing the conflict and `git add`-ing files
git rebase --skip # drop the current commit and move to the next
git rebase --abort # cancel the whole rebase and return to the starting state
--abort is your safety net. If a rebase is going sideways, abort, take a breath, and consider whether merge is the right call instead.
A practical conflict tip
When the same lines conflict in commit after commit, it usually means an unrelated reformatting (whitespace, lint fix) landed on main and your branch is replaying old versions of the same lines. You have two choices:
- Squash your branch first (interactive rebase your branch onto itself) so only one commit needs to be rebased onto
main. One conflict resolution instead of five. - Use
git rerere— the next section.
rerere: remembering conflict resolutions
git rerere (“reuse recorded resolution”) is one of Git’s best-kept secrets. Once enabled, Git remembers how you resolved a given conflict and replays the same resolution if the exact conflict appears again.
Enable it once per repo (or globally):
git config --global rerere.enabled true
That is it. Now when you resolve a conflict during a rebase, Git records the resolution. Next time you rebase the same branch (or a similar branch), Git applies the recorded resolution automatically and tells you so:
Resolved 'src/auth.js' using previous resolution.
You still review the result and git add the file before continuing — rerere does not blindly commit. But the tedious mechanical work of repeating the same resolution is gone. This is especially valuable on long-running feature branches that are rebased onto main repeatedly.
Try it yourself. In a practice repo, enable rerere, then create a conflict between main and a feature branch by editing the same line on both. Rebase, resolve, and complete. Reset both branches, recreate the same conflict, rebase again — Git resolves it for you and prints the “previous resolution” message.
A small worked example
Walk through a realistic flow. You started a payments branch a week ago. Five commits in, you want to update onto main and tidy up before opening a pull request.
# 1. Get the latest main
git fetch origin
# 2. Update your branch on top of it
git switch payments
git rebase origin/main
# (Resolve any conflicts, --continue / --abort as needed.)
# 3. Tidy up: squash WIP commits, drop debug commits
git rebase -i origin/main
# 4. Push the rewritten branch (force-with-lease because hashes changed)
git push --force-with-lease
By the time the pull request is open, your branch is a small number of well-described commits sitting cleanly on top of the latest main. The reviewer sees only your real changes, not a tangle of integration noise.
When to merge instead
Reach for merge — and not rebase — when:
- The branch has been pulled by collaborators and rewriting history would hurt them.
- The history of “this branch existed and was integrated on date X” is worth recording.
- Your team has a merge-only policy (some projects use squash-merge in the host UI and keep
mainlinear that way). - The conflict resolution would be easier as a single merge commit than as a chain of replayed conflicts.
A common professional pattern: rebase locally to keep your branch clean while you work, then squash-merge into main via the GitHub/GitLab UI. You get the linear history without exposing collaborators to hash changes.
A short decision guide
| Situation | Use |
|---|---|
| Tidying up your own unpushed commits | rebase -i |
Catching up your private feature branch with main | rebase |
Integrating a finished feature into main (team has no policy) | either; follow convention |
| Working on a branch with collaborators | merge |
| Already shared the commits — and now you want to “clean them up” | leave them; merge new fixes |
| In doubt | merge |
The two questions that decide it: has this commit been shared with anyone yet, and would changing its hash inconvenience them? If both are no, rebase is fair game.
Recap
You now know:
- Rebase replays commits as new commits with new hashes; merge preserves them under a new merge commit
- Interactive rebase actions: pick, reword, edit, squash, fixup, drop — plus reordering
- The golden rule: never rebase commits that have been shared
- Force-push with
--force-with-lease, never plain--force - During a rebase you can
--continue,--skip, or--abortat any pause git rerereremembers conflict resolutions and reapplies them automatically
Next steps
Cleaning up history is a small subset of the larger discipline of working well with branches and pull requests. The next post in the workflow series puts these tools into a daily routine.
→ Next: A Daily Git Workflow That Scales From Solo to Team
Questions or feedback? Email codeloomdevv@gmail.com.