EngineeringCode Quality

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
  • main is always deployable
  • Suitable for teams with strong CI and feature flags

GitFlow (suitable for structured release cycles)

  • main — production-ready code only
  • develop — integration branch
  • feature/* — individual features, branched from develop
  • release/* — release preparation branches
  • hotfix/* — emergency fixes branched directly from main
  • 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 #123

Types:

TypeWhen to use
featA new user-facing feature
fixA bug fix
refactorCode restructure, no behavior change
testAdding or updating tests
docsDocumentation only
choreTooling, deps, build system — no production code
perfPerformance improvement
ciCI/CD configuration changes

Rules:

  • The summary line is imperative mood, lowercase, no period: feat(auth): add OAuth2 login not Added 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.

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-789

Most 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 componentWhen to increment
MAJORBreaking change — existing callers must update
MINORNew backward-compatible feature
PATCHBackward-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` endpoint

This 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:

FilePurpose
README.mdProject overview, setup instructions, links to docs
CONTRIBUTING.mdHow to contribute — branching, commits, PR process
CHANGELOG.mdRelease history
.gitignoreFiles 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.db

Do 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 .env files — add them to .gitignore immediately 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:

ToolHow it works
GitHub Secret ScanningBuilt-in; scans public and (on paid plans) private repos automatically
gitleaksOpen-source; runs in CI or as a pre-commit hook
TruffleHogScans 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@v2

Response (if a secret is found in history):

  1. Rotate the credential immediately — assume it is already compromised
  2. Use git filter-repo to remove it from history
  3. Force-push the cleaned history (coordinate with the team)
  4. Audit access logs for the credential

Automation Summary

PracticeEnforcement mechanism
Conventional commitscommitlint + commit-msg hook
Branch name formatCI branch name validation step
Branch protectionRepository host settings (GitHub branch rules)
Secrets scanninggitleaks in CI + GitHub Secret Scanning
Release tagging + CHANGELOGsemantic-release or release-please in CI
Dependency hygieneDependabot or Renovate
Required PR checks passBranch 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.