Every team argues about Git workflows. Here’s how to pick one that won’t make you miserable.

The Spectrum

Git workflows exist on a spectrum from simple to complex:

T(SFHrSoaiuilsgnmothkp-lddtBeeera)vpuslseotdysSWCmeoaedlkellyrteervaeimleewasesLSFacorhrGgemi(edatCulFotllmeepopadrwlmoercxee)lsesases

Pick based on your reality, not your aspirations.

Trunk-Based Development

Everyone commits to main. That’s it.

1
2
3
4
5
6
7
# The entire workflow
git checkout main
git pull
# make changes
git add .
git commit -m "Add user search feature"
git push

When it works:

  • Solo projects
  • Small teams with high trust
  • Continuous deployment
  • Strong test coverage

The catch: Requires feature flags for incomplete work:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Feature flags for trunk-based
from config import features

def get_users():
    users = fetch_all_users()
    
    if features.is_enabled('user_search'):
        users = apply_search_filter(users)
    
    return users

GitHub Flow

One step up: short-lived feature branches with pull requests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Start a feature
git checkout main
git pull
git checkout -b feature/user-search

# Work on it
git add .
git commit -m "Add search endpoint"
git push -u origin feature/user-search

# Open PR, get review, merge to main
# Deploy main automatically
mfaeianture

Branch naming conventions:

1
2
3
4
5
feature/user-search      # New feature
fix/login-timeout        # Bug fix
hotfix/security-patch    # Urgent fix
docs/api-reference       # Documentation
refactor/auth-module     # Code cleanup

PR template (.github/pull_request_template.md):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
## What
Brief description of changes

## Why
Context and motivation

## Testing
- [ ] Unit tests pass
- [ ] Manual testing done
- [ ] No console errors

## Screenshots
If applicable

GitFlow

For teams with scheduled releases.

mrdfaeeeilvaneetalusorepe/1.0release/1.1

The branches:

1
2
3
4
5
main        # Production code, tagged releases
develop     # Integration branch, next release
feature/*   # New features, branch from develop
release/*   # Release prep, branch from develop
hotfix/*    # Emergency fixes, branch from main

Starting a feature:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
git checkout develop
git pull
git checkout -b feature/user-search

# Work...
git add .
git commit -m "Add search"

# Merge back to develop
git checkout develop
git merge --no-ff feature/user-search
git push
git branch -d feature/user-search

Creating a release:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Branch from develop
git checkout develop
git checkout -b release/1.2.0

# Bump version, final fixes
echo "1.2.0" > VERSION
git commit -am "Bump version to 1.2.0"

# Merge to main and tag
git checkout main
git merge --no-ff release/1.2.0
git tag -a v1.2.0 -m "Release 1.2.0"
git push origin main --tags

# Merge back to develop
git checkout develop
git merge --no-ff release/1.2.0
git push

git branch -d release/1.2.0

Hotfix workflow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Branch from main
git checkout main
git checkout -b hotfix/security-fix

# Fix the issue
git commit -am "Fix XSS vulnerability"

# Merge to main
git checkout main
git merge --no-ff hotfix/security-fix
git tag -a v1.2.1 -m "Hotfix 1.2.1"
git push origin main --tags

# Also merge to develop
git checkout develop
git merge --no-ff hotfix/security-fix
git push

Commit Messages

Good commit messages matter:

1
2
3
4
5
6
7
8
9
# Bad
git commit -m "fix"
git commit -m "update"
git commit -m "WIP"

# Good
git commit -m "Fix null pointer in user search when query is empty"
git commit -m "Add pagination to /api/users endpoint"
git commit -m "Refactor auth middleware for better testability"

Conventional commits format:

1
2
3
4
5
6
7
8
9
<type>(<scope>): <description>

# Examples
feat(auth): add OAuth2 login support
fix(api): handle empty search queries gracefully
docs(readme): update installation instructions
refactor(db): extract connection pooling logic
test(users): add integration tests for search
chore(deps): update axios to 1.6.0

Automating changelog with conventional commits:

1
2
3
4
5
# Install standard-version
npm install -g standard-version

# Generate changelog and bump version
standard-version

Protected Branches

Enforce quality with branch protection:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# .github/settings.yml (with probot/settings)
branches:
  - name: main
    protection:
      required_status_checks:
        strict: true
        contexts:
          - ci/tests
          - ci/lint
      required_pull_request_reviews:
        required_approving_review_count: 1
        dismiss_stale_reviews: true
      enforce_admins: true
      restrictions: null

Rebasing vs Merging

Merge commits (preserve history):

1
2
3
git checkout main
git merge feature/user-search
# Creates merge commit, preserves branch history

Rebase (linear history):

1
2
3
4
5
git checkout feature/user-search
git rebase main
git checkout main
git merge feature/user-search  # Fast-forward
# Linear history, as if changes were made sequentially

Squash merge (clean main):

1
2
3
4
git checkout main
git merge --squash feature/user-search
git commit -m "Add user search feature"
# All feature commits become one on main

My preference: Squash merge for features, merge commits for releases.

Handling Conflicts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# During rebase
git rebase main
# CONFLICT in file.py

# Fix the conflict
vim file.py
git add file.py
git rebase --continue

# If it's a mess, abort
git rebase --abort

Preventing conflicts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Rebase onto main frequently
git fetch origin
git rebase origin/main

# Before opening PR
git checkout main
git pull
git checkout feature/my-feature
git rebase main
git push --force-with-lease  # Safe force push

Git Hooks

Automate quality checks:

1
2
3
4
# .git/hooks/pre-commit
#!/bin/bash
npm run lint
npm run test:unit

With husky (easier):

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

The Decision Tree

SoloYNeopsrCoojneTtYNcrieotuns?nuSkoc-uhbsGeYNaideosdtuseeHldpuelbdGoGiyFritmletHeolFunwelbtao?swFelso?w

My Setup

For most projects:

1
2
3
4
5
6
# GitHub Flow with squash merge
# - main is always deployable
# - Feature branches are short-lived (<1 week)
# - PRs require 1 review
# - Squash merge to keep main clean
# - Deploy on merge to main

Complexity is earned, not assumed. Start simple.


Got a Git workflow that works? Share it on Twitter.