Your Git workflow affects how fast you ship, how often you break things, and how much your team fights over merge conflicts. Choose wisely.

The Contenders

Git Flow

The traditional branching model with long-lived branches:

main(pdreovdeulcotpffrieeeo(aalnitte)nuuatrrseeeehg///orup2tasa.ftey0iirmxo-e/nanc)utrt-ihstyisctaelm-bug

How it works:

  1. main always reflects production
  2. develop is the integration branch
  3. Features branch from develop, merge back to develop
  4. Releases branch from develop, merge to both main and develop
  5. Hotfixes branch from main, merge to both main and develop
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Start a feature
git checkout develop
git checkout -b feature/new-feature

# Finish a feature
git checkout develop
git merge --no-ff feature/new-feature
git branch -d feature/new-feature

# Start a release
git checkout develop
git checkout -b release/1.2.0

# Finish a release
git checkout main
git merge --no-ff release/1.2.0
git tag -a v1.2.0
git checkout develop
git merge --no-ff release/1.2.0

Pros:

  • Clear separation between development and production
  • Parallel release preparation
  • Good for versioned software (mobile apps, installable software)

Cons:

  • Complex, many branches to manage
  • Merge conflicts accumulate
  • Slow to ship (features wait for releases)

GitHub Flow

Simplified: just main and feature branches.

main(afffleiewaxaat/tyulusroreged/i/eunpps-alebyorumy-geaanbutlt-ehs)ystem

How it works:

  1. main is always deployable
  2. Create feature branches from main
  3. Open PR, get review, merge to main
  4. Deploy immediately after merge
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Start work
git checkout main
git pull
git checkout -b feature/new-feature

# Make changes, push
git add .
git commit -m "Add new feature"
git push -u origin feature/new-feature

# Open PR, get review, merge via GitHub
# Deploy happens automatically

Pros:

  • Simple, easy to understand
  • Fast iteration (ship multiple times per day)
  • Less merge conflict pain

Cons:

  • Requires solid CI/CD and testing
  • No staging in the workflow itself
  • Harder for versioned releases

Trunk-Based Development

Even simpler: everyone commits to main (trunk).

main(short-livedfeaturebranches,optional)

How it works:

  1. Everyone commits to main (directly or via very short-lived branches)
  2. Branches live hours, not days
  3. Feature flags hide incomplete work
  4. Deploy continuously
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Option 1: Direct to main
git checkout main
git pull
# Make small change
git add .
git commit -m "Add button to checkout page"
git push

# Option 2: Short-lived branch
git checkout -b small-fix
# Work for a few hours max
git checkout main
git merge small-fix
git push

Pros:

  • Minimal merge conflicts
  • Fastest iteration
  • Forces small, incremental changes
  • Works great with feature flags

Cons:

  • Requires discipline (small commits)
  • Needs excellent CI/CD
  • Feature flags add complexity
  • Scary without good test coverage

GitLab Flow

Middle ground: environment branches.

main(psrtoadguicntgfieoant)ure/new-feature

Or with release branches:

main(l12fa..et00ae--tsssutttr)aaebb/llneeew-feature

How it works:

  1. Feature branches merge to main
  2. main deploys to staging automatically
  3. Promote to production by merging main to production branch
  4. Or: maintain stable branches for each release

How to Choose

Use Git Flow if:

  • You ship versioned releases (v1.0, v2.0)
  • Multiple versions need parallel support
  • You have dedicated release managers
  • Compliance requires formal release process

Use GitHub Flow if:

  • You deploy continuously (SaaS, web apps)
  • You have solid CI/CD
  • Team is small to medium
  • You want simplicity

Use Trunk-Based if:

  • You deploy many times per day
  • You have excellent test coverage
  • Team is disciplined about small changes
  • You’re comfortable with feature flags

Use GitLab Flow if:

  • You need environment promotion (staging → prod)
  • You want something between Git Flow and GitHub Flow
  • You maintain multiple release versions

Branch Naming Conventions

Consistent naming helps automation and readability:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Feature branches
feature/ABC-123-user-authentication
feature/add-payment-processing

# Bug fixes
fix/ABC-456-login-crash
bugfix/null-pointer-checkout

# Hotfixes (production emergencies)
hotfix/critical-security-patch

# Releases
release/2.1.0
release/2024-03-04

# Experiments
experiment/new-checkout-flow
spike/evaluate-new-db

Automate enforcement:

1
2
3
4
5
6
7
# GitHub Actions: check branch name
- name: Check branch name
  run: |
    if [[ ! "${{ github.head_ref }}" =~ ^(feature|fix|hotfix|release)/ ]]; then
      echo "Branch name must start with feature/, fix/, hotfix/, or release/"
      exit 1
    fi

Commit Message Standards

Use conventional commits for automated changelogs:

tffdrtcyeioeehpaxcfsoet(satr((c(c(esahrtu(cueeosdotcareephkd(rpe)oma)s):uep:):t)i:a):)add::duedudpshpedcOadxrarAnatetiudtrgeptleaithecsdi2itteoenrpnlmsvaeoptatngtalidiylioenldnncaacatttiriieetoossnntssltoegpisc

Enforce with commitlint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', [
      'feat', 'fix', 'docs', 'style', 
      'refactor', 'test', 'chore', 'perf'
    ]],
    'subject-max-length': [2, 'always', 72],
  },
};

Pull Request Best Practices

Keep PRs Small

#"#""""IAAAABmGddddapodddddlo:edpSppm:ataa2eyryy0n2mimm0t0epee00nennetttlnlAitifPceninoIorerernrsesmifo,,nirpvtr5a5aemh0ylgaamfirtnfeidaidinlatolltetiniesionssonpgyn"a"s"gtee"m"

PR Template

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- .github/pull_request_template.md -->
## What
Brief description of changes

## Why
Link to issue or explanation

## How
Technical approach taken

## Testing
- [ ] Unit tests added
- [ ] Integration tests pass
- [ ] Manual testing done

## Screenshots
(if applicable)

Review Checklist

Reviewers should check:

  • Does the code do what it claims?
  • Are there tests?
  • Are edge cases handled?
  • Is it readable?
  • Any security concerns?

Merge Strategies

Merge Commit

1
git merge --no-ff feature-branch

Preserves full history. Good for seeing what was in each feature.

Squash and Merge

1
2
git merge --squash feature-branch
git commit -m "Add feature X"

Clean history. One commit per feature. Loses individual commit detail.

Rebase and Merge

1
2
3
git rebase main
git checkout main
git merge feature-branch

Linear history. Rewrites commits. Can be confusing if not careful.

My recommendation: Squash for features, merge commit for releases.

Handling Conflicts

Prevention

  • Small, frequent PRs
  • Pull from main often
  • Communicate about shared files
  • Use code owners for critical paths

Resolution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Update your branch
git checkout feature-branch
git fetch origin
git rebase origin/main

# Resolve conflicts
# Edit conflicted files
git add .
git rebase --continue

# Force push (your branch only!)
git push --force-with-lease

Quick Reference

WorkflowBest ForComplexityDeploy Speed
Git FlowVersioned releasesHighSlow
GitHub FlowContinuous deploymentLowFast
Trunk-BasedHigh-frequency deploysLowFastest
GitLab FlowEnvironment promotionMediumMedium

The best workflow is one your team actually follows. Start simple, add complexity only when needed.