CodeOath
← All posts
Git65 min total · 17 parts

Git Internals and Workflows: Branching, Merging, and Rebasing

Contents — Part 4 of 17: Merge vs. Rebase: Same Two Branches, Different History
Part 4 of 17 · ~2 min

Merge vs. Rebase: Same Two Branches, Different History

Diagram comparing git merge and git rebase producing different commit histories from the same two branches

Both answer the same question — "bring feature's changes into main" — but they record the answer completely differently:

  • git merge feature (run from main) creates a new merge commit with two parents, joining both histories exactly as they happened. You can always see when and how the branches diverged and rejoined, because that information is preserved permanently in the graph.
  • git rebase main (run from feature) rewrites feature's commits one by one on top of main's latest commit, producing a straight line — as if you'd started the feature branch now instead of when you actually did. The original commits are replaced with brand-new ones (different hashes, even though the actual code changes are identical), and the old commits become unreachable from feature once the rebase completes.
# Merge: preserves exact history, adds a merge commit
git switch main
git merge feature

# Rebase: rewrites feature's commits onto the tip of main
git switch feature
git rebase main
MergeRebase
History shapeBranching, with merge commits showing where things joinedLinear — looks as if development happened sequentially
Original commit hashesPreservedReplaced (new hashes, same content)
Safe on already-pushed/shared commitsYesNo — see the next chapter
git log --graph readabilityShows true concurrent developmentReads as if there was never more than one line of work
Typical useIntegrating a finished feature branch into mainCleaning up your own local commits before sharing them, or keeping a long-lived branch current with main without a merge commit each time

Neither is universally "correct" — many teams use rebase for keeping a personal feature branch current with main during development, then merge (often with --no-ff to force a merge commit even when a fast-forward would be possible) when the feature is actually done, to leave a clear record in history of when it landed.