Engineer's toolkit
Merge, rebase and squash
The three ways to combine a branch into main, what each one does to the commit graph, how to resolve conflicts without guessing, and the one rule about rewriting shared history.
Suppose main and feature both branched from the same commit. Each has new commits now. There are three ways to bring feature into main, and they produce different histories. The choice changes what you can read later, and what you can safely undo.
Concept
Before you start: the merge base is the best common ancestor of two branches. Git compares each branch's changes against that base to combine them.
Walking through it: three outcomes
Arrows point forward in time here. Inside Git, each commit stores a pointer to its parent, so the links point backward.
Fast-forward. If main has not moved since B, merging feature creates no commit. Git moves the main pointer forward to F, and history stays linear.
Merge commit. When both sides have new commits, git merge does a three-way merge. It finds the merge base (B), diffs B to D and B to F, and combines the two sets of changes. The result is a new commit, M, with two parents. Since Git 2.34 the default strategy is ort, which is much faster on large repositories. A merge is honest: the history shows what happened, in parallel. The cost is a graph that can become hard to read.
Rebase. git rebase main, run on feature, takes each commit unique to feature (E and F) and replays it on top of D. That produces new commits, E′ and F′, with new parents, new hashes and possibly different trees. Then main fast-forwards to F′. History is linear and easy to read. The cost is that E′ and F′ are different commits from E and F. Anyone holding the old ones now has a diverged history.
Squash merge. Squash-and-merge, a button on GitHub or git merge --squash, folds all of the branch's changes into one new commit on main. One pull request becomes one commit, which is easy to revert. You lose the branch's intermediate history. Git also stops recognizing the branch as merged, so delete it afterward.
The golden rule
Important
Do not rebase commits that other people have built on. Rebasing your own unpushed or personal-branch commits is fine. Rewriting a shared branch forces everyone else to untangle duplicated commits. When you must force-push your own branch, use git push --force-with-lease. It refuses if the remote moved since you last fetched, so you cannot silently overwrite a teammate's push.
Resolving conflicts without guessing
A conflict means both sides changed the same region relative to the merge base. A few habits make conflicts much less painful:
- Set
git config --global merge.conflictStyle zdiff3. Conflict markers then show the base version too, so you see what each side changed, not only where each side ended up. - Read both sides' messages with
git log --merge -pbefore you edit. - Rebuild and run the tests after you resolve. A clean textual merge can still break the program. For example, one side renames a function while the other adds a new call to the old name.
- For long-lived branches you rebase often, enable
git config --global rerere.enabled true. Git then remembers how you resolved a conflict and reapplies it.
Common mistakes
- Force-pushing a shared branch. Use
--force-with-lease, and only on branches you own. - Squashing away the history you need for bisect. Large squashed commits make it hard to find where a bug came from.
- Rebasing a long-lived branch once a month. Rebase often. Ten small conflicts over a week are easier than one huge one.
Check yourself
- Why does rebasing change the hashes of E and F? Rebase replays each commit onto a new parent, and a commit's hash covers its parent, so the new commits get new hashes.
- When is a squash merge a bad choice? When the branch's commit history matters for later bisecting or review.
Key takeaways
- A fast-forward moves a pointer. A merge commit joins two histories with two parents. A rebase replays commits as new ones on top of the target.
- Squash turns a branch into one commit on
main, which is easy to revert but loses intermediate history. - Never rewrite commits other people use. Use
--force-with-leasewhen you must force-push. - Use
zdiff3andrerereto make conflicts easier to read and to repeat.
Further learning
Next, see how to clean up your own commits and recover from mistakes, in editing history and recovering work.
Related
Refs, HEAD and the reflog
Branches, tags and HEAD are small files that point at commits, and the reflog records every move, which is why almost any committed work can be recovered.
Editing history and recovering work
How interactive rebase, fixup commits and backup branches make your own history clean, and the recovery steps for a botched reset, rebase or deleted branch.
Code review
Author small, reviewable PRs and give reviews that improve both the code and the author — design first, tone that teaches, turnaround that unblocks.