`n

Git Hook Implementation - Complete Guide

Published: September 25, 2024 | Reading time: 18 minutes

Git Hooks Overview

Git hooks automate tasks during Git operations:

Git Hooks Benefits
# Git Hooks Benefits
- Automated code quality checks
- Consistent workflow enforcement
- Prevents bad commits
- Automated testing
- Code formatting
- Security validation
- Team workflow standardization

Git Hooks Types

Client-Side and Server-Side Hooks

Git Hooks Types
# Git Hooks Types

# 1. Client-Side Hooks
# Pre-commit hook
# .git/hooks/pre-commit
#!/bin/bash
echo "Running pre-commit checks..."

# Run linting
npm run lint
if [ $? -ne 0 ]; then
    echo "Linting failed. Commit aborted."
    exit 1
fi

# Run tests
npm test
if [ $? -ne 0 ]; then
    echo "Tests failed. Commit aborted."
    exit 1
fi

echo "Pre-commit checks passed!"

# 2. Pre-push hook
# .git/hooks/pre-push
#!/bin/bash
echo "Running pre-push checks..."

# Run integration tests
npm run test:integration
if [ $? -ne 0 ]; then
    echo "Integration tests failed. Push aborted."
    exit 1
fi

echo "Pre-push checks passed!"

# 3. Commit-msg hook
# .git/hooks/commit-msg
#!/bin/bash
commit_regex='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+'

if ! grep -qE "$commit_regex" "$1"; then
    echo "Invalid commit message format!"
    echo "Format: type(scope): description"
    echo "Types: feat, fix, docs, style, refactor, test, chore"
    exit 1
fi

# 4. Post-commit hook
# .git/hooks/post-commit
#!/bin/bash
echo "Commit completed successfully!"
echo "Commit hash: $(git rev-parse HEAD)"
echo "Branch: $(git rev-parse --abbrev-ref HEAD)"

# 5. Pre-rebase hook
# .git/hooks/pre-rebase
#!/bin/bash
echo "Pre-rebase checks..."

# Check if rebasing onto main
if [ "$1" = "main" ]; then
    echo "Rebasing onto main branch..."
    # Add any specific checks for main branch
fi

# 6. Post-merge hook
# .git/hooks/post-merge
#!/bin/bash
echo "Merge completed!"

# Install new dependencies if package.json changed
if git diff-tree -r --name-only --no-commit-id HEAD | grep -q package.json; then
    echo "package.json changed, installing dependencies..."
    npm install
fi

# 7. Server-Side Hooks
# Pre-receive hook (server-side)
#!/bin/bash
echo "Pre-receive checks..."

# Check commit message format
while read oldrev newrev refname; do
    if [ "$refname" = "refs/heads/main" ]; then
        commits=$(git rev-list $oldrev..$newrev)
        for commit in $commits; do
            commit_msg=$(git log --format=%B -n 1 $commit)
            if ! echo "$commit_msg" | grep -qE '^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+'; then
                echo "Invalid commit message in $commit"
                exit 1
            fi
        done
    fi
done

# 8. Update hook (server-side)
#!/bin/bash
refname="$1"
oldrev="$2"
newrev="$3"

if [ "$refname" = "refs/heads/main" ]; then
    echo "Updating main branch..."
    # Add specific checks for main branch updates
fi

# 9. Post-receive hook (server-side)
#!/bin/bash
echo "Post-receive processing..."

# Deploy to staging if pushed to develop
if [ "$1" = "refs/heads/develop" ]; then
    echo "Deploying to staging..."
    # Add deployment commands
fi

# Deploy to production if pushed to main
if [ "$1" = "refs/heads/main" ]; then
    echo "Deploying to production..."
    # Add deployment commands
fi

Pre-commit Hooks

Code Quality Automation

Pre-commit Hook Implementation
# Pre-commit Hook Implementation

# 1. Basic Pre-commit Hook
# .git/hooks/pre-commit
#!/bin/bash
set -e

echo "Running pre-commit checks..."

# Check for debug statements
if grep -r "console.log\|debugger\|TODO\|FIXME" --include="*.js" --include="*.ts" src/; then
    echo "Found debug statements or TODOs. Please remove them before committing."
    exit 1
fi

# Check for large files
if find . -name "*.js" -o -name "*.ts" -o -name "*.json" | xargs wc -l | awk '$1 > 500 {print $2 " has " $1 " lines"}' | head -1; then
    echo "Found files with more than 500 lines. Consider splitting them."
    exit 1
fi

# Run linting
echo "Running ESLint..."
npm run lint
if [ $? -ne 0 ]; then
    echo "ESLint failed. Please fix the issues before committing."
    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 "Pre-commit checks passed!"

# 2. Advanced Pre-commit Hook
# .git/hooks/pre-commit
#!/bin/bash
set -e

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${YELLOW}Running pre-commit checks...${NC}"

# Function to check if command exists
command_exists() {
    command -v "$1" >/dev/null 2>&1
}

# Check for required tools
if ! command_exists npm; then
    echo -e "${RED}npm is not installed${NC}"
    exit 1
fi

# Check for staged files
staged_files=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$staged_files" ]; then
    echo -e "${YELLOW}No staged files to check${NC}"
    exit 0
fi

# Check for JavaScript/TypeScript files
js_files=$(echo "$staged_files" | grep -E '\.(js|ts|jsx|tsx)$' || true)
if [ -n "$js_files" ]; then
    echo -e "${YELLOW}Checking JavaScript/TypeScript files...${NC}"
    
    # Run ESLint
    echo "Running ESLint..."
    npm run lint
    if [ $? -ne 0 ]; then
        echo -e "${RED}ESLint failed${NC}"
        exit 1
    fi
    
    # Run Prettier
    echo "Running Prettier..."
    npm run format:check
    if [ $? -ne 0 ]; then
        echo -e "${RED}Prettier formatting issues found${NC}"
        exit 1
    fi
fi

# Check for Python files
py_files=$(echo "$staged_files" | grep -E '\.py$' || true)
if [ -n "$py_files" ]; then
    echo -e "${YELLOW}Checking Python files...${NC}"
    
    # Run flake8
    if command_exists flake8; then
        echo "Running flake8..."
        flake8 $py_files
        if [ $? -ne 0 ]; then
            echo -e "${RED}flake8 failed${NC}"
            exit 1
        fi
    fi
    
    # Run black
    if command_exists black; then
        echo "Running black..."
        black --check $py_files
        if [ $? -ne 0 ]; then
            echo -e "${RED}black formatting issues found${NC}"
            exit 1
        fi
    fi
fi

# Check for secrets
echo "Checking for secrets..."
if grep -r -i "password\|secret\|key\|token" --include="*.js" --include="*.ts" --include="*.json" $staged_files; then
    echo -e "${RED}Potential secrets found in staged files${NC}"
    exit 1
fi

# Run tests
echo "Running tests..."
npm test
if [ $? -ne 0 ]; then
    echo -e "${RED}Tests failed${NC}"
    exit 1
fi

echo -e "${GREEN}Pre-commit checks passed!${NC}"

# 3. Pre-commit Hook with Husky
# Install Husky
npm install --save-dev husky

# Configure Husky
npx husky install
npx husky add .husky/pre-commit "npm run pre-commit"

# package.json scripts
{
  "scripts": {
    "pre-commit": "lint-staged",
    "lint-staged": "lint-staged"
  },
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}

# 4. Pre-commit Hook with pre-commit Framework
# Install pre-commit
pip install pre-commit

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
      - id: check-merge-conflict
      
  - repo: https://github.com/psf/black
    rev: 22.12.0
    hooks:
      - id: black
        language_version: python3
        
  - repo: https://github.com/pycqa/flake8
    rev: 6.0.0
    hooks:
      - id: flake8

# Install hooks
pre-commit install

# 5. Custom Pre-commit Hook
# .git/hooks/pre-commit
#!/bin/bash
set -e

# Configuration
MAX_FILE_SIZE=1048576  # 1MB
MAX_LINES=500
REQUIRED_TESTS_PASS=true

echo "Running custom pre-commit checks..."

# Check file sizes
echo "Checking file sizes..."
for file in $(git diff --cached --name-only); do
    if [ -f "$file" ]; then
        size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
        if [ "$size" -gt "$MAX_FILE_SIZE" ]; then
            echo "Error: $file is too large ($size bytes)"
            exit 1
        fi
    fi
done

# Check line counts
echo "Checking line counts..."
for file in $(git diff --cached --name-only --diff-filter=ACM); do
    if [ -f "$file" ]; then
        lines=$(wc -l < "$file")
        if [ "$lines" -gt "$MAX_LINES" ]; then
            echo "Warning: $file has $lines lines (max: $MAX_LINES)"
        fi
    fi
done

# Check for TODO comments
echo "Checking for TODO comments..."
if git diff --cached | grep -q "TODO\|FIXME"; then
    echo "Warning: Found TODO or FIXME comments"
fi

# Run specific checks based on file type
for file in $(git diff --cached --name-only --diff-filter=ACM); do
    case "$file" in
        *.js|*.ts)
            echo "Checking JavaScript/TypeScript file: $file"
            # Add specific JS/TS checks
            ;;
        *.py)
            echo "Checking Python file: $file"
            # Add specific Python checks
            ;;
        *.json)
            echo "Checking JSON file: $file"
            # Validate JSON
            python -m json.tool "$file" > /dev/null
            ;;
    esac
done

echo "Pre-commit checks completed successfully!"

Pre-push Hooks

Push Validation

Pre-push Hook Implementation
# Pre-push Hook Implementation

# 1. Basic Pre-push Hook
# .git/hooks/pre-push
#!/bin/bash
set -e

echo "Running pre-push checks..."

# Get the remote name and URL
remote="$1"
url="$2"

# Get the local and remote refs
while read local_ref local_sha remote_ref remote_sha; do
    if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
        # Handle delete
        continue
    fi
    
    if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
        # Handle new branch
        echo "Pushing new branch: $local_ref"
    else
        # Handle update
        echo "Updating branch: $local_ref"
    fi
    
    # Run integration tests
    echo "Running integration tests..."
    npm run test:integration
    if [ $? -ne 0 ]; then
        echo "Integration tests failed. Push aborted."
        exit 1
    fi
    
    # Run build
    echo "Running build..."
    npm run build
    if [ $? -ne 0 ]; then
        echo "Build failed. Push aborted."
        exit 1
    fi
    
    # Check for sensitive data
    echo "Checking for sensitive data..."
    if grep -r -i "password\|secret\|key\|token" --include="*.js" --include="*.ts" --include="*.json" src/; then
        echo "Potential sensitive data found. Push aborted."
        exit 1
    fi
    
done

echo "Pre-push checks passed!"

# 2. Advanced Pre-push Hook
# .git/hooks/pre-push
#!/bin/bash
set -e

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${YELLOW}Running pre-push checks...${NC}"

# Configuration
REQUIRED_BRANCHES=("main" "develop")
PROTECTED_BRANCHES=("main")

# Function to check if branch is protected
is_protected_branch() {
    local branch="$1"
    for protected in "${PROTECTED_BRANCHES[@]}"; do
        if [ "$branch" = "$protected" ]; then
            return 0
        fi
    done
    return 1
}

# Function to check if branch is required
is_required_branch() {
    local branch="$1"
    for required in "${REQUIRED_BRANCHES[@]}"; do
        if [ "$branch" = "$required" ]; then
            return 0
        fi
    done
    return 1
}

# Process each ref being pushed
while read local_ref local_sha remote_ref remote_sha; do
    # Skip if local ref is empty (delete)
    if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
        continue
    fi
    
    # Get branch name
    branch=$(echo "$local_ref" | sed 's/refs\/heads\///')
    
    echo -e "${YELLOW}Processing branch: $branch${NC}"
    
    # Check if pushing to protected branch
    if is_protected_branch "$branch"; then
        echo -e "${RED}Error: Cannot push directly to protected branch: $branch${NC}"
        echo "Please create a pull request instead."
        exit 1
    fi
    
    # Check if branch exists locally
    if ! git show-ref --verify --quiet "refs/heads/$branch"; then
        echo -e "${RED}Error: Branch $branch does not exist locally${NC}"
        exit 1
    fi
    
    # Check if branch is up to date with remote
    if [ "$remote_sha" != "0000000000000000000000000000000000000000" ]; then
        # Check if local branch is behind remote
        if ! git merge-base --is-ancestor "$remote_sha" "$local_sha"; then
            echo -e "${RED}Error: Local branch $branch is behind remote${NC}"
            echo "Please pull the latest changes first."
            exit 1
        fi
    fi
    
    # Run tests
    echo "Running tests..."
    npm test
    if [ $? -ne 0 ]; then
        echo -e "${RED}Tests failed${NC}"
        exit 1
    fi
    
    # Run build
    echo "Running build..."
    npm run build
    if [ $? -ne 0 ]; then
        echo -e "${RED}Build failed${NC}"
        exit 1
    fi
    
    # Check for sensitive data
    echo "Checking for sensitive data..."
    if grep -r -i "password\|secret\|key\|token" --include="*.js" --include="*.ts" --include="*.json" src/; then
        echo -e "${RED}Potential sensitive data found${NC}"
        exit 1
    fi
    
    # Check commit message format
    echo "Checking commit messages..."
    commits=$(git rev-list "$remote_sha".."$local_sha" 2>/dev/null || git rev-list "$local_sha")
    for commit in $commits; do
        commit_msg=$(git log --format=%B -n 1 "$commit")
        if ! echo "$commit_msg" | grep -qE '^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+'; then
            echo -e "${RED}Invalid commit message in $commit${NC}"
            echo "Format: type(scope): description"
            exit 1
        fi
    done
    
    echo -e "${GREEN}Pre-push checks passed for $branch${NC}"
done

echo -e "${GREEN}All pre-push checks passed!${NC}"

# 3. Pre-push Hook with Husky
# Install Husky
npm install --save-dev husky

# Configure Husky
npx husky install
npx husky add .husky/pre-push "npm run pre-push"

# package.json scripts
{
  "scripts": {
    "pre-push": "npm run test:integration && npm run build",
    "test:integration": "jest --testPathPattern=integration",
    "build": "webpack --mode=production"
  }
}

# 4. Pre-push Hook for Specific Branches
# .git/hooks/pre-push
#!/bin/bash
set -e

echo "Running pre-push checks..."

# Process each ref being pushed
while read local_ref local_sha remote_ref remote_sha; do
    # Skip if local ref is empty (delete)
    if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
        continue
    fi
    
    # Get branch name
    branch=$(echo "$local_ref" | sed 's/refs\/heads\///')
    
    echo "Processing branch: $branch"
    
    # Different checks for different branches
    case "$branch" in
        main)
            echo "Running production checks..."
            npm run test:all
            npm run build:production
            npm run security:audit
            ;;
        develop)
            echo "Running development checks..."
            npm run test:unit
            npm run build:development
            ;;
        feature/*)
            echo "Running feature branch checks..."
            npm run test:unit
            npm run lint
            ;;
        hotfix/*)
            echo "Running hotfix checks..."
            npm run test:unit
            npm run test:integration
            npm run build:production
            ;;
        *)
            echo "Running default checks..."
            npm run test:unit
            npm run lint
            ;;
    esac
    
    if [ $? -ne 0 ]; then
        echo "Checks failed for branch: $branch"
        exit 1
    fi
    
done

echo "Pre-push checks passed!"

# 5. Pre-push Hook with Notifications
# .git/hooks/pre-push
#!/bin/bash
set -e

echo "Running pre-push checks..."

# Function to send notification
send_notification() {
    local message="$1"
    local status="$2"
    
    # Send to Slack (if webhook URL is set)
    if [ -n "$SLACK_WEBHOOK_URL" ]; then
        curl -X POST -H 'Content-type: application/json' \
            --data "{\"text\":\"$message\", \"color\":\"$status\"}" \
            "$SLACK_WEBHOOK_URL"
    fi
    
    # Send email (if configured)
    if [ -n "$EMAIL_RECIPIENTS" ]; then
        echo "$message" | mail -s "Git Push Notification" "$EMAIL_RECIPIENTS"
    fi
}

# Process each ref being pushed
while read local_ref local_sha remote_ref remote_sha; do
    # Skip if local ref is empty (delete)
    if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
        continue
    fi
    
    # Get branch name
    branch=$(echo "$local_ref" | sed 's/refs\/heads\///')
    
    echo "Processing branch: $branch"
    
    # Run checks
    npm test
    if [ $? -ne 0 ]; then
        send_notification "Push failed for branch $branch: Tests failed" "danger"
        exit 1
    fi
    
    npm run build
    if [ $? -ne 0 ]; then
        send_notification "Push failed for branch $branch: Build failed" "danger"
        exit 1
    fi
    
    # Send success notification
    send_notification "Push successful for branch $branch" "good"
    
done

echo "Pre-push checks passed!"

Hook Management

Hook Organization and Maintenance

Hook Best Practices

  • Keep hooks simple and fast
  • Use proper error handling
  • Provide clear error messages
  • Test hooks thoroughly
  • Document hook behavior
  • Use version control for hooks
  • Consider performance impact

Common Mistakes

  • Making hooks too complex
  • Not handling errors properly
  • Slow hook execution
  • Not testing hooks
  • Hardcoding paths
  • Ignoring hook failures
  • Not documenting hooks

Summary

Git hook implementation involves several key components:

  • Hook Types: Client-side and server-side hooks
  • Pre-commit Hooks: Code quality, testing, formatting
  • Pre-push Hooks: Integration tests, build validation
  • Hook Management: Organization, maintenance, best practices
  • Automation: Husky, pre-commit framework, custom scripts
  • Best Practices: Guidelines, common mistakes, performance considerations

Need More Help?

Struggling with Git hook implementation or need help setting up automated workflows? Our Git experts can help you implement effective hook strategies.

Get Git Hook Help