Git Rebase vs. Merge: Choose a History Shape Deliberately

Published · Updated · Programming

Rebase and merge are not rival moral systems. They create different commit graphs, and the useful choice depends on who already depends on the history. Choose the shape that makes collaboration, debugging, and recovery boring—not the one that wins an argument in a pull request.

The graph is the important part

Start with a feature branch that diverged from main:

A---B---C  main
     \
      D---E  feature

Both operations integrate feature with the newer work on main; they do not change the desired source tree in some magical, different way. They change the history that explains how it got there.

Merge keeps both lines of development

From feature, merge the current main branch:

git fetch origin
git switch feature
git merge origin/main

If Git needs to combine changes, it creates a merge commit with two parents:

A---B---C---------M  feature
     \           /
      D---E------'

The original commits D and E keep their identities. This is the safe default when the branch is shared, when somebody may have based work on it, or when the integration event itself is useful context. A merge commit says plainly that two lines of work came together here.

Rebase copies your unpublished commits onto a new base

From the same original graph, rebase feature onto the current main:

git fetch origin
git switch feature
git rebase origin/main

Git finds commits on feature that are not already equivalent to commits on the upstream, then replays them one by one. The resulting commits have new parents, timestamps, and object IDs:

A---B---C---D'---E'  feature

That is why rebase rewrites history. D' and E' may contain the same patches as D and E, but they are different commit objects. A linear graph can be easier to read with git log, bisect, or revert, but it is not inherently more honest or more professional.

The rule that prevents most damage

Rebase commits that only you own. Merge commits that other people may already have based work on.

If you rebase a branch you already pushed but nobody else uses, update it with the guarded form:

git push --force-with-lease origin feature

Never reach for plain --force as a reflex. --force-with-lease refuses to overwrite a remote tip that changed since your last fetch, which turns a bad assumption into a visible failure rather than someone else's lost work.

Resolve conflicts in the right context

Both operations can stop for conflicts. During a merge, resolve files, stage them, and finish with git commit. During a rebase, resolve files, stage them, and continue:

git add path/to/resolved-file
git rebase --continue

When the conflict turns out to be a bad plan instead of a hard puzzle, back out:

git rebase --abort
git merge --abort

The correct command is the one for the operation currently in progress. Check git status rather than guessing.

A practical team policy

My preferred default is uncomplicated:

  • Rebase your private feature branch onto the current target before review when it makes the patch easier to understand.
  • Do not rebase a branch after other developers have started building on it.
  • Let the repository's pull-request merge policy determine the final integration shape: merge commit, squash merge, or rebase-and-merge all communicate different things.
  • Keep one logical change per pull request. History tooling cannot rescue a branch that mixes a refactor, a behavior change, and a drive-by cleanup.

Before changing history, use git log --graph --oneline --decorate --all and make sure you can explain what will move. If the remote or upstream itself is wrong, solve that first with the remote-migration workflow. If stale remote names are obscuring the graph, clean those references with Git fetch pruning.

Rebase is a sharp and excellent tool. Merge is not a failure to use it. The decision is whether preserving the existing graph or presenting a rewritten, linear one better serves the people who need to understand the change later.

More practical engineering notes

Slaptijack's Koding Kraken