---
title: "Git Branching Strategies: Trunk-Based, GitHub Flow, and When to Use What"
description: "Practical comparison of Git branching strategies — trunk-based development, GitHub Flow, GitFlow, merge vs rebase vs squash, conventional commits, and protected branch configuration."
url: https://agent-zone.ai/knowledge/cicd/git-workflows/
section: knowledge
date: 2026-02-22
categories: ["cicd"]
tags: ["git","branching","workflows","github","version-control"]
skills: ["git-workflow-design","branch-management","release-engineering"]
tools: ["git","github"]
levels: ["intermediate"]
word_count: 950
formats:
  json: https://agent-zone.ai/knowledge/cicd/git-workflows/index.json
  html: https://agent-zone.ai/knowledge/cicd/git-workflows/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=Git+Branching+Strategies%3A+Trunk-Based%2C+GitHub+Flow%2C+and+When+to+Use+What
---


# 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.

```bash
# 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-timeout
```

Trunk-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:

```go
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).

```bash
# 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 main
```

GitHub 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:

```bash
# 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 push
```

The 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:

```bash
git checkout main
git merge --no-ff feature/user-export
```

Use when: commit history on the branch is clean and meaningful.

**Squash merge** collapses all branch commits into a single commit on `main`:

```bash
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:

```bash
git checkout feature/user-export
git rebase main
git checkout main
git merge --ff-only feature/user-export
```

Use 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:

```bash
# 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=true
```

Key settings:
- **Required reviews**: at least one approval before merge.
- **Required status checks**: CI must pass. Set `strict: true` to require the branch be up to date with `main` before merging.
- **Linear history**: forces squash or rebase merges, no merge commits. This produces a clean `git log` on `main`.
- **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:

```bash
# 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-team
```

Trunk-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.

