Git Branching and Merging¶
Fast-forward vs. true merge¶
If the target branch's tip is a direct ancestor of the branch being merged in, Git doesn't need to combine anything — it just moves the target branch's pointer forward.
No new commit is created — main simply now points at the same commit
feature does. This is only possible when main hasn't moved forward
independently since feature branched off. If both branches have new
commits since they diverged, Git creates a merge commit — a commit
with two parents, combining both histories:
The merge commit's tree is computed by a three-way merge: compare
feature's tree, main's tree, and their common ancestor's tree; for
each file, if only one side changed it, take that side's version; if
both changed the same lines, that's a conflict requiring manual
resolution.
Rebase: rewriting history to look linear¶
git rebase main (run while on feature) takes each commit unique to
feature (just C3 here) and replays it on top of main's
current tip — but replaying creates a genuinely new commit object
(C3', a different hash from C3, even though the resulting file
content is identical) with main's tip as its parent instead of the
original C1. The old C3 still exists in the repository (until
garbage collected) but feature now points at C3' — the history
looks like feature was always built directly on top of main's
latest commit, with no merge commit and no visible fork.
When to use which¶
Rebase before merging a feature branch into main produces a clean,
linear history that's easier to read with git log and easier to
bisect when hunting a regression — every commit sits in a single,
unambiguous line. A true merge preserves the actual history (this
work really did happen on a separate branch, in parallel) — valuable
when that context matters, but it makes git log visually noisier
with merge commits and forked lines.
The golden rule of rebasing¶
Never rebase a branch that other people have already pulled from.
Rebase creates new commit objects with different hashes — if someone
else has feature checked out (or has based their own work on it) and
the remote feature branch gets rebased and force-pushed, their local
history now disagrees with the remote's at the object level. Their
next pull either creates a confusing duplicate set of commits or
requires them to manually reset to match the rewritten history. Rebase
freely on a local, not-yet-pushed branch (or one only you use) —
once it's shared, prefer merge, or coordinate explicitly if a rebase
of shared history is genuinely necessary.
Common pitfall¶
Running git rebase main on a feature branch, hitting a conflict,
resolving it, and then being surprised the same conflict reappears
on the next commit being replayed is actually expected behavior, not a
bug — rebase replays commits one at a time, and if multiple
commits touch the same conflicting lines, each one triggers its own
conflict resolution independently, because each is being individually
re-applied against a history it wasn't originally written against.
git rebase --abort at any point returns to the pre-rebase state
cleanly if the conflict resolution gets too tangled to continue.