Community Article

Git Rebase vs Merge: The Team Conversation to Have Once

Why your team needs one git workflow rule, the trade-offs each strategy hides, and the configuration I now ship to every new repo.

Git Rebase vs Merge: The Team Conversation to Have Once

Why your team needs one git workflow rule, the trade-offs each strategy hides, and the configuration I now ship to every new repo.

craftsmanship
collaboration
code-organization
teamwork
milozhang

By @milozhang

May 8, 2026

·

Updated May 20, 2026

385 views

3

4.2 (14)

"Why are there 47 merge commits in this PR?"

That was the message that started a forty-five minute call with three engineers and ended with the team's first written git policy. Until that call, our team's strategy was "do whatever your last team did". Two of us rebased before pushing. One of us merged main into our branch every morning. One had a graphical client that did something different on every commit. The git history looked like a spider web. Bisecting a regression was a small adventure. The forty-seven merge commits in the PR were the natural result of nobody having said out loud what to do with main while a feature branch was open.

This article is the conversation I now have on day one of every new team, the trade-offs each strategy actually has (not the ones blog posts shout about), and the small set of configuration choices I now make a non-negotiable on every new repo. The goal is not to convert anyone to one camp. The goal is to make the choice once, write it down, and never relitigate.

What rebase and merge actually do

A quick refresher because the words get used loosely. When feature is two commits ahead of main and main has moved one commit forward since feature branched off:

State before integration

  main:    A --- B --- M
  feature:        \
                   C --- D

Merge creates a new commit that has two parents (M and D), preserving the divergent history. The graph keeps the diamond.

After merging feature into main

  main:    A --- B --- M --------- N
                       \          /
  feature:               C --- D

Rebase rewrites C and D so they sit on top of M, creating new commits C' and D' with new hashes but the same content. The graph becomes linear; the original C and D no longer exist.

After rebasing feature onto main, then fast-forwarding

  main:    A --- B --- M --- C' --- D'

The two histories carry the same code on disk after integration. They do not carry the same metadata. That metadata is what the rest of the article is about.

What you give up with each

The internet has long made up its mind about which is better. The internet is wrong, in that the answer depends on what your team uses git history for. Three real questions decide the trade-off.

Question 1: How readable do you need history to be six months from now? Linear history is dramatically easier to read with git log --oneline. Each commit is one logical change, in order, and the graph is a straight line. Merge-heavy history is a mess to read with --oneline and only legible with --graph. Teams that read history often (for example, before a release, before a postmortem, before a database migration) benefit from linear. Teams that never look at history beyond the last week do not care.

Question 2: How important is preserving the actual order in which work happened? Merge preserves it. Rebase rewrites it. If you ever need to know "which commit was on the team's main branch on Tuesday at 3pm", merge gives you that information accurately and rebase gives you a polished retelling. For most product teams this is a non-issue. For teams that need precise audit trails (think regulated industries, certain financial systems, anything with SOC 2 history requirements), the difference matters.

Question 3: Are commits ever pushed to a shared branch and then rewritten? This is the only place where rebase has a true gotcha. Rewriting commits that other people have already pulled creates orphaned commits in their local checkouts and confusing force-push errors. The rule that protects you: rebase your own private branch all you want; never rebase a branch others are working on. If you have ever had to talk a coworker through git reflog to recover commits they thought they had lost, you know why this rule exists.

The strategy I now ship

After the forty-seven-merge-commit incident, the team adopted this written policy. We have not changed it in three years. Every team I have advised since has ended up close to the same shape.

1. Feature branches rebase onto main, never merge from main. When my feature branch falls behind main, I rebase, not merge. This keeps the eventual PR diff scoped to the actual feature, not "feature plus three weeks of unrelated main commits". The reviewer reads exactly the commits I wrote.

2. The merge into main is a squash-merge or a single rebase-merge. GitHub's "Squash and merge" or "Rebase and merge" buttons. Never the regular merge commit option. This means main's history has one commit per PR, with the PR title as the commit message. Future-me, debugging on main, can read main's log as a feature stream rather than fighting through every WIP commit the author made along the way.

3. The PR commits, while in flight, can look like anything. I do not care if my feature branch has 12 fixup commits with messages like "oops" and "refactor again". Once it is squashed into main, those become one clean commit and the noise is gone. This decouples the author's local development experience from the project's historical experience.

