`n

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