Git Best Practices
Branching strategy, commit conventions, release hygiene, and secrets management for clean, maintainable repositories.
Overview
Git is the shared source of truth for every change that enters a codebase. How a team uses it — how branches are named, how commits are written, how releases are tagged, how secrets are kept out — determines how easy it is to understand history, trace bugs, coordinate work, and recover from incidents.
This page covers the conventions and practices that keep repositories clean and teams moving fast. The automation column for each practice indicates how much of the enforcement can be handled by tooling so that compliance doesn't depend on individual discipline.
For automated checks that run before commits are created, see Pre-Commit Hooks. For how changes are reviewed before they merge, see Code Review Best Practices.
Why It Matters
A clean git history is a debugging tool. git log, git bisect, and git blame are only useful when commits are small, well-named, and coherent. A history full of "wip", "fix", "update stuff" commits cannot be bisected or blamed meaningfully.
Branch hygiene prevents integration debt. Long-lived branches diverge. Diverged branches produce large merge conflicts. Large merge conflicts take time to resolve and introduce bugs in the resolution. Short-lived branches merged frequently are the primary defense.
Secrets in git are permanent. Even after a commit is amended or a file deleted, secrets remain accessible in history. A credential committed to a public repository must be treated as compromised immediately, regardless of any subsequent cleanup.
Consistent conventions enable automation. Conventional Commits aren't just readable — they are machine-parseable. Tools like semantic-release and release-please generate changelogs and bump version numbers automatically, because the commit history has enough structure to make that possible.
Standards & Best Practices
Branching strategy
Choose one strategy and apply it consistently across all repositories on the team. Two approaches cover most teams:
Trunk Based Development (recommended for most teams)
- All engineers work on short-lived feature branches off
main - Branches live for 1–2 days maximum, then merge back
mainis always deployable- Suitable for teams with strong CI and feature flags
GitFlow (suitable for structured release cycles)
main— production-ready code onlydevelop— integration branchfeature/*— individual features, branched fromdeveloprelease/*— release preparation brancheshotfix/*— emergency fixes branched directly frommain- Suitable for teams shipping versioned software on a scheduled cadence
The choice between them matters less than consistency. Mixing approaches within a team creates confusion about what is safe to deploy and what isn't.
Keep branches short-lived
Feature branches should exist for 2–3 days maximum. Branches that live longer diverge further from the base, produce larger merge conflicts, and make it harder to review and understand what changed.
If a feature takes longer than a few days to build:
- Break it into smaller, independently-mergeable pieces
- Merge partial implementations behind a feature flag
- Merge non-functional refactoring and scaffolding first, then the behavioral change
Conventional Commits
All commits must follow the Conventional Commits specification:
type(scope): short summary in imperative mood
Optional longer body explaining the why, not the what.
Optional footer: BREAKING CHANGE: ..., Fixes #123Types:
| Type | When to use |
|---|---|
feat | A new user-facing feature |
fix | A bug fix |
refactor | Code restructure, no behavior change |
test | Adding or updating tests |
docs | Documentation only |
chore | Tooling, deps, build system — no production code |
perf | Performance improvement |
ci | CI/CD configuration changes |
Rules:
- The summary line is imperative mood, lowercase, no period:
feat(auth): add OAuth2 loginnotAdded OAuth2 Login. - 72-character limit on the summary line
- The body explains why, not what — the diff shows what
Enforce this with commitlint in a commit-msg hook. See Pre-Commit Hooks for setup.
Branch naming
Use a consistent naming pattern so branch protection rules and CI can act on branch type:
feature/<ticket-id>-short-description
bugfix/<ticket-id>-short-description
hotfix/<ticket-id>-short-description
release/v<version>
chore/<short-description>Enforce this in CI with a branch name validation step.
Protect the main branch
main (and develop in GitFlow) must have branch protection rules:
- Require pull request before merging — no direct pushes
- Require CI checks to pass before merge
- Require at least one approving review
- Dismiss stale reviews when new commits are pushed
- Restrict who can push to the branch
These rules are configured in the repository host (GitHub, GitLab, Bitbucket) and are not bypassable by default.
Link PRs to issues or tickets
Every pull request should reference the work item it closes. This creates a traceable link from code change to business requirement:
Fixes #123
Closes PROJ-456
Relates to PROJ-789Most issue trackers auto-close the linked issue when the PR merges.
Squash or rebase — no merge commits
Merge commits (Merge branch 'feature/x' into main) pollute the history with noise. Use one of:
- Squash merge — collapses the entire branch into one commit on
main. Keeps history linear. Best when the branch commits are messy WIP commits. - Rebase — replays branch commits on top of
main. Keeps individual commits but keeps history linear. Best when branch commits are already clean and meaningful.
Configure the repository host to allow only squash or rebase merges, disabling regular merge commits.
Release Practices
Semantic versioning
All releases must use Semantic Versioning: vMAJOR.MINOR.PATCH.
| Version component | When to increment |
|---|---|
MAJOR | Breaking change — existing callers must update |
MINOR | New backward-compatible feature |
PATCH | Backward-compatible bug fix |
Pre-release suffixes: v1.2.0-beta.1, v2.0.0-rc.3.
Tag every production release
Every production deployment should correspond to a git tag. Tags provide a stable reference for:
- Reproducing production bugs (checkout the exact version)
- Rollback (redeploy from the previous tag)
- Generating changelogs (diff between tags)
Maintain a CHANGELOG
Every release should have a corresponding CHANGELOG.md entry categorized by change type:
## [1.4.2] - 2026-04-17
### Added
- OAuth2 login support
### Fixed
- Session timeout not resetting on activity
### Removed
- Deprecated `/api/v1/users` endpointThis can be fully automated with semantic-release or release-please when Conventional Commits are in use — the tool reads the commit messages and generates the changelog entry automatically.
Repository Hygiene
Required files
Every repository should include:
| File | Purpose |
|---|---|
README.md | Project overview, setup instructions, links to docs |
CONTRIBUTING.md | How to contribute — branching, commits, PR process |
CHANGELOG.md | Release history |
.gitignore | Files and directories git should not track |
.gitignore essentials
# Dependencies
node_modules/
vendor/
# Build output
dist/
.next/
*.pyc
__pycache__/
# Environment
.env
.env.local
.env.*.local
# Editor
.vscode/
.idea/
*.swp
# OS
.DS_Store
Thumbs.dbDo not commit binaries, build artifacts, or generated files. If it can be produced from source, it doesn't belong in git.
Secrets Must Never Enter Git
A secret committed to a repository — even briefly, even to a private repo — must be treated as compromised. Git history is permanent; git revert and file deletion do not remove data from history.
Prevention (highest priority):
- Never commit
.envfiles — add them to.gitignoreimmediately when the project is created - Use environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) for all credentials
- Store example values in
.env.example(no real credentials)
Detection (automated): Enable secrets scanning tools that inspect commits and flag credentials before they land:
| Tool | How it works |
|---|---|
| GitHub Secret Scanning | Built-in; scans public and (on paid plans) private repos automatically |
gitleaks | Open-source; runs in CI or as a pre-commit hook |
| TruffleHog | Scans git history for high-entropy strings and known secret patterns |
Run gitleaks in CI as a required check:
- name: Scan for secrets
uses: gitleaks/gitleaks-action@v2Response (if a secret is found in history):
- Rotate the credential immediately — assume it is already compromised
- Use
git filter-repoto remove it from history - Force-push the cleaned history (coordinate with the team)
- Audit access logs for the credential
Automation Summary
| Practice | Enforcement mechanism |
|---|---|
| Conventional commits | commitlint + commit-msg hook |
| Branch name format | CI branch name validation step |
| Branch protection | Repository host settings (GitHub branch rules) |
| Secrets scanning | gitleaks in CI + GitHub Secret Scanning |
| Release tagging + CHANGELOG | semantic-release or release-please in CI |
| Dependency hygiene | Dependabot or Renovate |
| Required PR checks pass | Branch protection (required status checks) |
Common Pitfalls
Long-lived feature branches. A branch that lives for two weeks is a merge conflict waiting to happen and a code review that is too large to review effectively. Break the work down.
Committing directly to main. Even for "quick fixes," direct commits to the main branch bypass review, bypass CI, and create an unpredictable deployment surface. Branch protection should make this impossible by default.
Treating git history as disposable. Amending commits, force-pushing shared branches, and squashing everything into one commit before review all discard context. The history is documentation — write it that way.
Secrets in git, even "temporarily." There is no temporary in git history. If it was committed, it was potentially visible to anyone with repository access (and cached by any git mirrors or CI systems that fetched it). Rotate immediately.
Inconsistent commit style. A repository where some engineers write Conventional Commits and others write "fix stuff" cannot be automatically released or have its changelog generated. Consistency is the prerequisite for automation — enforce it with tooling, not convention.