Git Branching Strategies#
Choosing a branching strategy is choosing your team’s speed limit. The wrong model introduces merge conflicts, stale branches, and release bottlenecks. The right model depends on how you deploy, how big your team is, and how much you trust your test suite.
Trunk-Based Development#
Everyone commits to main (or very short-lived branches that merge within hours). No long-running feature branches. No develop branch. No release branches unless you need to patch old versions.
# Create a short-lived branch
git checkout -b fix-login-timeout main
# Work, commit, push
git add src/auth/timeout.go
git commit -m "fix: increase login timeout to 30s"
git push -u origin fix-login-timeout
# Open PR, get review, merge same day
# Then delete the branch
git branch -d fix-login-timeout
git push origin --delete fix-login-timeoutTrunk-based development works when you have strong CI, feature flags for incomplete work, and a team that reviews quickly. Google, Facebook, and most high-performing teams use this. The constraint is discipline: if PRs sit open for days, you lose the benefit.
Feature flags replace feature branches. Instead of isolating incomplete code in a branch, you merge it behind a flag:
if featureflags.Enabled("new-checkout-flow", user) {
return newCheckoutHandler(w, r)
}
return legacyCheckoutHandler(w, r)This lets you merge daily without exposing unfinished features to users.
GitHub Flow#
A simplified model: main is always deployable, all work happens on branches, branches merge via pull request. This is trunk-based development with slightly longer-lived branches (days, not hours).
# Branch from main
git checkout -b feature/user-export main
# Work over 2-3 days, push regularly
git push -u origin feature/user-export
# Open PR when ready, merge after review
# Deploy from mainGitHub Flow works for most teams. It fails when branches live for weeks, which creates painful merge conflicts and integration risk.
GitFlow (And Why It Is Mostly Dead)#
GitFlow uses main, develop, release/*, hotfix/*, and feature/* branches. It was designed for software shipped on a release cadence (quarterly releases, boxed software). For teams deploying continuously, GitFlow adds ceremony without value.
The develop branch becomes a merge conflict magnet. Release branches create a parallel world where fixes must be cherry-picked in multiple directions. Most teams that adopted GitFlow between 2010 and 2015 have since moved to trunk-based or GitHub Flow.
When GitFlow still makes sense: you ship versioned software (libraries, SDKs, on-premise products) where multiple versions exist in production simultaneously and need independent patches.
Release Branches#
Even in trunk-based development, you may need release branches for versioned software:
# Cut a release branch
git checkout -b release/2.4 main
git push -u origin release/2.4
# Fixes go to main first, then cherry-pick
git checkout release/2.4
git cherry-pick abc1234
git pushThe rule: fixes land on main first, then get cherry-picked to release branches. Never fix on a release branch and merge back – that creates divergence.
Merge vs Rebase vs Squash#
Three ways to integrate a branch. Each has real tradeoffs.
Merge commit preserves full history. Every commit on the branch appears in main, plus a merge commit:
git checkout main
git merge --no-ff feature/user-exportUse when: commit history on the branch is clean and meaningful.
Squash merge collapses all branch commits into a single commit on main:
git checkout main
git merge --squash feature/user-export
git commit -m "feat: add user export to CSV"Use when: branch history is messy (WIP commits, fixups, “fix typo” commits). This is the most common choice for teams using GitHub PRs.
Rebase replays branch commits on top of main, creating a linear history with no merge commits:
git checkout feature/user-export
git rebase main
git checkout main
git merge --ff-only feature/user-exportUse when: you want linear history and clean individual commits. The risk is rewriting history on shared branches – never rebase commits others have pulled.
Conventional Commits#
A structured format for commit messages that enables automated changelogs and semantic versioning:
feat: add CSV export for user data
fix: prevent timeout on large file uploads
docs: update API authentication guide
refactor: extract payment processing into service
chore: upgrade Go to 1.23
feat!: redesign authentication API (BREAKING CHANGE)The prefixes (feat, fix, docs, refactor, chore) categorize changes. feat bumps the minor version. fix bumps the patch. ! or a BREAKING CHANGE footer bumps the major version. Tools like release-please and semantic-release automate this.
Protected Branches#
Configure branch protection on main to enforce quality gates:
# Via GitHub CLI
gh api repos/myorg/myrepo/branches/main/protection -X PUT -f \
required_status_checks='{"strict":true,"contexts":["ci/test","ci/lint"]}' \
enforce_admins=true \
required_pull_request_reviews='{"required_approving_review_count":1}' \
required_linear_history=trueKey settings:
- Required reviews: at least one approval before merge.
- Required status checks: CI must pass. Set
strict: trueto require the branch be up to date withmainbefore merging. - Linear history: forces squash or rebase merges, no merge commits. This produces a clean
git logonmain. - Enforce admins: prevents admins from bypassing protections. Without this, every admin is a potential shortcut.
Monorepo Considerations#
In monorepos, branching strategy stays the same, but you need path-based triggers:
# Only run backend CI when backend code changes
git diff --name-only main...HEAD | grep '^backend/'Use CODEOWNERS to require the right reviewers per directory:
# .github/CODEOWNERS
/backend/ @backend-team
/frontend/ @frontend-team
/infra/ @platform-teamTrunk-based development works well in monorepos because short-lived branches minimize cross-team conflicts. Long-lived branches in monorepos are painful – every team’s changes stack up, and the merge at the end is a disaster.
Choosing a Strategy#
- Deploying continuously, single version in production: trunk-based or GitHub Flow.
- Shipping versioned software (libraries, SDKs): GitHub Flow with release branches.
- Multiple versions in production needing independent patches: GitFlow or release branches from trunk.
- Small team, fast reviews: trunk-based development.
- Larger team, slower reviews: GitHub Flow with squash merges and required reviews.
The branching strategy that works is the one your team actually follows. A simple model that everyone uses beats an elaborate model that people bypass.