Git Workflow Guide: Branching Strategies for Teams and Solo Developers
Every team eventually argues about Git branching strategy. Should we use Git Flow? Is trunk-based development realistic for a team of three? What happens when a hotfix needs to go out but the release branch has unfinished features? These questions matter because your branching strategy directly affects how fast you can ship, how often you break production, and how painful merges become.
This guide presents the major Git workflow strategies, their trade-offs, and practical recommendations based on team size and release cadence. It also covers the supporting practices that make any strategy work: commit conventions, pull request discipline, code review, and CI/CD integration.
The Three Major Branching Strategies
| Strategy | Branches | Best For | Release Cadence |
|---|---|---|---|
| Git Flow | main, develop, feature/*, release/*, hotfix/* | Versioned releases, large teams | Scheduled (weekly/monthly) |
| GitHub Flow | main, feature/* | Continuous deployment, SaaS | Continuous (multiple per day) |
| Trunk-Based | main (short-lived feature branches optional) | High-velocity teams, CI/CD maturity | Continuous |
Git Flow
Git Flow, introduced by Vincent Driessen in 2010, uses two long-lived branches (main and develop) plus three types of supporting branches:
# Initialize Git Flow
git flow init
# Start a feature
git flow feature start user-authentication
# Work on the feature, make commits...
git flow feature finish user-authentication
# Merges into develop
# Start a release
git flow release start v2.1.0
# Bug fixes, version bumps, changelog updates...
git flow release finish v2.1.0
# Merges into both main and develop, tags main
# Emergency hotfix
git flow hotfix start fix-login-crash
# Fix the issue...
git flow hotfix finish fix-login-crash
# Merges into both main and develop
Without the git-flow CLI tool, the same operations look like:
# Feature branch (manual)
git checkout develop
git checkout -b feature/user-authentication
# ... work and commit ...
git checkout develop
git merge --no-ff feature/user-authentication
git branch -d feature/user-authentication
# Release branch (manual)
git checkout develop
git checkout -b release/v2.1.0
# ... fix bugs, bump version ...
git checkout main
git merge --no-ff release/v2.1.0
git tag -a v2.1.0 -m "Release v2.1.0"
git checkout develop
git merge --no-ff release/v2.1.0
git branch -d release/v2.1.0
Git Flow's strengths are clear separation between development and production code, explicit release management, and the ability to support multiple versions simultaneously. Its weaknesses are complexity (five branch types, multiple merge targets), slow integration (features can diverge from develop for weeks), and merge conflicts that compound as branches age.
When to use Git Flow: Software with versioned releases (desktop apps, mobile apps, libraries), teams that ship on a schedule rather than continuously, projects that must maintain multiple release branches (v2.x and v3.x simultaneously).
GitHub Flow
GitHub Flow simplifies to two concepts: main is always deployable, and all work happens on feature branches that merge via pull requests.
# Create a feature branch from main
git checkout main
git pull origin main
git checkout -b add-user-search
# Make commits with clear messages
git add .
git commit -m "feat: add user search API endpoint"
git commit -m "feat: add search UI with debounced input"
git commit -m "test: add integration tests for user search"
# Push and create a pull request
git push -u origin add-user-search
# Open PR on GitHub, request reviews
# After approval and CI passes, merge to main
# Deploy automatically from main
The rules are intentionally simple:
- Anything in
mainis deployable - Create descriptive branch names from
main - Push to the branch regularly
- Open a pull request for discussion and review
- Merge to
mainonly after CI passes and review is complete - Deploy immediately after merging
GitHub Flow works best when you have CI/CD that automatically deploys main after every merge. Without automated deployment, you end up accumulating unreleased changes in main, which defeats the purpose.
Trunk-Based Development
Trunk-based development takes GitHub Flow further: developers commit directly to main (the "trunk") or use very short-lived feature branches (1-2 days maximum).
# Option 1: Commit directly to main
git checkout main
git pull origin main
# Make a small, self-contained change
git add .
git commit -m "feat: add email validation to signup form"
git push origin main
# Option 2: Short-lived branch (merged same day)
git checkout -b add-email-validation
# Small focused change...
git push -u origin add-email-validation
# Create PR, get quick review, merge within hours
# Delete branch immediately after merge
Trunk-based development requires:
- Feature flags - Incomplete features are deployed behind flags, not on long-lived branches
- Comprehensive CI - Every commit to main runs the full test suite
- Small changes - Each commit is a small, reviewable, deployable unit
- Team discipline - Everyone must keep main green; a broken build is an emergency
# Feature flags in code (example)
if feature_enabled("new_search_algorithm"):
results = new_search(query)
else:
results = legacy_search(query)
# Feature flags decouple deployment from release
# Deploy code to production, then enable the flag when ready
Choosing a Strategy by Team Size
| Team Size | Recommended Strategy | Reasoning |
|---|---|---|
| Solo developer | GitHub Flow or trunk-based | Git Flow overhead is not justified for one person |
| 2-5 developers | GitHub Flow | Simple, effective, supports code review via PRs |
| 5-15 developers | GitHub Flow or trunk-based | Depends on CI/CD maturity and deployment frequency |
| 15+ developers | Trunk-based with short-lived branches | Long-lived branches cause integration problems at scale |
| Multiple release versions | Git Flow | Only strategy that naturally supports versioned releases |
Commit Message Conventions
Regardless of branching strategy, consistent commit messages make history readable and enable automated changelogs.
Conventional Commits
# Format: type(scope): description
feat(auth): add OAuth2 login with Google
fix(api): handle null response from payment gateway
docs(readme): update installation instructions
refactor(database): extract query builder into separate module
test(users): add edge case tests for email validation
chore(deps): update Go dependencies to latest versions
perf(search): add index on users.email for faster lookups
ci(github): add caching to GitHub Actions workflow
# Breaking changes (append !)
feat(api)!: change authentication endpoint response format
# Multi-line commit with body and footer
feat(notifications): add email notification service
Implements async email sending using the queue system.
Emails are templated with the new notification templates.
Closes #234
Co-authored-by: Jane Developer <[email protected]>
Enforce commit conventions with Git hooks:
# .git/hooks/commit-msg (or use commitlint)
#!/bin/bash
commit_msg=$(cat "$1")
pattern="^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?!?: .{1,100}"
if ! echo "$commit_msg" | head -1 | grep -qE "$pattern"; then
echo "ERROR: Commit message does not follow Conventional Commits format."
echo "Example: feat(auth): add login endpoint"
exit 1
fi
Pull Request Best Practices
Pull requests are where code quality is enforced. A good PR workflow includes:
- Keep PRs small - Under 400 lines changed. Large PRs get rubber-stamped, not reviewed.
- Write descriptive PR descriptions - What changed, why it changed, and how to test it.
- Use PR templates - Standardize what information every PR must include.
- Require CI to pass before merge - No exceptions. A failing test is a failing PR.
- Require at least one approval - Even in small teams.
- Use draft PRs for work in progress - Get early feedback without triggering reviews.
# .github/pull_request_template.md
## What Changed
## Why
## How to Test
## Checklist
- [ ] Tests added/updated
- [ ] Documentation updated
- [ ] No breaking changes (or documented in description)
- [ ] Self-reviewed the diff
Code Review Guidelines
Effective code review is a skill that needs explicit guidelines:
- Review the diff, not the developer. Focus on the code, not who wrote it.
- Distinguish between blocking and non-blocking feedback. Use "nit:" for style suggestions and "IMPORTANT:" for issues that must be fixed.
- Review within 24 hours. Stale PRs kill productivity. Set a team expectation for review turnaround time.
- Check for: correctness, edge cases, error handling, test coverage, naming clarity. Do not check for: style (that is the formatter's job).
- Approve when "good enough." Perfect is the enemy of shipped. If the code works, is tested, and is readable, approve it.
CI/CD Integration
Your branching strategy only works if CI/CD enforces the rules:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: go test -race -cover ./...
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: golangci/golangci-lint-action@v4
with:
version: latest
deploy:
needs: [test, lint]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t myapp:${{ github.sha }} .
- run: docker push myregistry/myapp:${{ github.sha }}
Git Hooks for Quality Enforcement
Client-side Git hooks catch issues before they reach the remote repository:
# Install hooks with husky (Node.js) or pre-commit (Python)
# Or manually create .git/hooks/pre-commit:
#!/bin/bash
set -e
echo "Running pre-commit checks..."
# Format check
if ! gofmt -l . | grep -q '^'; then
echo "gofmt: OK"
else
echo "ERROR: Files need formatting:"
gofmt -l .
exit 1
fi
# Vet
go vet ./...
# Quick tests (skip slow tests)
go test -short -count=1 ./...
echo "All pre-commit checks passed."
# pre-push hook: run full test suite before pushing
#!/bin/bash
set -e
echo "Running full test suite before push..."
go test -race -cover ./...
echo "All tests passed. Pushing."
Monorepos vs Multi-Repos
The repository structure affects which branching strategy is practical:
| Aspect | Monorepo | Multi-Repo |
|---|---|---|
| Cross-project changes | Single PR changes multiple services | Requires coordinated PRs across repos |
| CI/CD | Must detect which services changed | Each repo has independent CI |
| Code sharing | Direct imports, no versioning needed | Published packages with versioning |
| Repository size | Can grow very large | Each repo stays focused |
| Best workflow | Trunk-based (Google, Meta use this) | GitHub Flow or Git Flow per repo |
| Tooling | Needs Nx, Turborepo, Bazel, etc. | Standard Git tooling works fine |
Practical Recommendations
After years of working with teams of various sizes, here is what consistently works:
- Start with GitHub Flow. It is simple enough that everyone follows it and powerful enough for most teams. Add complexity only when you hit specific problems that require it.
- Enforce commit conventions from day one. It is much harder to introduce Conventional Commits to a team that has been writing "fix stuff" for two years.
- Automate everything enforceable. Formatting, linting, test execution, and commit message validation should all be automated. If a human has to remember to run it, it will not happen consistently.
- Keep branches short-lived. A feature branch that lives for more than a week is a branch that will cause merge pain. If the feature is too large for a week, break it into smaller deployable pieces behind feature flags.
- Deploy from main automatically. If merging to main does not trigger a deployment, you will accumulate unreleased changes that become a risk.
The best branching strategy is the one your entire team follows consistently. When managing infrastructure with Docker, tools like usulnet integrate with your deployment workflow, allowing you to track which Git commit is running in which container across your infrastructure.