Pre-commit Hooks Implementation - Complete Guide
Published: September 25, 2024 | Reading time: 24 minutes
Pre-commit Hooks Overview
Pre-commit hooks ensure code quality and consistency before commits are made:
Pre-commit Benefits
# Pre-commit Hooks Benefits
- Automated code quality checks
- Consistent code formatting
- Error prevention
- Team collaboration
- Reduced code review time
- Automated testing
- Security validation
Git Hooks Setup
Basic Git Hooks Implementation
Git Hooks Setup
# Pre-commit Hooks Implementation
# 1. Basic Git Hook Setup
# .git/hooks/pre-commit
#!/bin/sh
# Run ESLint
echo "Running ESLint..."
npm run lint
if [ $? -ne 0 ]; then
echo "ESLint failed. Please fix the errors before committing."
exit 1
fi
# Run Prettier
echo "Running Prettier..."
npm run format:check
if [ $? -ne 0 ]; then
echo "Code formatting issues found. Please run 'npm run format' to fix."
exit 1
fi
# Run tests
echo "Running tests..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed. Please fix the issues before committing."
exit 1
fi
echo "All checks passed!"
exit 0
# 2. Advanced Git Hook
# .git/hooks/pre-commit
#!/bin/sh
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "${YELLOW}Running pre-commit checks...${NC}"
# Check if we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "${RED}Error: Not in a git repository${NC}"
exit 1
fi
# Get staged files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx|json|css|md)$')
if [ -z "$STAGED_FILES" ]; then
echo "${GREEN}No relevant files staged for commit${NC}"
exit 0
fi
echo "${YELLOW}Staged files:${NC}"
echo "$STAGED_FILES"
# Run ESLint on staged files
echo "${YELLOW}Running ESLint...${NC}"
npm run lint -- --fix
if [ $? -ne 0 ]; then
echo "${RED}ESLint failed. Please fix the errors before committing.${NC}"
exit 1
fi
# Run Prettier on staged files
echo "${YELLOW}Running Prettier...${NC}"
npm run format -- --write
if [ $? -ne 0 ]; then
echo "${RED}Prettier failed. Please fix the formatting issues before committing.${NC}"
exit 1
fi
# Run tests
echo "${YELLOW}Running tests...${NC}"
npm test
if [ $? -ne 0 ]; then
echo "${RED}Tests failed. Please fix the issues before committing.${NC}"
exit 1
fi
# Check for console.log statements
echo "${YELLOW}Checking for console.log statements...${NC}"
if echo "$STAGED_FILES" | xargs grep -l "console\.log" > /dev/null 2>&1; then
echo "${RED}Warning: console.log statements found in staged files${NC}"
echo "Files with console.log:"
echo "$STAGED_FILES" | xargs grep -l "console\.log"
echo "Consider removing them before committing."
fi
# Check for TODO comments
echo "${YELLOW}Checking for TODO comments...${NC}"
if echo "$STAGED_FILES" | xargs grep -l "TODO\|FIXME\|HACK" > /dev/null 2>&1; then
echo "${YELLOW}Warning: TODO/FIXME/HACK comments found${NC}"
echo "Files with TODO comments:"
echo "$STAGED_FILES" | xargs grep -l "TODO\|FIXME\|HACK"
fi
echo "${GREEN}All pre-commit checks passed!${NC}"
exit 0
# 3. Husky Setup
# Installation
npm install --save-dev husky
# Initialize Husky
npx husky install
# Add prepare script
npm pkg set scripts.prepare="husky install"
# Create pre-commit hook
npx husky add .husky/pre-commit "npm test"
# 4. Husky Configuration
# package.json
{
"scripts": {
"prepare": "husky install",
"pre-commit": "lint-staged",
"lint": "eslint .",
"format": "prettier --write .",
"test": "jest"
},
"devDependencies": {
"husky": "^8.0.0",
"lint-staged": "^13.0.0"
}
}
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
# 5. Lint-staged Configuration
# package.json
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
],
"*.{js,jsx,ts,tsx,json,css,md}": [
"git add"
]
}
}
# 6. Advanced Lint-staged Configuration
# package.json
{
"lint-staged": {
"*.{js,jsx}": [
"eslint --fix",
"prettier --write",
"jest --bail --findRelatedTests"
],
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write",
"tsc --noEmit",
"jest --bail --findRelatedTests"
],
"*.{json,css,md}": [
"prettier --write"
],
"*.{js,jsx,ts,tsx,json,css,md}": [
"git add"
]
}
}
# 7. Custom Hook Scripts
# scripts/pre-commit-checks.js
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
class PreCommitChecker {
constructor() {
this.errors = [];
this.warnings = [];
}
async runChecks() {
console.log('Running pre-commit checks...');
try {
await this.checkESLint();
await this.checkPrettier();
await this.checkTests();
await this.checkConsoleLogs();
await this.checkTodos();
this.reportResults();
} catch (error) {
console.error('Pre-commit check failed:', error.message);
process.exit(1);
}
}
async checkESLint() {
console.log('Checking ESLint...');
try {
execSync('npm run lint', { stdio: 'inherit' });
} catch (error) {
this.errors.push('ESLint failed');
throw error;
}
}
async checkPrettier() {
console.log('Checking Prettier...');
try {
execSync('npm run format:check', { stdio: 'inherit' });
} catch (error) {
this.errors.push('Prettier formatting issues found');
throw error;
}
}
async checkTests() {
console.log('Running tests...');
try {
execSync('npm test', { stdio: 'inherit' });
} catch (error) {
this.errors.push('Tests failed');
throw error;
}
}
async checkConsoleLogs() {
console.log('Checking for console.log statements...');
const stagedFiles = this.getStagedFiles();
const filesWithConsoleLog = [];
stagedFiles.forEach(file => {
if (file.endsWith('.js') || file.endsWith('.jsx') || file.endsWith('.ts') || file.endsWith('.tsx')) {
const content = fs.readFileSync(file, 'utf8');
if (content.includes('console.log')) {
filesWithConsoleLog.push(file);
}
}
});
if (filesWithConsoleLog.length > 0) {
this.warnings.push(`Console.log statements found in: ${filesWithConsoleLog.join(', ')}`);
}
}
async checkTodos() {
console.log('Checking for TODO comments...');
const stagedFiles = this.getStagedFiles();
const filesWithTodos = [];
stagedFiles.forEach(file => {
if (file.endsWith('.js') || file.endsWith('.jsx') || file.endsWith('.ts') || file.endsWith('.tsx')) {
const content = fs.readFileSync(file, 'utf8');
if (content.includes('TODO') || content.includes('FIXME') || content.includes('HACK')) {
filesWithTodos.push(file);
}
}
});
if (filesWithTodos.length > 0) {
this.warnings.push(`TODO comments found in: ${filesWithTodos.join(', ')}`);
}
}
getStagedFiles() {
try {
const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' });
return output.trim().split('\n').filter(file => file.length > 0);
} catch (error) {
return [];
}
}
reportResults() {
if (this.errors.length > 0) {
console.error('Errors found:');
this.errors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
if (this.warnings.length > 0) {
console.warn('Warnings:');
this.warnings.forEach(warning => console.warn(` - ${warning}`));
}
console.log('All pre-commit checks passed!');
}
}
const checker = new PreCommitChecker();
checker.runChecks();
# 8. TypeScript Pre-commit Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run TypeScript compiler
echo "Running TypeScript compiler..."
npx tsc --noEmit
if [ $? -ne 0 ]; then
echo "TypeScript compilation failed"
exit 1
fi
# Run ESLint
echo "Running ESLint..."
npx eslint . --ext .ts,.tsx
if [ $? -ne 0 ]; then
echo "ESLint failed"
exit 1
fi
# Run Prettier
echo "Running Prettier..."
npx prettier --check .
if [ $? -ne 0 ]; then
echo "Prettier formatting issues found"
exit 1
fi
# Run tests
echo "Running tests..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed"
exit 1
fi
echo "All checks passed!"
# 9. React Pre-commit Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run ESLint
echo "Running ESLint..."
npx eslint . --ext .js,.jsx,.ts,.tsx
if [ $? -ne 0 ]; then
echo "ESLint failed"
exit 1
fi
# Run Prettier
echo "Running Prettier..."
npx prettier --check .
if [ $? -ne 0 ]; then
echo "Prettier formatting issues found"
exit 1
fi
# Run tests
echo "Running tests..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed"
exit 1
fi
# Check for console.log in production code
echo "Checking for console.log statements..."
if git diff --cached --name-only | xargs grep -l "console\.log" > /dev/null 2>&1; then
echo "Warning: console.log statements found in staged files"
echo "Consider removing them before committing"
fi
echo "All checks passed!"
# 10. Node.js Pre-commit Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run ESLint
echo "Running ESLint..."
npx eslint . --ext .js
if [ $? -ne 0 ]; then
echo "ESLint failed"
exit 1
fi
# Run Prettier
echo "Running Prettier..."
npx prettier --check .
if [ $? -ne 0 ]; then
echo "Prettier formatting issues found"
exit 1
fi
# Run tests
echo "Running tests..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed"
exit 1
fi
# Check for security vulnerabilities
echo "Checking for security vulnerabilities..."
npm audit --audit-level moderate
if [ $? -ne 0 ]; then
echo "Security vulnerabilities found"
exit 1
fi
echo "All checks passed!"
Advanced Hook Configurations
Custom Hook Implementations
Advanced Hook Configurations
# Advanced Pre-commit Hook Configurations
# 1. Multi-language Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Get staged files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
# Check JavaScript/TypeScript files
JS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(js|jsx|ts|tsx)$')
if [ ! -z "$JS_FILES" ]; then
echo "Checking JavaScript/TypeScript files..."
echo "$JS_FILES" | xargs npx eslint --fix
if [ $? -ne 0 ]; then
echo "ESLint failed"
exit 1
fi
echo "$JS_FILES" | xargs npx prettier --write
if [ $? -ne 0 ]; then
echo "Prettier failed"
exit 1
fi
fi
# Check Python files
PY_FILES=$(echo "$STAGED_FILES" | grep -E '\.py$')
if [ ! -z "$PY_FILES" ]; then
echo "Checking Python files..."
echo "$PY_FILES" | xargs python -m flake8
if [ $? -ne 0 ]; then
echo "Python linting failed"
exit 1
fi
echo "$PY_FILES" | xargs python -m black --check
if [ $? -ne 0 ]; then
echo "Python formatting issues found"
exit 1
fi
fi
# Check Go files
GO_FILES=$(echo "$STAGED_FILES" | grep -E '\.go$')
if [ ! -z "$GO_FILES" ]; then
echo "Checking Go files..."
echo "$GO_FILES" | xargs gofmt -s -w
if [ $? -ne 0 ]; then
echo "Go formatting failed"
exit 1
fi
echo "$GO_FILES" | xargs go vet
if [ $? -ne 0 ]; then
echo "Go vet failed"
exit 1
fi
fi
echo "All checks passed!"
# 2. Performance-focused Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run only on changed files for performance
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$CHANGED_FILES" ]; then
echo "No files to check"
exit 0
fi
# Run ESLint only on changed files
echo "Running ESLint on changed files..."
echo "$CHANGED_FILES" | grep -E '\.(js|jsx|ts|tsx)$' | xargs npx eslint --fix
if [ $? -ne 0 ]; then
echo "ESLint failed"
exit 1
fi
# Run Prettier only on changed files
echo "Running Prettier on changed files..."
echo "$CHANGED_FILES" | xargs npx prettier --write
if [ $? -ne 0 ]; then
echo "Prettier failed"
exit 1
fi
# Run tests only for changed files
echo "Running tests for changed files..."
npx jest --onlyChanged --passWithNoTests
if [ $? -ne 0 ]; then
echo "Tests failed"
exit 1
fi
echo "All checks passed!"
# 3. Security-focused Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check for secrets
echo "Checking for secrets..."
if git diff --cached | grep -i "password\|secret\|key\|token" > /dev/null 2>&1; then
echo "Warning: Potential secrets found in staged files"
echo "Please review and remove any sensitive information"
exit 1
fi
# Check for hardcoded URLs
echo "Checking for hardcoded URLs..."
if git diff --cached | grep -E "https?://[^/]*localhost\|127\.0\.0\.1" > /dev/null 2>&1; then
echo "Warning: Localhost URLs found in staged files"
echo "Consider using environment variables for URLs"
fi
# Run security audit
echo "Running security audit..."
npm audit --audit-level moderate
if [ $? -ne 0 ]; then
echo "Security vulnerabilities found"
exit 1
fi
# Run ESLint security rules
echo "Running ESLint security checks..."
npx eslint . --ext .js,.jsx,.ts,.tsx --config .eslintrc.security.js
if [ $? -ne 0 ]; then
echo "ESLint security checks failed"
exit 1
fi
echo "Security checks passed!"
# 4. Documentation Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check for updated documentation
echo "Checking documentation..."
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
# Check if any .js/.ts files were changed
JS_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(js|jsx|ts|tsx)$')
if [ ! -z "$JS_FILES" ]; then
# Check if README.md was updated
if ! echo "$CHANGED_FILES" | grep -q "README.md"; then
echo "Warning: JavaScript/TypeScript files changed but README.md not updated"
echo "Consider updating documentation for the changes"
fi
# Check if CHANGELOG.md was updated
if ! echo "$CHANGED_FILES" | grep -q "CHANGELOG.md"; then
echo "Warning: JavaScript/TypeScript files changed but CHANGELOG.md not updated"
echo "Consider updating the changelog for the changes"
fi
fi
# Check for JSDoc comments
echo "Checking for JSDoc comments..."
MISSING_JSDOC=$(echo "$JS_FILES" | xargs grep -L "@param\|@returns\|@description")
if [ ! -z "$MISSING_JSDOC" ]; then
echo "Warning: Some files are missing JSDoc comments"
echo "Files without JSDoc: $MISSING_JSDOC"
fi
echo "Documentation checks completed!"
# 5. Database Migration Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check for database migration files
echo "Checking database migrations..."
MIGRATION_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E 'migration|migrate')
if [ ! -z "$MIGRATION_FILES" ]; then
echo "Database migration files detected:"
echo "$MIGRATION_FILES"
# Check if migration tests exist
for file in $MIGRATION_FILES; do
TEST_FILE=$(echo "$file" | sed 's/\.sql$/.test.js/' | sed 's/\.js$/.test.js/')
if [ ! -f "$TEST_FILE" ]; then
echo "Warning: No test file found for migration: $file"
echo "Consider creating a test file: $TEST_FILE"
fi
done
# Check if migration is reversible
for file in $MIGRATION_FILES; do
if ! grep -q "down\|rollback" "$file"; then
echo "Warning: Migration file $file may not be reversible"
echo "Consider adding rollback functionality"
fi
done
fi
echo "Migration checks completed!"
# 6. API Documentation Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check for API changes
echo "Checking API documentation..."
API_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E 'routes|controllers|api')
if [ ! -z "$API_FILES" ]; then
echo "API files changed:"
echo "$API_FILES"
# Check if OpenAPI/Swagger documentation was updated
if ! git diff --cached --name-only --diff-filter=ACM | grep -q "swagger\|openapi\|api\.json"; then
echo "Warning: API files changed but documentation not updated"
echo "Consider updating OpenAPI/Swagger documentation"
fi
# Check for API versioning
for file in $API_FILES; do
if ! grep -q "version\|v[0-9]" "$file"; then
echo "Warning: API file $file may not have versioning"
echo "Consider adding API versioning"
fi
done
fi
echo "API documentation checks completed!"
# 7. Environment Configuration Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check for environment file changes
echo "Checking environment configuration..."
ENV_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.env|config')
if [ ! -z "$ENV_FILES" ]; then
echo "Environment files changed:"
echo "$ENV_FILES"
# Check for sensitive information
for file in $ENV_FILES; do
if grep -q "password\|secret\|key\|token" "$file"; then
echo "Error: Sensitive information found in $file"
echo "Please use environment variables or secure configuration"
exit 1
fi
done
# Check for required environment variables
if [ -f ".env.example" ]; then
echo "Checking against .env.example..."
for file in $ENV_FILES; do
if [ "$file" != ".env.example" ]; then
# Compare with .env.example
if ! diff -q "$file" ".env.example" > /dev/null 2>&1; then
echo "Warning: $file differs from .env.example"
echo "Consider updating .env.example to match"
fi
fi
done
fi
fi
echo "Environment configuration checks completed!"
# 8. Test Coverage Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run tests with coverage
echo "Running tests with coverage..."
npm test -- --coverage --watchAll=false
if [ $? -ne 0 ]; then
echo "Tests failed"
exit 1
fi
# Check coverage threshold
COVERAGE_THRESHOLD=80
COVERAGE=$(npm test -- --coverage --watchAll=false --coverageReporters=text-summary | grep "All files" | awk '{print $4}' | sed 's/%//')
if [ ! -z "$COVERAGE" ]; then
if [ "$COVERAGE" -lt "$COVERAGE_THRESHOLD" ]; then
echo "Error: Test coverage ($COVERAGE%) is below threshold ($COVERAGE_THRESHOLD%)"
exit 1
else
echo "Test coverage ($COVERAGE%) meets threshold ($COVERAGE_THRESHOLD%)"
fi
fi
echo "Test coverage checks passed!"
# 9. Dependency Check Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check for dependency changes
echo "Checking dependencies..."
if git diff --cached --name-only --diff-filter=ACM | grep -q "package\.json\|package-lock\.json\|yarn\.lock"; then
echo "Dependency files changed"
# Check for security vulnerabilities
echo "Running security audit..."
npm audit --audit-level moderate
if [ $? -ne 0 ]; then
echo "Security vulnerabilities found in dependencies"
exit 1
fi
# Check for outdated dependencies
echo "Checking for outdated dependencies..."
OUTDATED=$(npm outdated --json)
if [ ! -z "$OUTDATED" ] && [ "$OUTDATED" != "{}" ]; then
echo "Warning: Outdated dependencies found"
echo "Consider updating dependencies"
fi
# Check for unused dependencies
echo "Checking for unused dependencies..."
npx depcheck
if [ $? -ne 0 ]; then
echo "Unused dependencies found"
echo "Consider removing unused dependencies"
fi
fi
echo "Dependency checks completed!"
# 10. Build Verification Hook
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Verify build
echo "Verifying build..."
npm run build
if [ $? -ne 0 ]; then
echo "Build failed"
exit 1
fi
# Check build output
echo "Checking build output..."
if [ ! -d "dist" ] && [ ! -d "build" ]; then
echo "Error: Build output directory not found"
exit 1
fi
# Check for build artifacts
echo "Checking for build artifacts..."
BUILD_FILES=$(find dist build -name "*.js" -o -name "*.css" -o -name "*.html" 2>/dev/null)
if [ -z "$BUILD_FILES" ]; then
echo "Error: No build artifacts found"
exit 1
fi
# Check build size
echo "Checking build size..."
BUILD_SIZE=$(du -sh dist build 2>/dev/null | awk '{print $1}')
echo "Build size: $BUILD_SIZE"
# Check for source maps
echo "Checking for source maps..."
SOURCE_MAPS=$(find dist build -name "*.map" 2>/dev/null)
if [ -z "$SOURCE_MAPS" ]; then
echo "Warning: No source maps found"
echo "Consider generating source maps for debugging"
fi
echo "Build verification completed!"
Pre-commit Hook Best Practices
Implementation Strategies
Hook Types
- Pre-commit hooks
- Commit-msg hooks
- Pre-push hooks
- Post-commit hooks
- Pre-rebase hooks
- Post-merge hooks
- Custom hooks
Implementation Areas
- Code quality checks
- Formatting validation
- Test execution
- Security scanning
- Documentation checks
- Dependency validation
- Build verification
Summary
Pre-commit hooks implementation involves several key components:
- Setup: Git hooks, Husky, and lint-staged configuration
- Checks: ESLint, Prettier, tests, and security validation
- Automation: Automated formatting, testing, and quality assurance
- Integration: IDE integration and CI/CD pipeline compatibility
Need More Help?
Struggling with pre-commit hooks implementation or need help setting up automated code quality checks? Our Git workflow experts can help you implement effective pre-commit strategies.
Get Pre-commit Help