Version Control Best Practices — Branching, Commits, and Code Reviews
Your Git log reads like a diary written by someone having a breakdown: "fix stuff", "WIP", "please work", "final final v2", "asdfjkl". If your future self (or your teammates) cannot understand what happened from reading git log --oneline, you are doing version control wrong.
Conventional Commits: Stop Writing Terrible Commit Messages
The Conventional Commits specification gives your commit history structure, making it machine-readable and human-understandable.
# Format: <type>(<scope>): <description>
# Types:
feat: # A new feature
fix: # A bug fix
docs: # Documentation only
style: # Formatting, no code change
refactor: # Code change that neither fixes a bug nor adds a feature
perf: # Performance improvement
test: # Adding or updating tests
build: # Build system or dependencies
ci: # CI configuration changes
chore: # Other changes (updating .gitignore, etc.)
# Examples:
git commit -m "feat(auth): add JWT token refresh endpoint"
git commit -m "fix(payment): resolve double-charge on retry"
git commit -m "docs(api): update OpenAPI spec for v2 endpoints"
git commit -m "refactor(db): extract connection pool into separate module"
git commit -m "ci(actions): add Node.js 22 to test matrix"
# Breaking changes — add ! after type or BREAKING CHANGE in footer
git commit -m "feat(api)!: change authentication from API keys to OAuth2"
Why "What" Is Not Enough — Write the "Why"
# BAD — tells you what, but not why
git commit -m "change timeout to 30s"
# GOOD — tells you why
git commit -m "fix(api): increase timeout to 30s to prevent 504 on large exports
The export endpoint processes up to 100k rows, which takes ~25s
on average. The previous 10s timeout caused intermittent 504s
for customers with large datasets.
Closes #1234"
# BAD — meaningless
git commit -m "update config"
# GOOD — actionable context
git commit -m "perf(cache): increase Redis max-memory to 2GB
Traffic analysis shows cache hit rate dropped to 60% after the
recent product catalog expansion. Increasing memory restores
the 95%+ hit rate."
Atomic Commits: One Logical Change Per Commit
Each commit should represent one complete, logical change that can be understood (and reverted) independently.
# BAD — one giant commit with unrelated changes
git add .
git commit -m "add login, fix header bug, update deps, refactor utils"
# GOOD — separate, atomic commits
git add src/auth/login.ts src/auth/login.test.ts
git commit -m "feat(auth): add email/password login endpoint"
git add src/components/Header.tsx
git commit -m "fix(ui): correct header z-index on mobile"
git add package.json package-lock.json
git commit -m "build(deps): upgrade react to 18.3.1"
git add src/utils/string.ts src/utils/string.test.ts
git commit -m "refactor(utils): extract slug generation into utility function"
Use git add -p (patch mode) to stage specific hunks within a file when you have made multiple unrelated changes.
# Stage only specific changes within a file
git add -p src/api/server.ts
# Git shows each hunk and asks: Stage this? [y/n/s/q/...]
# y = stage this hunk
# n = skip this hunk
# s = split into smaller hunks
.gitignore for DevOps Projects
A proper .gitignore prevents secrets, build artifacts, and OS junk from ever reaching your repository.
# Dependencies
node_modules/
vendor/
.venv/
__pycache__/
# Build outputs
dist/
build/
*.o
*.pyc
# Environment and secrets — NEVER commit these
.env
.env.local
.env.production
*.pem
*.key
credentials.json
serviceAccountKey.json
terraform.tfvars
*.tfstate
*.tfstate.backup
# IDE and OS files
.idea/
.vscode/settings.json
*.swp
*.swo
.DS_Store
Thumbs.db
# Docker
docker-compose.override.yml
# Terraform
.terraform/
crash.log
# Logs
*.log
logs/
# Coverage reports
coverage/
.nyc_output/
htmlcov/
Pro tip: Use git check-ignore -v <file> to debug why a file is or is not being ignored.
Branch Naming Conventions
Consistent branch names make automation easier and history clearer.
# Format: <type>/<ticket-id>-<short-description>
# Feature branches
git checkout -b feat/JIRA-1234-user-authentication
git checkout -b feat/GH-56-search-autocomplete
# Bug fix branches
git checkout -b fix/JIRA-5678-login-redirect-loop
git checkout -b fix/GH-89-null-pointer-on-empty-cart
# Chore/maintenance branches
git checkout -b chore/upgrade-node-20
git checkout -b chore/JIRA-9999-update-ci-pipeline
# Documentation branches
git checkout -b docs/api-v2-migration-guide
# Hotfix branches (urgent production fixes)
git checkout -b hotfix/payment-double-charge
# Release branches (if using GitFlow)
git checkout -b release/2.5.0
Pull Request Templates
Create .github/pull_request_template.md to standardize code reviews:
## What does this PR do?
<!-- Brief description of the change -->
## Why is this change needed?
<!-- Link to issue/ticket, explain the motivation -->
Closes #
## Type of change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Refactoring (no functional changes)
- [ ] Documentation update
## Checklist
- [ ] I have written tests that cover my changes
- [ ] All new and existing tests pass
- [ ] I have updated documentation where needed
- [ ] My code follows the project style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have added comments for hard-to-understand areas
## Screenshots (if applicable)
<!-- Add screenshots for UI changes -->
## How to test
<!-- Step-by-step instructions for reviewers -->
1.
2.
3.
Git Hooks: Automate Quality Checks
Git hooks run scripts at specific points in the Git workflow. Use them to enforce standards before code reaches the remote.
# Install hooks manually
# Hooks live in .git/hooks/ — not committed to the repo
# pre-commit hook: lint and format before every commit
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
set -e
echo "Running linter..."
npm run lint --silent
echo "Running formatter check..."
npx prettier --check "src/**/*.{ts,tsx,js,json}" --silent
echo "Checking for secrets..."
if git diff --cached --name-only | xargs grep -l "AKIA\|password\s*=\s*['\"]" 2>/dev/null; then
echo "ERROR: Possible secrets detected in staged files!"
exit 1
fi
echo "Pre-commit checks passed!"
EOF
chmod +x .git/hooks/pre-commit
# commit-msg hook: enforce conventional commits
cat > .git/hooks/commit-msg << 'EOF'
#!/bin/bash
COMMIT_MSG=$(cat "$1")
PATTERN="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+\))?(!)?: .{1,72}"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "ERROR: Commit message does not follow Conventional Commits format."
echo "Expected: <type>(<scope>): <description>"
echo "Example: feat(auth): add JWT token refresh"
echo ""
echo "Your message: $COMMIT_MSG"
exit 1
fi
EOF
chmod +x .git/hooks/commit-msg
For team-wide hooks, use Husky (Node.js) or pre-commit (Python):
# Using Husky (Node.js projects)
npx husky init
echo "npm run lint && npm test" > .husky/pre-commit
# Using pre-commit framework (Python)
pip install pre-commit
# .pre-commit-config.yaml
cat << 'EOF'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: detect-private-key
- repo: https://github.com/commitizen-tools/commitizen
rev: v3.13.0
hooks:
- id: commitizen
stages: [commit-msg]
EOF
pre-commit install
pre-commit install --hook-type commit-msg
Signed Commits with GPG
Signed commits prove that you — not someone impersonating you — authored the code.
# Generate a GPG key
gpg --full-generate-key
# Choose: RSA and RSA, 4096 bits, key does not expire
# List your keys
gpg --list-secret-keys --keyid-format=long
# Output: sec rsa4096/ABC123DEF456 2025-04-19
# Tell Git to use your key
git config --global user.signingkey ABC123DEF456
git config --global commit.gpgsign true # Sign all commits
# Make a signed commit
git commit -S -m "feat(auth): add MFA support"
# Verify a signed commit
git log --show-signature -1
# Export your public key for GitHub
gpg --armor --export ABC123DEF456
# Paste this into GitHub > Settings > SSH and GPG keys > New GPG key
Once configured, your commits show a "Verified" badge on GitHub.
Monorepo vs Polyrepo
| Aspect | Monorepo | Polyrepo |
|---|---|---|
| Structure | All projects in one repo | Each project has its own repo |
| Code sharing | Easy (same repo) | Requires packages/modules |
| Dependency management | Unified (one lockfile) | Independent per repo |
| CI/CD | Complex (must detect changed projects) | Simple (repo = project) |
| Code reviews | Cross-project reviews easy | Reviews isolated to one project |
| Tooling | Needs monorepo tools (Nx, Turborepo, Bazel) | Standard Git workflow |
| Permissions | Harder (one repo, many teams) | Easy (repo-level access) |
| Clone size | Large (entire org codebase) | Small (just your project) |
| Refactoring | Atomic cross-project changes | Multi-repo coordinated changes |
| Who uses it | Google, Meta, Microsoft | Netflix, Amazon (mostly) |
Git Bisect: Find the Bug in O(log n)
Your app broke sometime in the last 50 commits. Instead of checking each one, use git bisect to binary search for the bad commit.
# Start bisecting
git bisect start
# Mark the current commit as bad (bug exists)
git bisect bad
# Mark a known good commit (bug did not exist)
git bisect good v2.0.0 # or a specific commit hash
# Git checks out a middle commit. Test it.
npm test
# If tests pass:
git bisect good
# If tests fail:
git bisect bad
# Git narrows the range and checks out the next middle commit.
# Repeat until Git identifies the exact commit that introduced the bug.
# Automate it with a test script:
git bisect start HEAD v2.0.0
git bisect run npm test
# Git will automatically find the first bad commit!
# When done, reset to your original HEAD
git bisect reset
With 50 commits, manual checking takes up to 50 steps. Bisect finds it in about 6.
Cherry-Pick and Interactive Rebase
# Cherry-pick: apply a specific commit to your current branch
git cherry-pick abc1234
# Useful for: applying a hotfix from main to a release branch
# Cherry-pick without committing (stage only)
git cherry-pick --no-commit abc1234
# Cherry-pick a range
git cherry-pick abc1234..def5678
Good version control is invisible when done right. Your teammates can understand what changed and why just from reading the Git log. Your CI catches problems before they reach production. Your branches merge cleanly because they are short-lived. That is the goal.
Next in our DevOps series, we will explore Testing in DevOps — from unit tests to E2E, shift-left testing, and how to build a test stage into your CI/CD pipeline that actually catches bugs.