4. Force-pushes on personal branches are fine; force-pushes on shared branches are forbidden. GitHub branch protection rules enforce this on main. The team trusts each other on personal branches.

This policy is opinionated. It is not the only correct policy. It is, however, a single policy, and that is the part that makes the team's git life calm. The pre-policy chaos of "do whatever" is gone, and the bisects, blames, and reverts on main work the way the documentation says they should.

The five commands I actually run

After the policy, the day-to-day git surface for me has shrunk to about five commands. I have not memorized any of the ten advanced ones I used to know.

git fetch origin                              # always start here
git checkout -b feature-x origin/main         # branch from latest main
# ...code, commit, repeat...
git fetch origin && git rebase origin/main    # bring my branch up to date
git push --force-with-lease                   # safe force push to my branch
# open PR, review, address feedback...
# in GitHub UI: "Squash and merge" button

The --force-with-lease flag is the one I want to call out. A plain --force will overwrite remote commits even if someone else pushed to the same branch (which on a personal branch should never happen, but "should" is doing a lot of work in that sentence). --force-with-lease only succeeds if the remote tip is what I last fetched. If a coworker has pushed to my branch (e.g. to fix a typo before approving), --force-with-lease refuses and I get to find out without losing their commit. I have had this save me from deleting a coworker's commit at least three times.

What about merge conflicts during rebase?

The one objection to rebase that I find most credible is that conflict resolution during a long rebase is genuinely worse than during a single merge. If my branch has 8 commits and main has moved by 30 commits, a rebase makes me resolve conflicts up to 8 times (once per commit), and each one is in the context of a partial history. A merge makes me resolve them once, at the end.

My honest take: this matters less than people say, because the policy from the previous section pushes me to rebase often (every morning, ideally) so that my branch is rarely far behind. If I am rebasing 1-3 times against a single new commit in main, conflicts are rare and small. If I am rebasing 8 commits against 30 new ones at the end of a two-week branch, the pain is real, and the better answer is "do not let your branch live for two weeks".

The second-best answer when you do find yourself in a long rebase: git rebase -i and squash your branch's 8 commits into 1 first, then rebase that single commit onto main. You resolve conflicts once, and the original 8 commits' messages can be combined into one. This is the move most rebase haters do not know exists.

The repo configuration I now ship on day one

This is the .gitconfig and branch protection setup I now drop into every new repo. It is short.

# Repository-local git config
git config pull.rebase true                   # default `git pull` to rebase, not merge
git config rebase.autoStash true              # auto-stash dirty work during rebase
git config rebase.autoSquash true             # honor `fixup!` commit messages

On GitHub, in branch protection for main:

  • Require a pull request before merging
  • Require status checks to pass (CI)
  • Disable the regular "Create a merge commit" merge option; leave only "Squash and merge" and "Rebase and merge" enabled
  • Forbid force-pushes to main
  • Require linear history

The last one ("require linear history") is the rule that mechanically enforces the policy. The team cannot accidentally create a merge commit on main, because GitHub will refuse the merge.

A quick word on git pull --rebase

The one command I have adopted as a personal default: git pull --rebase (or, equivalently, the pull.rebase=true config above) instead of plain git pull. The default git pull is git fetch && git merge, which creates a tiny merge commit every time you sync. On a busy team this means main's history has hundreds of "Merge branch 'main' of github.com/..." commits authored by anyone who ever pulled. They contain no information. They make the log unreadable.

git pull --rebase does git fetch && git rebase instead, which produces no commit. After several years of this default, my logs are clean and I have never noticed a downside.

Have the conversation

The single highest-leverage move on this whole topic is not picking the "right" answer. It is having the conversation once, with the whole team, writing down what you decided, and pinning it in the team handbook. The forty-seven merge commits in my opening story were not really a git problem. They were a coordination problem. The git policy was the artifact of solving it. Once the artifact existed, the next person who joined the team did not have to relitigate the choice; they read the handbook and were productive on day one.

The wrong choice, made consistently, is better than the right choice made by half the team. Pick a strategy that fits your team's history-reading habits, write it down with the four rules above, configure the repo to enforce it, and move on to problems that are actually about the product.

Back to Articles