Skip to content

Rebase and Merge: Linear History Without Merge Commits

Lesson 5 of 7Intermediate11 min readModern Git Workflows · MergingVerified: Git 2.43.0 on Ubuntu 24.04; platform behaviour checked against GitHub's merge-methods documentation

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.

Before: the branch has diverged from main

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.

ABCXYmainfeature
After: every commit preserved, history linear, IDs changed

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.

ABCX'Y'main, featureX' and Y' contain the same changes as X and Y but are new commit objects with new IDs.

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.

Rebase-and-merge is two operations:

  1. Rebase the branch onto the target.

    Terminal window
    git switch feature
    git fetch origin
    git rebase origin/main

    Each commit is replayed. Conflicts, if any, are resolved one commit at a time.

  2. Fast-forward the target.

    Terminal window
    git switch main
    git merge --ff-only feature

    Because 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.

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 locallyPlatform rebase-and-merge
New commit IDsYesAlways
Committer metadataMay be preservedAlways updated
Empty commitsPreserved unless told otherwiseDropped
Conflict resolutionInteractive, on your machineNot possible — the button is disabled if it would conflict
Force push requiredYes, on your branchNo — 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.

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.

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.

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.

Terminal window
git switch feature
git rebase main
git switch main
git merge --no-ff feature
Rebase then --no-ff: linear commits, plus an integration point

A 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.

ABCMX'Y'mainfeatureNo interleaving, because the branch was replayed onto C first. The merge commit records integration.

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.

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:

Terminal window
git switch feature
git fetch origin
git rebase origin/main
git push --force-with-lease

Done 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.

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 log will show your branch as diverged from main.
  • git branch -d refuses, 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:

Terminal window
git switch main
git pull
git branch -D feature

Use -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.

Merge commitSquashRebase and merge
Commits added to targetAll, plus a merge commitOneOne per branch commit
New commit IDsNoYesYes, all
History shapeGraphLinearLinear
Integration recordedYesNoNo
Branch structure visibleYesNoNo
git bisect granularityPer commitPer branchPer commit
git blame usefulnessFullCoarseFull
Revert whole changegit revert -m 1One revertSeveral reverts
git branch --merged sees itYesNoNo
Conflicts resolvedOnceOncePossibly repeatedly
SuitsMeaningful branches, audit needsShort branches, messy commitsClean commits, linear preference

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.

Good fit when:

  • Commits within branches are individually meaningful and well-formed.
  • Your team wants linear history without losing granularity.
  • git blame and fine-grained git bisect matter.
  • 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.

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:

Terminal window
git switch main && git pull
git branch -D feature

Expecting 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.

Rebase-and-merge asks: what if this branch had been written on top of current main all 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.

  • 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-lease is the safer push.
  • git branch --merged cannot see a rebase-integrated branch.
  1. Create a repository with a commit on main.
  2. Create feature and add two commits. Note their short IDs: git log --oneline feature.
  3. Switch to main and add one commit so the branches diverge.
  4. Rebase: git switch feature && git rebase main.
  5. Compare git log --oneline feature with the IDs from step 2. Every one should differ.
  6. Fast-forward main: git switch main && git merge --ff-only feature.
  7. Confirm the history is linear: git log --oneline --graph.
  8. Confirm no merge commit: git cat-file -p HEAD | grep -c parent prints 1.
  9. Run git reflog and 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.

Whichever method you choose, sooner or later two changes will genuinely conflict. The next lesson is the practical guide to resolving them.