Every team eventually has the rebase-vs-merge argument. Both commands solve the same problem — combining diverging branches — but they leave very different history behind.
What each command actually does
git merge creates a new commit that ties two branches together, preserving both histories exactly as they happened.
git checkout main
git merge feature/logingit rebase replays your branch's commits on top of another branch, rewriting commit hashes in the process.
git checkout feature/login
git rebase mainThe tradeoff in one sentence
Merge preserves true history at the cost of a messier graph; rebase produces a clean, linear graph at the cost of rewriting history.
| Aspect | Merge | Rebase |
|---|---|---|
| History | Preserves exact history, including a merge commit | Rewrites commit hashes to replay changes linearly |
| Graph shape | Branches and merges — shows real history | Clean, straight line — reads like it happened sequentially |
| Safe on shared branches? | Yes — never rewrites what others have | No — breaks anyone who already pulled the old commits |
| Typical use | Landing a feature branch into main via PR | Cleaning up your own local commits before opening a PR |
When to merge
- Merging a feature branch into
mainvia a pull request — keep the merge commit as a record of when the feature landed. - Any branch that other people have already pulled and built on top of. Rewriting shared history breaks their local branches.
When to rebase
- Cleaning up your own local commits before opening a pull request (
git rebase -i). - Keeping a long-lived feature branch up to date with
mainwithout a "merge main into feature" commit on every sync.
git fetch origin
git rebase origin/mainThe golden rule
Never rebase a branch that others have already pulled from, unless the whole team has agreed to force-push and re-sync. Rebasing rewrites commit SHAs — anyone with the old commits will get conflicting history.
A sane default for most teams
- Rebase locally to keep your own commits clean.
- Merge (often via a "squash and merge" PR) when landing into a shared branch.
That combination gives you a readable personal workflow and a stable, non-destructive shared history.
Rebasing cleanly is one habit; automating the rest of the pre-commit checklist with Git hooks is the complementary one — together they keep history both clean and actually correct before it ever reaches a shared branch.
Squashing commits before opening a PR
git rebase -i isn't just for replaying onto another branch — its most common day-to-day use is cleaning up your own commit history before anyone else reviews it:
git rebase -i HEAD~5This opens an editor listing the last 5 commits, each prefixed pick. Changing a line's prefix to squash (or s) merges that commit into the one above it, letting five "wip", "fix typo", "actually fix it" commits become one clean, reviewable commit before a teammate ever sees the history:
pick a1b2c3d Add login form
squash e4f5g6h wip
squash h7i8j9k fix typo
squash k1l2m3n actually fix it
pick n4o5p6q Add tests
The two pick commits stay separate; the three squash commits collapse into the first pick above them, prompting for a combined commit message. This is entirely local history rewriting — safe on a branch nobody else has pulled yet, which is exactly the golden rule from above.
Rewriting just the last commit
For the narrower case of fixing only the most recent commit (a typo in the message, or a small missed change), git commit --amend is simpler than a full interactive rebase:
git add forgotten-file.ts
git commit --amend --no-edit--no-edit keeps the existing commit message; omit it to open the editor and change the message too. Like any history rewrite, this changes the commit's SHA — the same force-push caveat from the golden rule applies if it's already been pushed.
Reordering and editing older commits
Interactive rebase isn't limited to squashing — changing pick to edit on any commit in the list pauses the rebase right after that commit is applied, letting you amend it (add a forgotten change, split it into two commits) before continuing:
git rebase -i HEAD~5
# change 'pick' to 'edit' on the commit to modify# rebase pauses here, on that commit
git add fix.ts
git commit --amend --no-edit
git rebase --continueReordering the lines in the interactive rebase editor before saving also reorders the commits themselves, replaying them in whatever sequence you arranged — useful for grouping related changes together in the final history even if they weren't made in that order originally.
Common mistakes
- Rebasing a branch that a teammate has already pulled and built on top of. Their local history now conflicts with the rewritten one, and reconciling it is more disruptive than whatever the rebase was meant to clean up.
- Using
git push --forceafter a rebase instead ofgit push --force-with-lease. Plain--forcewill happily overwrite commits a teammate pushed after your last fetch;--force-with-leaserefuses if the remote moved since you last saw it. - Resolving a rebase conflict by blindly accepting "theirs" or "ours" without reading the diff — during a rebase, "ours" and "theirs" are reversed from what most people expect (relative to the target you're rebasing onto, not your branch), which leads to silently reverting the wrong side.
- Rebasing to "clean up history" on a branch that's about to be squash-merged anyway. If the PR merge strategy already squashes to one commit, an interactive rebase to tidy commits first is often wasted effort.
Handling rebase conflicts
- When
git rebasestops on a conflict, resolve the conflicting file(s) exactly as you would in a merge, then stage them withgit add. - Run
git rebase --continueto move to the next commit in the replay. - If a specific commit turns out to be unnecessary once conflicts are resolved,
git rebase --skipdrops just that commit and continues. - If it's gone wrong,
git rebase --abortreturns you to exactly the state before the rebase started — there's no partial-state risk to worry about.
Related reading
- Git Hooks Explained: Automate Your Workflow — shares tags: git, devops (same category).
- CI/CD Pipelines Explained: From Commit to Production — shares tags: devops, git.
- Async Python with asyncio: A Practical Introduction — shares tags: programming.
- Big O Notation Without the Math Panic — shares tags: programming.
- Clean Code Principles That Actually Hold Up in Practice — shares tags: programming.