TL;DR: A branch is a cheap, isolated copy of your work; a pull request is how it gets back into
main. GitHub gives you three merge buttons — Create a merge commit, Squash and merge, and Rebase and merge — and they match three real ways of shaping work: many small commits, a few large logical commits, or one clean squashed commit per feature. Pick one default for the repo, and let the shape of the work pick the exception.
Key takeaways
- Always branch off
main; never commit straight to it. The branch is your scratchpad, the PR is your gate. - Squash and merge keeps
mainlinear and readable — one feature, one commit. It’s the safest default for most projects. - Rebase and merge preserves a curated set of logical commits without a merge bubble — use it when each commit is a reviewable step.
- Create a merge commit keeps the full branch history and records when things merged — use it for long-lived or release branches.
- Required status checks and review turn the merge button from a hope into a guarantee.
This is Part 5 of Build in the Open, a 14-part series on taking a software project from a blank idea to a public release using GitHub and Claude Code. Each post teaches a technique you can apply to any project in any language, then shows how the open-source GopherTrunk scanner does it for real.
In this post
- What a branch actually is — and why you always make one.
- The pull request as the gate: review, status checks, draft PRs.
- The three ways to merge to main, mapped to GitHub’s three merge buttons, with a “use this when” for each.
- How to choose a default for your repo and when to break it.
- How GopherTrunk does it — squash-on-merge, Conventional Commits, and a “one logical change per PR” rule.
What is a branch, and why always make one?
A branch is a movable pointer to a line of commits. When you branch off
main, you get an isolated copy of the project’s history that you can commit to
freely without touching the shared, “known-good” main branch. If the
experiment works, you merge it back; if it doesn’t, you delete the branch and
nothing is lost.
The rule experienced teams follow is simple: main is always releasable, so
nobody commits to it directly. All work happens on a branch, and the only way
in is a pull request. That single discipline buys you code review, automated
testing, and a clean audit trail — for free.
A good branch is one thing: one bug fix, one feature, one refactor. A branch
named fix-timezone-parsing should not also quietly reformat three unrelated
files. Keeping branches narrow is what makes the merge decision below easy.
The pull request is the gate
A pull request (PR) proposes merging your branch into main. It’s where
three safety mechanisms live:
- Review. A second pair of eyes (or, on a solo project, your own cooled-off self the next morning) reads the diff and approves or requests changes.
- Required status checks. GitHub can block the merge button until your CI — build, tests, linters — reports success. We cover building those checks in Part 7.
- Draft PRs. Open a PR as a draft when the work isn’t done but you want CI to run and reviewers to peek early. You can’t merge a draft until you mark it “Ready for review” — a built-in guardrail against merging half-finished work.
Crucially, the shape of the commits inside your branch and the merge button you press are two different decisions. The next sections connect them.
The three ways to merge to main
There are three honest ways to land a branch, defined by how many commits you
want to appear on main. GitHub exposes each as a merge-button option.
1. Many small commits → one PR
You committed often while exploring — “wip”, “try other approach”, “fix typo”,
“actually fix it”. That’s a perfectly good way to work; it’s a terrible way to
leave main looking. You have two choices at merge time:
- Squash and merge collapses all of it into a single commit on
main. The messy journey stays on the branch (and in the closed PR),mainstays clean. - Create a merge commit keeps every commit and adds a merge commit that
records the join. Honest, but it makes
mainnoisy.
Use this when: the work was exploratory or incremental and the individual commits aren’t worth preserving. Reach for squash and merge here — it’s the most common real-world case.
2. Three to five large logical commits → one PR
Sometimes a feature genuinely has a few distinct, reviewable steps: “add the parser”, “wire it into the pipeline”, “add the config flag”. Each commit compiles, passes tests, and tells a story. You curated this history on purpose — often with an interactive rebase to tidy it before opening the PR.
Use this when: each commit is a meaningful, self-contained step a future
reader (or git bisect) would want to land on individually. Reach for rebase
and merge to replay those commits onto main with no merge bubble, or a
careful merge commit if you also want to record the integration point.
3. One squashed / “special” commit → one PR
The cleanest possible main: one feature equals one commit. Every entry in
git log main is a complete, shippable unit you could revert in one move. This
is what most polished open-source projects present, regardless of how messy the
branch was underneath.
Use this when: you want a linear, scannable main history and you don’t need
intermediate commits preserved. Reach for squash and merge and write the
squash commit message carefully — it’s the one line that survives.
Mapping it to GitHub’s three buttons
| Merge button | What lands on main |
Best for |
|---|---|---|
| Create a merge commit | All branch commits + a merge commit | Long-lived / release branches; preserving exact history |
| Squash and merge | One new commit | Exploratory work; “one feature, one commit”; linear history |
| Rebase and merge | Each branch commit, replayed, no merge commit | A small curated set of logical commits |
You set which options are even allowed in Settings → General → Pull Requests. Many teams disable the ones they don’t want so nobody picks the wrong one by accident.
How do I choose a default — and when do I break it?
Pick one default merge method for the whole repo and use it 95% of the time. For most projects, that default should be squash and merge, because:
- It makes
maina clean list of features and fixes, not a transcript of someone’s afternoon. - It makes reverting trivial — one commit, one
git revert. - It pairs naturally with Conventional Commits (e.g.
feat:,fix:,docs:), where the single squash message becomes the changelog entry.
Break the default only when the work’s shape demands it: a carefully sequenced refactor that benefits from rebase-and-merge, or a release/integration branch where a merge commit’s record of “what merged when” is the whole point.
How GopherTrunk does it
GopherTrunk keeps main linear
with squash-on-merge as the standing rule. Its
CONTRIBUTING.md
spells out both the branch discipline and the merge policy:
- One logical change per PR. The guide’s rule of thumb: “if you can’t summarise the change in one sentence in the imperative mood, the change is doing more than one thing and should be split.” Bug fixes ship as one commit with a regression test; refactors are a separate PR, never bundled with a behaviour change.
- Messy branches are fine;
mainis not. Contributors are told force-push is fine on feature branches because “the maintainer squashes on merge to keepmainhistory clean.” That’s case 1 above, applied as policy. - Conventional Commits. Look at the real history and the pattern is obvious —
feat: expose P25 site identity in grant events and via /api/v1/sites (#698),fix(sdr): serialize StreamIQ re-open behind async teardown (#686),ci(pages): run docs rebuild daily at 11:00 Central. Each squashed commit is a typed, scoped, one-line summary that doubles as a changelog line. - The merge button is gated, not free. GopherTrunk’s branch-protection
ruleset (
.github/rulesets/main-branch-protection.json) requires thebuild-test,usb-windows, andusb-macosstatus checks to pass and requires review threads resolved before the merge button unlocks — and it allows all three merge methods ("allowed_merge_methods": ["merge", "squash", "rebase"]) so the maintainer can pick per-PR while defaulting to squash. - A test plan rides along. Every PR fills in the template at
.github/pull_request_template.md, whose Test plan checklist asks formake vet testgreen locally,make integrationif the daemon changed, added/updated tests, and a hardware smoke test if SDR/USB/vocoder code changed. The shape of the work is reviewed before anyone reaches for the merge button.
You’re not building a radio, but the moves port directly: branch per change, keep each PR to one logical thing, gate the merge with CI and review, and squash unless the work’s structure earns an exception.
FAQ
Should I squash, rebase, or merge-commit by default?
Default to squash and merge for a clean, linear main where one feature is
one commit. Use rebase and merge when a few curated commits each deserve to
survive, and a merge commit for long-lived or release branches where the
record of when things merged matters.
What’s the difference between rebase-and-merge and a merge commit?
Rebase-and-merge replays your branch’s commits directly onto main with no extra
commit, giving a straight line. A merge commit keeps your commits and adds a
new commit recording the join, which preserves the exact branch topology but
makes the history non-linear.
What is a draft pull request and when should I open one? A draft PR runs your CI and lets reviewers see the work, but its merge button is disabled until you mark it “Ready for review.” Open one when you want early feedback or to confirm checks pass before the work is finished.
Can I commit directly to main? You can, but you shouldn’t. Branch-protection rules (Part 12) can forbid it outright. Routing every change through a branch and PR gives you review, automated testing, and an easy path to revert.
How many commits should one PR have? As many as the work honestly needs — but the PR should still represent one logical change. If your commits span two unrelated concerns, that’s two PRs, not one.
Series navigation
Part 5 of 14 · ← Part 4: Git & GitHub Fundamentals (the Web Interface) · Next → Part 6: Planning & Tracking Work — and Inviting Contributors