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
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
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)]
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"
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
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
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
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:
rebaseyour feature branch onto main to catch conflicts early -
When merging a PR: use
squash merge(one clean commit) ormerge 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]
- 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?
Automate Git Hygiene with Hooks
Use Husky to run checks automatically:
npm install --save-dev husky
npx husky init
Pre-commit hook: run linter and formatter
# .husky/pre-commit
npx lint-staged
// package.json
{
"lint-staged": {
"*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,css}": ["prettier --write"]
}
}
Commit-msg hook: enforce Conventional Commits
# .husky/commit-msg
npx --no -- commitlint --edit $1
Pre-push hook: run tests
# .husky/pre-push
npm test -- --passWithNoTests
Keeping History Clean
Interactive rebase to clean up before PR
# Squash last 3 commits
git rebase -i HEAD~3
In the editor, use:
-
pick— keep the commit -
squash(ors) — combine with previous -
reword(orr) — keep but edit the message -
drop(ord) — 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
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
.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/
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
CI/CD Integration
Every push and PR should automatically:
-
Run linting (
eslint,flake8, etc.) - Run tests (unit + integration)
-
Check types (
tsc --noEmitfor TypeScript) - 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
The Non-Negotiables
-
Never commit secrets — use environment variables and
.gitignore - Never force-push to main — use protected branch rules
-
One logical change per commit — makes
git bisectand reverting possible - Write commit messages in imperative mood — "Add login" not "Added login"
- 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)