Every team reinvents Git workflows. Most end up with something that worked for three people but breaks at fifteen. Here’s what actually scales.

The Problem With “Whatever Works”

Small teams can get away with anything. Push to main, YOLO merges, commit messages like “fix stuff” — it all works when you can shout across the room.

Then the team grows. Suddenly:

  • Two people edit the same file and spend an hour on merge conflicts
  • Nobody knows what’s in production vs staging
  • “Which commit broke this?” becomes an archaeological dig
  • Releases are terrifying because nobody’s sure what changed

The solution isn’t more process. It’s the right process.

Trunk-Based Development

The simplest workflow that scales. Everyone commits to main (trunk) with short-lived branches.

mainf(e2atduaryes-)af(e1atduarye)-bf(e3atduaryes-)c

Rules

  1. Branches live < 2 days — If it takes longer, break it into smaller pieces
  2. Main is always deployable — Tests pass, builds work
  3. Feature flags hide incomplete work — Ship code, enable later
  4. No long-running branches — No develop, no release/v2

Why It Works

Short branches mean small diffs. Small diffs mean:

  • Easier code review
  • Fewer merge conflicts
  • Faster feedback
  • Lower risk per change
1
2
3
4
5
6
# Typical workflow
git checkout -b add-user-export
# ... work for a day ...
git push -u origin add-user-export
# Create PR, get review, merge
# Total time: 1-2 days max

When It Doesn’t Work

  • Teams without CI/CD (you need fast feedback)
  • Compliance requirements mandating release branches
  • Very junior teams without code review discipline

GitHub Flow

A slight variation: main + feature branches + PRs. No develop branch, no release branches.

mainfeature-1feature-2

The Process

  1. Create branch from main
  2. Make commits
  3. Open Pull Request
  4. Discuss, review, test
  5. Merge to main
  6. Deploy
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Start work
git checkout main
git pull
git checkout -b feature/user-notifications

# Work
git add .
git commit -m "Add notification preferences model"
git push -u origin feature/user-notifications

# Open PR via GitHub, merge when approved

PR Best Practices

Small PRs:

1
2
3
4
5
6
7
# Bad PR
feat: Add entire user management system
+2,847 lines, -423 lines

# Good PR
feat: Add user preferences table migration
+47 lines

Descriptive commits:

1
2
3
4
5
# Bad
git commit -m "fix"

# Good
git commit -m "fix: prevent duplicate notifications when user has multiple devices"

GitFlow (When You Need It)

More complex, but necessary for some contexts: packaged software, mobile apps, strict release schedules.

mhrdfaoeeeitlvanfeetialuxsorepe

Branches

  • main: Production code only
  • develop: Integration branch
  • feature/*: New features, branch from develop
  • release/*: Release prep, branch from develop
  • hotfix/*: Emergency fixes, branch from main

When To Use

  • Mobile apps with App Store review delays
  • On-premise software with versioned releases
  • Regulated industries requiring release documentation
  • Teams that deploy monthly, not daily

When To Avoid

  • Web apps with continuous deployment
  • Teams under 10 people
  • Microservices (each service is its own repo/workflow)

Commit Message Conventions

Consistent commits make history useful. Conventional Commits is the standard:

<<<tbfyoopdoeyt>>e(r<>scope>):<subject>

Types

1
2
3
4
5
6
7
feat:     # New feature
fix:      # Bug fix
docs:     # Documentation
style:    # Formatting (no code change)
refactor: # Code change that neither fixes nor adds
test:     # Adding tests
chore:    # Build process, dependencies

Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
feat(auth): add OAuth2 login with Google

Implements Google OAuth2 flow for user authentication.
Adds new endpoint /auth/google/callback.

Closes #234

---

fix(payments): handle decimal precision in currency conversion

Previously, converting between currencies could lose precision
due to floating point errors. Now uses Decimal type throughout.

Fixes #567

Enforcing With Hooks

1
2
3
4
5
6
7
8
9
# .git/hooks/commit-msg
#!/bin/bash
commit_regex='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}'

if ! grep -qE "$commit_regex" "$1"; then
    echo "Invalid commit message format."
    echo "Expected: type(scope): subject"
    exit 1
fi

Or use commitlint with Husky:

1
2
3
4
5
6
7
8
// package.json
{
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
}

Branch Protection

GitHub/GitLab branch protection prevents accidents:

1
2
3
4
5
6
# GitHub branch protection for main
- Require pull request reviews: 1
- Require status checks: [ci/test, ci/lint]
- Require branches to be up to date
- Include administrators: true
- Restrict who can push: [deploy-bot]

This ensures:

  • No direct pushes to main
  • All changes reviewed
  • Tests pass before merge
  • Even admins follow the rules

Rebasing vs Merging

Merge commits preserve history:

main

Rebasing linearizes history:

main

My Recommendation

  • Rebase feature branches before merging (clean history)
  • Merge with merge commits to main (preserve context)
  • Never rebase shared branches
1
2
3
4
5
6
# Before opening PR, rebase on main
git fetch origin
git rebase origin/main
git push --force-with-lease  # Safe force push

# Merge PR with merge commit (GitHub default)

Monorepo Considerations

When multiple services live in one repo:

spieanrcfvkriawwasuacpeoghtseibreaits/ksrlre/esurd/c/-ttuyrpee/s/

Path-Based CI

Only run tests for changed paths:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# GitHub Actions
on:
  push:
    paths:
      - 'services/api/**'
      - 'packages/**'

jobs:
  test-api:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cd services/api && npm test

CODEOWNERS

Route reviews to the right teams:

#////ssipCeenaOrrfcDvvrkEiiaaOccsgWeeteNssrsE//u/RawcSpetibu//re/@@@@bfpbarlacoackntketfeneondnrd-dm-t--tetteaeeamaammm@frontend-team

The Actual Advice

  1. Start with trunk-based — Add complexity only when needed
  2. Enforce via automation — Branch protection, CI checks, commit hooks
  3. Keep branches short — Days, not weeks
  4. Make main always deployable — Feature flags are your friend
  5. Document your workflow — A simple CONTRIBUTING.md saves arguments

The best workflow is the one your team actually follows. Start simple, add rules when you feel pain, and automate everything you can. 🌍