DEV Community

楊東霖
楊東霖

Posted on • Originally published at devplaybook.cc

Git Workflow Best Practices 2025: Team-Proven Strategies

Git Workflow Best Practices 2025: Team-Proven Strategies

Bad Git hygiene compounds. A messy history becomes a messy codebase becomes a messy team. This guide covers the practices that teams have standardized on in 2025 — from commit conventions to branching models to automated enforcement.

Choose Your Branching Strategy

There's no universal right answer, but here are the three most common models:

GitHub Flow (Recommended for most teams)

Simple: one main branch, short-lived feature branches, deploy from main.

main
├── feature/user-auth      ← branch, PR, merge, delete
├── fix/login-timeout      ← branch, PR, merge, delete
└── feature/dark-mode      ← branch, PR, merge, delete
Enter fullscreen mode Exit fullscreen mode

Best for: Continuous deployment, small-to-medium teams, SaaS products.

Gitflow (For release-based projects)

Adds develop, release/*, and hotfix/* branches:

main          ← production
develop       ← integration
├── feature/x ← branch from develop
├── release/1.2 ← branch from develop, merge to main + develop
└── hotfix/1.2.1 ← branch from main, merge to main + develop
Enter fullscreen mode Exit fullscreen mode

Best for: Mobile apps, libraries with versioned releases, enterprise software.

Trunk-Based Development

Everyone commits directly to main (with feature flags for incomplete features). Short-lived branches (< 1 day) allowed.

Best for: High-velocity teams with strong CI/CD and test coverage.

Write Better Commit Messages

Follow the Conventional Commits specification. It's machine-readable (useful for changelogs) and human-readable:

<type>(<scope>): <short summary>

[optional body]

[optional footer(s)]
Enter fullscreen mode Exit fullscreen mode

Types

Type When to use
feat New feature
fix Bug fix
docs Documentation only
style Formatting (no logic change)
refactor Code restructure (no feature/fix)
test Adding or fixing tests
chore Build process, dependency updates
perf Performance improvement

Examples

# Good
git commit -m "feat(auth): add OAuth2 login with Google"
git commit -m "fix(api): handle 429 rate limit response correctly"
git commit -m "docs(readme): add Docker setup instructions"

# Bad — vague and unhelpful
git commit -m "fix stuff"
git commit -m "WIP"
git commit -m "changes"
Enter fullscreen mode Exit fullscreen mode

Enforce this with Commitlint + a git hook.

Branching Naming Conventions

Consistent branch names help everyone at a glance:

feature/VIC-123-user-authentication
fix/VIC-456-broken-login-redirect
chore/upgrade-dependencies-march-2025
docs/update-api-reference
hotfix/critical-payment-bug
Enter fullscreen mode Exit fullscreen mode

Pattern: <type>/<ticket-id>-<short-description>

Enforce with a pre-push hook or CI check.

Rebase vs Merge: When to Use Each

Merge (preserves history)

git checkout main
git merge feature/new-login
Enter fullscreen mode Exit fullscreen mode

Creates a merge commit. History shows exactly when branches joined. Better for public branches where others may have based work on yours.

Rebase (clean linear history)

git checkout feature/new-login
git rebase main
Enter fullscreen mode Exit fullscreen mode

Replays your commits on top of the target branch. History looks linear and clean. Never rebase shared/public branches — it rewrites commit hashes.

The practical rule

  • Before opening a PR: rebase your feature branch onto main to catch conflicts early
  • When merging a PR: use squash merge (one clean commit) or merge commit (preserves context) depending on team preference
  • Never force-push to main

PR Review Best Practices

As the author

## What this PR does
[1-3 bullet points explaining the change]

## Why
[Business or technical motivation]

## Testing
- [ ] Unit tests pass
- [ ] Manual test: login with Google → redirects correctly
- [ ] Checked mobile view

## Screenshots (if UI change)
[before/after]
Enter fullscreen mode Exit fullscreen mode
  • Keep PRs small: under 400 lines changed is ideal
  • Link the ticket/issue
  • Self-review before requesting others

As the reviewer

  • Review within 24 hours (set team SLA)
  • Comment on the code, not the person
  • Use prefixes: nit: (optional style), blocking: (must fix), q: (genuine question)
nit: could use Array.from() here for clarity
blocking: this will crash if user.profile is null
q: why is this using localStorage instead of sessionStorage?
Enter fullscreen mode Exit fullscreen mode

Automate Git Hygiene with Hooks

Use Husky to run checks automatically:

npm install --save-dev husky
npx husky init
Enter fullscreen mode Exit fullscreen mode

Pre-commit hook: run linter and formatter

# .husky/pre-commit
npx lint-staged
Enter fullscreen mode Exit fullscreen mode
// package.json
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,css}": ["prettier --write"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Commit-msg hook: enforce Conventional Commits

# .husky/commit-msg
npx --no -- commitlint --edit $1
Enter fullscreen mode Exit fullscreen mode

Pre-push hook: run tests

# .husky/pre-push
npm test -- --passWithNoTests
Enter fullscreen mode Exit fullscreen mode

Keeping History Clean

Interactive rebase to clean up before PR

# Squash last 3 commits
git rebase -i HEAD~3
Enter fullscreen mode Exit fullscreen mode

In the editor, use:

  • pick — keep the commit
  • squash (or s) — combine with previous
  • reword (or r) — keep but edit the message
  • drop (or d) — remove completely

Amend the last commit (before pushing)

git commit --amend
# Opens editor to change message

git commit --amend --no-edit
# Keep message, add staged changes
Enter fullscreen mode Exit fullscreen mode

Fix a commit buried in history

# Stage the fix
git add path/to/fix.js

# Create a fixup commit targeting the hash
git commit --fixup <commit-hash>

# Auto-squash it in
git rebase -i --autosquash HEAD~5
Enter fullscreen mode Exit fullscreen mode

.gitignore Essentials

Always include:

# Secrets and environment
.env
.env.local
.env.*.local

# Dependencies
node_modules/
vendor/

# Build output
dist/
build/
.next/

# Editor files
.vscode/settings.json
.idea/
*.swp

# OS artifacts
.DS_Store
Thumbs.db

# Logs
*.log
logs/
Enter fullscreen mode Exit fullscreen mode

Commit .env.example with placeholder values so teammates know what variables are needed:

# .env.example
DATABASE_URL=postgresql://localhost:5432/mydb
API_KEY=your-api-key-here
Enter fullscreen mode Exit fullscreen mode

CI/CD Integration

Every push and PR should automatically:

  1. Run linting (eslint, flake8, etc.)
  2. Run tests (unit + integration)
  3. Check types (tsc --noEmit for TypeScript)
  4. Build the project (catch build-time errors)

Sample GitHub Actions workflow:

name: CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm test
      - run: npm run build
Enter fullscreen mode Exit fullscreen mode

The Non-Negotiables

  1. Never commit secrets — use environment variables and .gitignore
  2. Never force-push to main — use protected branch rules
  3. One logical change per commit — makes git bisect and reverting possible
  4. Write commit messages in imperative mood — "Add login" not "Added login"
  5. Delete merged branches — keep the repo clean

Good Git workflow isn't about being strict for its own sake. It's about making collaboration predictable and making future-you able to understand what past-you was thinking.


Level Up Your Dev Workflow

Found this useful? Explore DevPlaybook — cheat sheets, tool comparisons, and hands-on guides for modern developers.

🛒 Get the DevToolkit Starter Kit on Gumroad — 40+ browser-based dev tools, source code + deployment guide included.

Top comments (0)