Merge strategies
Intermediate
Why this matters
The merge button often offers three options, and the choice is usually the project's rather than yours. Knowing what each does explains why some projects have a tidy list of changes on main and others have a thicket, and it tells you how much your intermediate commits matter.
The same branch, three ways
A branch with three commits, one of which is "fix typo", alongside one unrelated commit on main.
Merge commit
* 7b783fd Merge branch 'docs/12-windows-install-steps'
|\
| * a0e02c8 fix typo
| * df2d30f docs: mention PowerShell
| * 7ebba71 docs: add Windows section
* | 60e7ee5 docs: add a support section to the README
|/
* d3b92ec chore: add CI configuration and contributing guideEverything is preserved, including the fact that a branch existed and when it was merged. The cost is noise: three commits and a merge commit for one change, and main's history becomes hard to skim on a busy project.
Squash and merge
* 9c5b252 docs: add Windows steps to installation guide (#12)
* 60e7ee5 docs: add a support section to the README
* d3b92ec chore: add CI configuration and contributing guideOne commit per merge request. main becomes a readable list of changes, and "fix typo" never reaches it. The cost is that the intermediate history is gone: if the branch contained a deliberate sequence of steps, that sequence is lost, and git bisect gets a coarser granularity.
Rebase and merge
* f361a7a fix typo
* 6da6d0c docs: mention PowerShell
* d177301 docs: add Windows section
* 60e7ee5 docs: add a support section to the READMEEvery commit is kept, replayed on top of main with new hashes, and there is no merge commit. A straight line, which reads well and hides that a branch existed. The cost is the rewriting: the commits on main are not the commits that were reviewed, and any that were half-finished are now permanently in the main history.
Choosing
| Priority | Strategy |
|---|---|
| An honest record of how work happened | Merge commit |
A readable main, one line per change |
Squash |
| A straight history with every commit kept | Rebase and merge |
| A change that genuinely needs its steps preserved | Merge commit, or rebase |
Squash is the most common default on both platforms, and it is a good default for a team where branches accumulate "wip" and "fix typo" commits, which is most teams.
Fast-forward, and why some projects insist on it
When main has not moved since your branch started, Git can integrate by simply moving the pointer forward: a fast-forward, with no merge commit at all (lesson 6.6). Projects that require a linear history enable "fast-forward only", which means your branch must be rebased onto current main before it can merge. That is what the Update branch or Rebase button on the merge request is for.
$ git switch docs/12-windows-install-steps
$ git rebase main
$ git push --force-with-leaseForcing here is expected and safe: it is your own branch (lesson 12.8).
Reading what a project does
$ git log --graph --oneline -20 main- Merge commits with
|\shapes: merge commits are used. - A straight line with one commit per merge request, each ending in
(#12): squash. - A straight line with several related commits in sequence and no merge commits: rebase and merge.
The project's settings say so too: Settings → Merge requests on GitLab, Settings → General → Pull Requests on GitHub, where the available buttons are chosen.
How to do it
Locally, the three shapes are three commands:
$ git merge --no-ff docs/12-windows-install-steps # always a merge commit
$ git merge --squash docs/12-windows-install-steps # stage everything, then commit once
$ git rebase main # replay, then fast-forward mergeIn practice you rarely run these on main: the platform's merge button does it. They are useful for understanding what the button will do.
Merging happens through the platform extension's merge button. Locally, Git: Merge Branch performs an ordinary merge; there is no squash option in the interface.
Git → Merge offers No fast forward, Squash commit and other options in a dropdown, which makes it the easiest tool for trying the three shapes and seeing the result in the log.
Settings → Merge requests chooses the method: Merge commit, Merge commit with semi-linear history, or Fast-forward merge, plus a separate Squash commits setting that can be off, optional, or required. The merge button then shows what is allowed.
Settings → General → Pull Requests enables Allow merge commits, Allow squash merging and Allow rebase merging independently. Whichever are enabled appear in the merge button's dropdown.
Common mistakes
- Accepting the default squash message, which is often a list of "wip" commits and lives on
mainforever. - Assuming your intermediate commits will be preserved. On a squashing project they will not, so put the explanation in the merge request description.
- Rebasing a shared branch to satisfy a linear-history rule (lesson 12.8).
- Arguing about the strategy on your first week. Every option here is defensible; the team has chosen.
- Forgetting to tick "delete source branch", whichever strategy is used.
Try it yourself
Goal: produce all three histories from the same starting point.
- In your practice repository, create a branch with three commits, one of them called "fix typo", and add one unrelated commit to
main. - Copy the whole repository folder twice, so you have three identical copies.
- In the first,
git merge --no-ff <branch>. In the second,git merge --squash <branch>then commit. In the third, rebase the branch ontomainand merge fast-forward. - Run
git log --graph --oneline -6in each and compare. - Decide which of the three you would want to read a year from now.
Expected result: three histories with identical files and visibly different shapes, matching the three outputs in this lesson.
Show solution
Copying the folder in step 2 is the trick that makes this comparison possible, and it is a useful habit in its own right: a repository is just a directory, so a copy is a complete backup that lets you try anything.