Git branching strategy is one of those things that seems obvious until you're dealing with a broken production deploy, three merge conflicts, and a hotfix that can't go out because the release branch is locked. Getting the strategy right early prevents a lot of pain.

Here are the main approaches, what they optimize for, and when to use each.

Trunk-Based Development

Everyone commits to main (or trunk) directly, or uses very short-lived feature branches (1-2 days max). CI runs on every commit. Deployments happen frequently — sometimes multiple times a day.

main: A - B - C - D - E - F ↑ ↑ ↑ deploys 

When to use: Teams with strong CI/CD, good test coverage, and feature flags. Works well for SaaS products where you control the deployment. Google and Meta use trunk-based development at scale.

Advantages:

  • No long-lived merge hell
  • Forces small, reviewable changes
  • Simulates what users will get quickly
  • Integrates well with feature flags (ship dark)

Disadvantages:

  • Requires discipline to keep main releasable at all times
  • Feature flags add complexity
  • Less intuitive for teams used to longer PR cycles

GitHub Flow

One long-lived branch (main) plus short-lived feature branches. Feature branches are merged via PR, then deployed immediately.

main: A -------- B -------- C \ / \ / feature-1 feature-2 

Workflow:

git checkout -b feature/user-auth # work git push -u origin feature/user-auth # open PR → review → merge # deploy main 

When to use: Teams deploying continuously, open-source projects, apps where main is always deployable.

Advantages:

  • Simple to understand
  • Works well with GitHub/GitLab PR workflow
  • Short feedback loops

Disadvantages:

  • No clear separation between development and release
  • Doesn't handle scheduled releases well

Git Flow

Multiple long-lived branches: main, develop, release/*, hotfix/*. Feature branches come off develop.

main: A -------- B -------- C (only releases + hotfixes) \ ↑ ↑ develop: A - D - E - F - G - H (ongoing work) \ / \ / feature/x feature/y 

Workflow:

# Start feature git checkout -b feature/payment develop # Finish feature git checkout develop git merge --no-ff feature/payment # Start release git checkout -b release/1.2 develop # bug fixes only on release branch git checkout main git merge --no-ff release/1.2 git tag -a v1.2 # Hotfix git checkout -b hotfix/fix-payment main git checkout main && git merge --no-ff hotfix/fix-payment git checkout develop && git merge --no-ff hotfix/fix-payment 

When to use: Mobile apps, versioned software, teams with scheduled releases, libraries with multiple supported versions.

Disadvantages:

  • Complex — many branches to track
  • Long-lived branches create merge conflicts
  • Overkill for most web apps that deploy continuously

A Simplified Two-Branch Strategy

For most web teams, a simplified version works well:

  • main: always deployable, auto-deploys to staging
  • production: tracks what's live, auto-deploys to production
  • Feature branches: off main, merged via PR
# Feature work git checkout -b feature/dark-mode main # develop, commit, push # Open PR → review → merge to main → deploys to staging # Release git checkout production git merge main git push # deploys to production git tag v1.4.0 

Hotfixes go directly to production and are immediately back-merged to main.

Practical Git Habits

Always branch from the right place

# Make sure your base is up to date git checkout main git pull origin main git checkout -b feature/my-thing 

Keep commits small and single-purpose

# Bad: one commit that does everything git commit -m "add auth, update UI, fix bug, refactor data layer" # Good: atomic commits git commit -m "add JWT validation middleware" git commit -m "add login/signup endpoint" git commit -m "add auth state to Redux store" 

Use --no-ff for merges you want to track

git merge --no-ff feature/my-thing 

This preserves the feature branch in history. Without --no-ff, fast-forward merges lose the branch topology.

Rebase to clean up before PR

# Squash work-in-progress commits before review git rebase -i origin/main # Rebase onto updated main instead of merging git rebase origin/main 

Interactive rebase lets you squash, reword, and reorder commits before they're reviewed. This keeps main history readable.

Protect your main branch

On GitHub:

  • Require PRs to merge to main
  • Require status checks (CI must pass)
  • Require at least 1 review
  • Prevent force pushes
# .github/branch-protection.yml (via API or UI) required_status_checks: strict: true contexts: - "ci/tests" - "ci/lint" required_pull_request_reviews: required_approving_review_count: 1 enforce_admins: true 

Versioning and Tags

Tag releases so you can track what's deployed:

# Semantic version tag git tag -a v1.4.2 -m "v1.4.2: fix checkout bug" git push origin v1.4.2 # List tags git tag -l "v1.*" # Find which tag a commit is in git describe --tags HEAD 

Semantic versioning: MAJOR.MINOR.PATCH

  • MAJOR: breaking changes
  • MINOR: new features, backwards-compatible
  • PATCH: bug fixes

Merge vs Rebase: The Practical Answer

  • Rebase your feature branch onto main before opening a PR: keeps a linear, readable history
  • Merge (or squash) when merging the PR: preserves the fact that a set of commits came in together
  • Never rebase commits that have been pushed to a shared branch (rewrites history others may have based work on)

The rule: rebase privately (your local feature branch), merge publicly (into main via PR).

Commit Message Format

A conventional commits format makes changelogs and searches easier:

feat: add dark mode toggle fix: resolve session expiry on Firefox mobile docs: update API auth section chore: upgrade dependencies refactor: split user service into auth and profile 

Tools like semantic-release can auto-generate changelogs and version bumps from these.

The right branching strategy is the simplest one that handles your deployment cadence and team size without creating merge nightmares. For most web apps: GitHub Flow (or a simple two-branch variant). For versioned software or scheduled releases: Git Flow or a lighter variant.