Rebase and Merge: Linear History Without Merge Commits
Rebase-and-merge integrates a branch by replaying each of its commits onto the tip of the target, then moving the target’s ref forward. The result is a linear history in which every commit from the branch appears individually, with no merge commit.
It sits between the other two methods: merge commits preserve everything including the branch structure, squashing preserves only the net change, and rebase-and-merge preserves every commit while discarding the structure.
What it produces
Section titled “What it produces”A trunk lane labelled main with commits A, B and C. A branch lane labelled feature leaves main after commit A with commits X and Y.
A single lane containing commits A, B, C, then X-prime and Y-prime, with both the main and feature labels at the end. The primed commits are the replayed versions of X and Y and are drawn in the accent colour to show they are new objects.
The prime marks matter. X' is not X. It contains the same changes and the same message, but its
parent is different — and because a commit’s ID is a hash of its content including its parent, a
different parent means a different ID.
Doing it locally
Section titled “Doing it locally”Rebase-and-merge is two operations:
-
Rebase the branch onto the target.
Terminal window git switch featuregit fetch origingit rebase origin/mainEach commit is replayed. Conflicts, if any, are resolved one commit at a time.
-
Fast-forward the target.
Terminal window git switch maingit merge --ff-only featureBecause the branch now descends directly from
main’s tip, this is a fast-forward — no merge commit is possible.
--ff-only is the right flag here. If it refuses, something moved since you rebased and you should rebase
again rather than silently creating a merge commit.
The platform button
Section titled “The platform button”Most teams using this method never run the commands — they press “Rebase and merge”. The platform performs
the equivalent server-side, and there are two documented differences from native git rebase that matter.
It always creates new commits. GitHub’s implementation always updates committer information and
produces new commit SHAs. Native git rebase can preserve committer data when replaying commits onto an
ancestor; the platform does not take that shortcut. In practice: after a platform rebase-and-merge, the
commits on main are always new objects, without exception.
It drops commits that were empty to begin with. An empty commit on the branch will not appear on the target.
git rebase locally | Platform rebase-and-merge | |
|---|---|---|
| New commit IDs | Yes | Always |
| Committer metadata | May be preserved | Always updated |
| Empty commits | Preserved unless told otherwise | Dropped |
| Conflict resolution | Interactive, on your machine | Not possible — the button is disabled if it would conflict |
| Force push required | Yes, on your branch | No — the server does it |
That last row is why teams like the button: the messy part of rebasing, the force push, never touches
anyone’s local repository. The branch on the remote is left as it was, and only main moves.
What you gain
Section titled “What you gain”Linear history. git log main reads as a single sequence. No graph, no interleaving, no
--first-parent needed.
Every commit preserved. Unlike squashing, a branch’s internal structure survives — the refactor, the behaviour change and the tests remain separate commits.
Fine-grained bisect. git bisect can isolate the exact commit that introduced a problem rather than
the whole branch.
Useful git blame. Each line traces to the commit that actually introduced it, with its own message.
No merge commits. For teams that consider them noise, this removes them entirely while keeping detail.
What you lose
Section titled “What you lose”Every commit ID changes. Any reference to a branch commit — in a review comment, a ticket, a chat
message, another branch — now points at an object that is not on main.
Commits are never tested in their final form. Each replayed commit is a new object whose parent is
different from when CI ran. CI tested the branch tip against main; it did not test X' in isolation.
Intermediate commits on main may not build even though the final state does.
No record of integration. Nothing says when the branch was merged, or that a branch existed at all.
Conflicts can repeat. A rebase replays commits one at a time, so a conflict in an early commit may recur in later ones. A merge resolves each conflict once.
Stacked branches break. Any branch based on the original commits now shares no history with main.
See Squash Merging — the same problem,
same fix.
git branch --merged will not see it. The original branch tip is unreachable from main, so -d
refuses.
Rebase-before-merge is a different thing
Section titled “Rebase-before-merge is a different thing”Two workflows get called “rebase and merge”, and they produce different histories.
Rebase, then fast-forward. What this lesson has described. The branch’s commits land on main
individually; no merge commit. Linear.
Rebase, then merge with --no-ff. Rebase the branch so it sits directly on main’s tip, then merge
it with an explicit merge commit. You get a nearly linear history in which each branch is still visible
as a unit.
git switch featuregit rebase maingit switch maingit merge --no-ff featureA trunk lane labelled main with commits A, B, C and a merge commit M. A branch lane labelled feature leaves main after commit C with rebased commits X-prime and Y-prime, which merge into M. The branch commits sit directly on top of C rather than diverging earlier.
This combination is popular with teams that want both properties: history that reads in order, and a record of which commits belonged to which branch. The cost is one merge commit per branch, and the rebase still rewrites the branch.
Hosting platforms do not usually offer this as a button — “Rebase and merge” means the first form. Teams that want the second normally require branches to be up to date (which forces the rebase) and then use the merge-commit button.
Keeping a branch rebased while you work
Section titled “Keeping a branch rebased while you work”Rebase-and-merge works best when the branch is already close to main. Teams using it tend to rebase
during development rather than only at integration:
git switch featuregit fetch origingit rebase origin/maingit push --force-with-leaseDone every day or two on a private branch, this keeps conflicts small and means the final integration is a trivial fast-forward. Done on a branch under active review, it invalidates review comments and irritates reviewers.
The practical rule most teams settle on:
- Before review opens: rebase freely. The branch is yours.
- During review: add commits. Do not rewrite.
- After approval, before merge: rebase if needed to bring it current — reviewers have finished.
What happens to the pull request
Section titled “What happens to the pull request”After a platform rebase-and-merge, the state can look confusing:
- The pull request shows as merged. The platform records it, independent of Git reachability.
- Your local branch still has the original commits. Nothing rewrote it; the server rebased its own
copy.
git logwill show your branch as diverged frommain. git branch -drefuses, for the same reachability reason as squashing.- The remote branch is usually deleted if auto-deletion is enabled.
The cleanup is straightforward once you expect it:
git switch maingit pullgit branch -D featureUse -D deliberately here, having confirmed the change is on main — this is one of the few routine
cases where the capital-D force delete is the correct tool rather than a shortcut.
Comparing all three methods
Section titled “Comparing all three methods”| Merge commit | Squash | Rebase and merge | |
|---|---|---|---|
| Commits added to target | All, plus a merge commit | One | One per branch commit |
| New commit IDs | No | Yes | Yes, all |
| History shape | Graph | Linear | Linear |
| Integration recorded | Yes | No | No |
| Branch structure visible | Yes | No | No |
git bisect granularity | Per commit | Per branch | Per commit |
git blame usefulness | Full | Coarse | Full |
| Revert whole change | git revert -m 1 | One revert | Several reverts |
git branch --merged sees it | Yes | No | No |
| Conflicts resolved | Once | Once | Possibly repeatedly |
| Suits | Meaningful branches, audit needs | Short branches, messy commits | Clean commits, linear preference |
Which method should a repository allow?
Section titled “Which method should a repository allow?”Most hosting platforms let you enable or disable each merge method per repository. Allowing all three and
leaving the choice to whoever presses the button produces an inconsistent main, which is worse than any
single consistent choice.
Three defensible configurations:
Squash only. The simplest to operate. Every change is one commit, history is linear, and nobody has to understand rebase. Suits teams with short branches and mixed experience levels.
Rebase and merge only. Linear history with full granularity. Requires branch commits to be worth keeping, which in turn requires contributors to tidy branches before review.
Merge commits only. Every branch visible, integration recorded, nothing rewritten. Suits audit-sensitive contexts and teams that stack branches.
Enabling squash and rebase together is reasonable if the team agrees on when each applies — for example, squash for small fixes and rebase for structured feature branches. Enabling all three without a rule generally means the default button gets pressed and the rule is whatever that happens to be.
When to choose it
Section titled “When to choose it”Good fit when:
- Commits within branches are individually meaningful and well-formed.
- Your team wants linear history without losing granularity.
git blameand fine-grainedgit bisectmatter.- Branches are short and conflicts are rare, so repeated conflict resolution is not a burden.
- Contributors are comfortable with rebase and force-with-lease.
Poor fit when:
- Branch commits are working notes — squashing is better.
- You need integration recorded for audit — merge commits are better.
- Branches are long-lived, so rebasing means many conflict rounds.
- Your team stacks branches routinely.
- Contributors are unfamiliar with rebase and the force-push failure mode.
Common mistakes
Section titled “Common mistakes”Rebasing a shared branch. The most consequential error. Everyone else’s copy is now orphaned.
Using bare --force. --force-with-lease refuses when the remote has moved, which is exactly the
protection you want. Bare --force overwrites regardless, discarding a colleague’s push silently.
Assuming the platform button rebases your local branch. It does not. Your local branch still has the original commits and will look diverged. Reset it or delete it:
git switch main && git pullgit branch -D featureExpecting git branch --merged to list it. It will not. Verify the change landed, then use -D.
Rebasing after review has started. Reviewers’ comments are anchored to commits that no longer exist. Add commits during review; reshape before it starts or after it finishes.
Choosing it for linear history without considering squash. If the branch commits are not worth keeping, squashing gives linear history with less complexity and no conflict repetition.
Mental Model
Section titled “Mental Model”Rebase-and-merge asks: what if this branch had been written on top of current
mainall along?Git answers by rewriting it that way — same changes, same messages, same authors, new commits with new parents. The branch’s content is preserved exactly; only its position in the graph changes, and position is part of a commit’s identity.
What You Learned
Section titled “What You Learned”- Rebase-and-merge replays each branch commit onto the target, then fast-forwards — no merge commit.
- Every replayed commit is a new object with a new ID, because the parent changed.
- Author is preserved; committer is updated.
- The platform button always creates new SHAs and always updates committer info, and drops empty commits.
- It gives linear history with full granularity, at the cost of integration records and stacked branches.
- Rebasing a shared branch breaks everyone else’s copy;
--force-with-leaseis the safer push. git branch --mergedcannot see a rebase-integrated branch.
Try It Yourself
Section titled “Try It Yourself”- Create a repository with a commit on
main. - Create
featureand add two commits. Note their short IDs:git log --oneline feature. - Switch to
mainand add one commit so the branches diverge. - Rebase:
git switch feature && git rebase main. - Compare
git log --oneline featurewith the IDs from step 2. Every one should differ. - Fast-forward
main:git switch main && git merge --ff-only feature. - Confirm the history is linear:
git log --oneline --graph. - Confirm no merge commit:
git cat-file -p HEAD | grep -c parentprints1. - Run
git reflogand find the original commit IDs from step 2 — they still exist.
Step 5 is the lesson. Step 9 is the reassurance: rewriting does not destroy the originals immediately, which is why recovery is usually possible.
Next Lesson
Section titled “Next Lesson”Whichever method you choose, sooner or later two changes will genuinely conflict. The next lesson is the practical guide to resolving them.