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.