Git Best Practices
Git best practices turn a powerful tool into a reliable team workflow. This guide covers commit hygiene, branching, pull requests, rebase vs merge, secrets, gitignore, signed commits, and team conventions.
info
Every commit should be a coherent, reviewable unit. Prefer many small commits over one mega-commit that mixes formatting, features, and refactors.
- One logical change per commit — feature, fix, or chore, not all three
- Subject at most 72 chars; use body for why, not what
- Prefer conventional commits: feat:, fix:, docs:, chore:
- Stage with git add -p when a file contains unrelated hunks
- Do not commit generated artifacts, secrets, or editor junk
| 1 | # Bad: everything mixed |
| 2 | git add -A && git commit -m "updates" |
| 3 | |
| 4 | # Good: split by intent |
| 5 | git add src/auth/session.ts |
| 6 | git commit -m "feat(auth): refresh tokens before expiry" |
| 7 | |
| 8 | git add src/auth/session.test.ts |
| 9 | git commit -m "test(auth): cover refresh edge cases" |
| 10 | |
| 11 | git add README.md |
| 12 | git commit -m "docs(auth): document refresh flow" |
best practice
A good subject summarizes intent; the body explains motivation and trade-offs.
| 1 | feat(checkout): accept percentage coupons |
| 2 | |
| 3 | Why: marketing needs time-boxed discounts without redeploying prices. |
| 4 | Coupons are validated server-side; expired codes return 422. |
| 5 | |
| 6 | Breaking: CouponService.validate now requires currency code. |
Conventional Commits cheat sheet
| Type | Use for |
|---|---|
| feat | New user-facing capability |
| fix | Bug fix |
| docs | Documentation only |
| style | Formatting; no logic change |
| refactor | Internal restructure |
| perf | Performance improvement |
| test | Tests only |
| chore | Tooling, deps, chores |
| ci | CI config |
| build | Build system |
Short-lived feature branches reduce merge pain. Agree as a team on trunk-based vs Git Flow — inconsistency is worse than either choice.
| Pattern | When | Risk |
|---|---|---|
| Trunk-based + short PRs | Most product teams | Needs good CI |
| Git Flow | Versioned releases / hotfixes | Long-lived branches drift |
| Release branches | Support multiple versions | Cherry-pick overhead |
| Forking | Open source contributors | Sync discipline |
| 1 | # Start from fresh main |
| 2 | git fetch origin |
| 3 | git switch -c feature/checkout-coupon origin/main |
| 4 | |
| 5 | # Keep up to date frequently |
| 6 | git fetch origin |
| 7 | git rebase origin/main |
| 8 | |
| 9 | # Delete after merge |
| 10 | git switch main |
| 11 | git pull --ff-only |
| 12 | git branch -d feature/checkout-coupon |
| 13 | git push origin --delete feature/checkout-coupon |
warning
PRs are a communication tool. Optimize for reviewer time: small diffs, clear description, linked issues, and green checks.
- Target under 400 changed lines when possible; split large features
- Description: problem, approach, test plan, screenshots if UI
- Self-review the diff before requesting review
- Respond to comments with commits or clear reasons
- Use gh pr create / templates for consistency
| 1 | gh pr create --title "feat(checkout): coupon codes" --body "## Summary |
| 2 | Adds coupon validation at checkout. |
| 3 | |
| 4 | ## Test plan |
| 5 | - [ ] Unit tests for CouponService |
| 6 | - [ ] Manual: apply valid/invalid/expired codes |
| 7 | - [ ] CI green" |
| 8 | gh pr checks |
| 9 | gh pr diff | head |
Both integrate histories. Rebase rewrites local commits for linearity; merge preserves exact branch topology.
| Situation | Prefer | Why |
|---|---|---|
| Update feature onto main (local) | rebase | Clean PR diff |
| Integrate finished feature to main | merge --no-ff or squash | Policy choice |
| Shared long-lived branch | merge | Avoid rewriting others commits |
| Public tag/release history | never rewrite | Use revert |
| 1 | # Local feature update (safe) |
| 2 | git switch feature/x |
| 3 | git fetch origin |
| 4 | git rebase origin/main |
| 5 | git push --force-with-lease |
| 6 | |
| 7 | # Integrate on main (example: merge commit) |
| 8 | git switch main |
| 9 | git merge --no-ff feature/x |
danger
Once a secret is pushed, treat it as compromised — rotate it even if you rewrite history.
- Ignore .env, key files, and local config via .gitignore
- Use secret scanning (gitleaks, trufflehog) in pre-commit and CI
- Store secrets in vaults / CI secret stores — never in the repo
- If leaked: rotate credentials first, then history cleanup if needed
| 1 | # Pre-commit mindset |
| 2 | git status -sb |
| 3 | git diff --staged | grep -iE 'api[_-]?key|secret|password|BEGIN RSA' && echo "ABORT" |
| 4 | |
| 5 | # Remove accidentally staged secret before commit |
| 6 | git restore --staged .env |
| 7 | echo ".env" >> .gitignore |
| 8 | git add .gitignore |
| 9 | git commit -m "chore: ignore env files" |
danger
Ignore build outputs, dependencies, IDE files, and secrets. Keep .gitignore reviewed like code.
| 1 | # dependencies |
| 2 | node_modules/ |
| 3 | .venv/ |
| 4 | vendor/ |
| 5 | |
| 6 | # build |
| 7 | dist/ |
| 8 | build/ |
| 9 | .next/ |
| 10 | coverage/ |
| 11 | |
| 12 | # env & secrets |
| 13 | .env |
| 14 | .env.* |
| 15 | !.env.example |
| 16 | *.pem |
| 17 | *.p12 |
| 18 | |
| 19 | # OS / IDE |
| 20 | .DS_Store |
| 21 | .idea/ |
| 22 | .vscode/* |
| 23 | !.vscode/extensions.json |
info
Signing proves authorship for protected branches and audit trails. Prefer SSH signing for simplicity or GPG for classic setups. See Signing.
| 1 | git config --global gpg.format ssh |
| 2 | git config --global user.signingkey ~/.ssh/id_ed25519.pub |
| 3 | git config --global commit.gpgsign true |
| 4 | git commit -S -m "feat: signed" |
| 5 | git log --show-signature -1 |
Write conventions down. Ambiguity causes force-push incidents.
| Convention | Example |
|---|---|
| Default branch | main |
| Branch names | feature/*, fix/*, chore/* |
| Commit style | Conventional Commits |
| PR size | Prefer under 400 LOC |
| Update strategy | rebase onto main before merge |
| Merge to main | Squash merge via GitHub |
| Protected branch | require reviews + signed commits + CI |
| Hotfix | branch from main; cherry-pick to release if needed |
| Tags | annotated semver vMAJOR.MINOR.PATCH |
| 1 | # .github/CODEOWNERS |
| 2 | * @org/platform |
| 3 | /docs/ @org/docs |
| 4 | /src/auth/ @org/security |
pro tip
Local hooks catch mistakes early; CI is the source of truth for mergeability.
| 1 | #!/usr/bin/env bash |
| 2 | # .git/hooks/pre-commit (or lefthook) |
| 3 | set -euo pipefail |
| 4 | if git diff --cached --name-only | grep -E '\.env$|id_rsa|\.pem$'; then |
| 5 | echo "Refusing to commit secret-like paths" >&2 |
| 6 | exit 1 |
| 7 | fi |
info
- git add . blindly before reading git status
- Force-pushing to shared branches
- Rewriting history to hide secrets instead of rotating
- Long-lived feature branches without rebasing
- Commit messages like fix, wip, asdf
- Committing node_modules or build artifacts
- Using reset --hard as a daily undo without understanding data loss
warning
- git status -sb is clean of secrets and junk
- Diff matches the PR description
- Tests pass locally for touched areas
- Rebased onto latest main without leftover conflict markers
- No force push to protected branches
| 1 | git status -sb |
| 2 | git diff --check |
| 3 | git log --oneline origin/main..HEAD |
| 4 | git push -u origin HEAD |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.