Advanced

Git Hooks

Master Git hooks for automation, quality control, and streamlined development workflows

Git Hooks: Automating Your Development Workflow

Git hooks are powerful scripts that automatically trigger at specific points in your Git workflow. They enable you to automate quality checks, enforce coding standards, and streamline development processes without manual intervention.

Understanding Git Hooks

What are Git Hooks?

Git hooks are custom scripts that Git executes automatically when certain events occur. They act as "triggers" that run before or after Git commands, allowing you to:

  • Validate code before commits
  • Enforce commit message standards
  • Run automated tests
  • Deploy code automatically
  • Notify team members of changes
  • Integrate with external tools

Types of Hooks

Git hooks fall into two categories:

Client-Side HooksServer-Side Hooks
Run on developer machinesRun on Git server/repository host
Can be bypassed with --no-verifyCannot be bypassed
Personal workflow automationTeam/project enforcement
Examples: pre-commit, post-commitExamples: pre-receive, post-receive

Hook Lifecycle and Events

Client-Side Hook Events

# Commit Workflow
pre-commit          # Before commit message editor
prepare-commit-msg  # Before commit message editor opens
commit-msg          # After commit message is entered
post-commit         # After commit is completed

# Merge Workflow  
pre-merge-commit    # Before merge commit
post-merge          # After successful merge

# Email Workflow
applypatch-msg      # Before applying patch
pre-applypatch      # After patch applied, before commit
post-applypatch     # After patch applied and committed

# Other Operations
pre-rebase          # Before rebase operation
post-rewrite        # After commands that rewrite commits
pre-push            # Before push to remote
post-checkout       # After checkout or clone
post-update         # After refs are updated

Server-Side Hook Events

# Receive Operations
pre-receive         # Before any ref updates
update              # Once per ref being updated
post-receive        # After all refs updated
post-update         # After all refs updated (similar to post-receive)

# Push Operations
push-to-checkout    # When pushing to checked-out branch

Setting Up Git Hooks

Hook Location and Structure

# Hooks are stored in the .git/hooks directory
cd your-repository/.git/hooks/

# List available hook templates
ls -la
# pre-commit.sample
# pre-push.sample
# commit-msg.sample
# etc.

# Enable a hook by removing .sample extension
mv pre-commit.sample pre-commit

# Make hook executable (Unix/Linux/macOS)
chmod +x pre-commit

Basic Hook Structure

#!/bin/sh
# Git hook script

# Exit codes:
# 0 = success (allow operation to continue)
# non-zero = failure (abort operation)

# Your automation logic here
echo "Running pre-commit hook..."

# Example: Run linting
if ! npm run lint; then
    echo "Linting failed. Commit aborted."
    exit 1
fi

echo "Pre-commit checks passed!"
exit 0

Common Client-Side Hooks

Pre-Commit Hook

Purpose: Validate code before committing

#!/bin/sh
# File: .git/hooks/pre-commit

echo "๐Ÿ” Running pre-commit checks..."

# Check for debugging statements
if git diff --cached --name-only | xargs grep -l "console.log\|debugger\|TODO:" 2>/dev/null; then
    echo "โŒ Found debugging statements or TODOs in staged files"
    echo "Please remove them before committing"
    exit 1
fi

# Run ESLint on JavaScript files
js_files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.jsx\?$')
if [ ! -z "$js_files" ]; then
    echo "๐Ÿ”ง Linting JavaScript files..."
    if ! npx eslint $js_files; then
        echo "โŒ ESLint failed"
        exit 1
    fi
fi

# Run Prettier formatting
echo "โœจ Formatting code with Prettier..."
npx prettier --write $js_files
git add $js_files

# Run tests
echo "๐Ÿงช Running tests..."
if ! npm test; then
    echo "โŒ Tests failed"
    exit 1
fi

echo "โœ… All pre-commit checks passed!"
exit 0

Commit-Msg Hook

Purpose: Enforce commit message conventions

#!/bin/sh
# File: .git/hooks/commit-msg

commit_regex='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}'
error_msg="โŒ Invalid commit message format!

Commit message should follow Conventional Commits:
  feat: add new feature
  fix: resolve bug
  docs: update documentation
  style: code formatting
  refactor: code restructuring
  test: add/update tests
  chore: maintenance tasks

Examples:
  feat: add user authentication
  fix(auth): resolve login timeout
  docs: update API documentation"

if ! grep -qE "$commit_regex" "$1"; then
    echo "$error_msg"
    exit 1
fi

echo "โœ… Commit message format valid"
exit 0

Pre-Push Hook

Purpose: Validate before pushing to remote

#!/bin/sh
# File: .git/hooks/pre-push

echo "๐Ÿš€ Running pre-push checks..."

# Get current branch
current_branch=$(git symbolic-ref --short HEAD)

# Prevent direct push to main/master
protected_branches="main master develop"
for branch in $protected_branches; do
    if [ "$current_branch" = "$branch" ]; then
        echo "โŒ Direct push to $branch is not allowed!"
        echo "Please create a feature branch and open a pull request"
        exit 1
    fi
done

# Run full test suite
echo "๐Ÿงช Running full test suite..."
if ! npm run test:full; then
    echo "โŒ Full test suite failed"
    exit 1
fi

# Check for merge conflicts markers
if git diff --check; then
    echo "โŒ Found potential merge conflict markers"
    exit 1
fi

echo "โœ… Pre-push checks passed!"
exit 0

Post-Commit Hook

Purpose: Actions after successful commit

#!/bin/sh
# File: .git/hooks/post-commit

echo "๐Ÿ“ Post-commit actions..."

# Get commit information
commit_hash=$(git rev-parse HEAD)
commit_message=$(git log -1 --pretty=%B)
author=$(git log -1 --pretty=%an)

# Log commit to file
echo "$(date): $commit_hash - $commit_message ($author)" >> .git/commit-log.txt

# Trigger build/deployment (example)
# if [ "$current_branch" = "develop" ]; then
#     echo "๐Ÿ—๏ธ  Triggering development build..."
#     curl -X POST "https://your-ci-server.com/trigger-build"
# fi

# Update documentation
if git diff HEAD~1 --name-only | grep -q "README.md\|docs/"; then
    echo "๐Ÿ“š Documentation updated, regenerating docs..."
    # npm run docs:generate
fi

echo "โœ… Post-commit actions completed"

Advanced Hook Scenarios

Language-Specific Quality Gates

Python Project Hook:

#!/bin/sh
# File: .git/hooks/pre-commit

echo "๐Ÿ Python pre-commit checks..."

# Get staged Python files
python_files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')

if [ ! -z "$python_files" ]; then
    # Code formatting with Black
    echo "โšซ Running Black formatter..."
    if ! black --check $python_files; then
        echo "โŒ Code formatting issues found"
        echo "Run: black $python_files"
        exit 1
    fi
    
    # Import sorting with isort
    echo "๐Ÿ“ฆ Checking import order..."
    if ! isort --check-only $python_files; then
        echo "โŒ Import order issues found"
        echo "Run: isort $python_files"
        exit 1
    fi
    
    # Linting with flake8
    echo "๐Ÿ” Running flake8 linter..."
    if ! flake8 $python_files; then
        echo "โŒ Linting errors found"
        exit 1
    fi
    
    # Type checking with mypy
    echo "๐Ÿ”ฌ Running type checks..."
    if ! mypy $python_files; then
        echo "โŒ Type checking errors found"
        exit 1
    fi
fi

# Run tests
echo "๐Ÿงช Running pytest..."
if ! python -m pytest --tb=short; then
    echo "โŒ Tests failed"
    exit 1
fi

echo "โœ… All Python checks passed!"

Go Project Hook:

#!/bin/sh
# File: .git/hooks/pre-commit

echo "๐Ÿน Go pre-commit checks..."

# Format code
echo "๐ŸŽจ Running gofmt..."
unformatted=$(gofmt -l .)
if [ ! -z "$unformatted" ]; then
    echo "โŒ The following files need formatting:"
    echo "$unformatted"
    echo "Run: gofmt -w ."
    exit 1
fi

# Run vet
echo "๐Ÿ” Running go vet..."
if ! go vet ./...; then
    echo "โŒ go vet failed"
    exit 1
fi

# Run linter
if command -v golint >/dev/null 2>&1; then
    echo "๐Ÿ“ Running golint..."
    if ! golint ./...; then
        echo "โŒ golint failed"
        exit 1
    fi
fi

# Run tests
echo "๐Ÿงช Running tests..."
if ! go test ./...; then
    echo "โŒ Tests failed"
    exit 1
fi

echo "โœ… All Go checks passed!"

Security-Focused Hooks

#!/bin/sh
# File: .git/hooks/pre-commit

echo "๐Ÿ”’ Security pre-commit checks..."

# Check for secrets
echo "๐Ÿ” Scanning for secrets..."
if command -v git-secrets >/dev/null 2>&1; then
    if ! git secrets --scan; then
        echo "โŒ Potential secrets detected!"
        exit 1
    fi
else
    # Manual secret patterns
    secrets_pattern="(password|api_key|secret|token|private_key)\s*[:=]\s*['\"][^'\"]*['\"]"
    if git diff --cached | grep -iE "$secrets_pattern"; then
        echo "โŒ Potential secrets found in staged changes"
        exit 1
    fi
fi

# Check file permissions
echo "๐Ÿ”‘ Checking file permissions..."
if git diff --cached --name-only | xargs ls -la | grep '^-rwxrwxrwx'; then
    echo "โŒ Files with overly permissive permissions detected"
    exit 1
fi

# Check for large files
echo "๐Ÿ“ Checking file sizes..."
large_files=$(git diff --cached --name-only | xargs ls -la | awk '$5 > 5242880 {print $9 " (" $5 " bytes)"}')
if [ ! -z "$large_files" ]; then
    echo "โŒ Large files detected (>5MB):"
    echo "$large_files"
    echo "Consider using Git LFS for large files"
    exit 1
fi

echo "โœ… Security checks passed!"

Server-Side Hooks

Pre-Receive Hook

Purpose: Enforce server-side policies

#!/bin/sh
# File: hooks/pre-receive

echo "๐Ÿ“ฅ Server pre-receive checks..."

while read oldrev newrev refname; do
    # Get branch name
    branch=$(git rev-parse --symbolic --abbrev-ref $refname)
    
    # Prevent deletion of protected branches
    if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
        if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then
            echo "โŒ Deletion of protected branch $branch is not allowed"
            exit 1
        fi
    fi
    
    # Prevent force push to protected branches
    if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then
        if ! git merge-base --is-ancestor $oldrev $newrev; then
            echo "โŒ Force push to $branch is not allowed"
            exit 1
        fi
    fi
    
    # Check commit messages
    for commit in $(git rev-list $oldrev..$newrev); do
        message=$(git log --format=%B -n 1 $commit)
        if ! echo "$message" | grep -qE "^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: "; then
            echo "โŒ Invalid commit message format in $commit"
            exit 1
        fi
    done
done

echo "โœ… Server pre-receive checks passed!"

Post-Receive Hook

Purpose: Trigger deployments and notifications

#!/bin/sh
# File: hooks/post-receive

echo "๐Ÿ“ฌ Post-receive processing..."

while read oldrev newrev refname; do
    branch=$(git rev-parse --symbolic --abbrev-ref $refname)
    
    # Auto-deploy on main branch push
    if [ "$branch" = "main" ]; then
        echo "๐Ÿš€ Deploying to production..."
        
        # Example deployment
        # /path/to/deploy-script.sh
        
        # Notify team
        commit_count=$(git rev-list --count $oldrev..$newrev)
        author=$(git log -1 --pretty=%an $newrev)
        
        # Send notification (example with curl)
        curl -X POST "https://hooks.slack.com/your-webhook-url" \
            -H "Content-Type: application/json" \
            -d "{\"text\":\"๐Ÿš€ $author deployed $commit_count commits to production\"}"
    fi
    
    # Auto-deploy staging on develop branch
    if [ "$branch" = "develop" ]; then
        echo "๐Ÿงช Deploying to staging..."
        # /path/to/deploy-staging.sh
    fi
done

echo "โœ… Post-receive processing completed!"

Hook Management and Best Practices

Sharing Hooks with Team

Problem: Git hooks are local and not tracked in version control

Solution: Create a hooks management system

# Create hooks directory in your repository
mkdir .githooks

# Move hooks to version-controlled directory
mv .git/hooks/pre-commit .githooks/pre-commit
mv .git/hooks/commit-msg .githooks/commit-msg

# Configure Git to use the new hooks directory
git config core.hooksPath .githooks

# Make hooks executable
chmod +x .githooks/*

# Add to version control
git add .githooks/
git commit -m "Add shared git hooks"

Setup script for new team members:

#!/bin/sh
# File: scripts/setup-hooks.sh

echo "๐Ÿ”— Setting up Git hooks..."

# Configure hooks path
git config core.hooksPath .githooks

# Make hooks executable
chmod +x .githooks/*

# Install hook dependencies
if [ -f "package.json" ]; then
    echo "๐Ÿ“ฆ Installing npm dependencies..."
    npm install
fi

echo "โœ… Git hooks setup complete!"

Hook Templates and Reusability

Create hook templates:

# File: .githooks/templates/pre-commit-template.sh

#!/bin/sh
# Pre-commit hook template

# Source project-specific configuration
if [ -f ".githooks/config.sh" ]; then
    . .githooks/config.sh
fi

# Common functions
run_linter() {
    echo "๐Ÿ” Running linter..."
    if [ "$LINTER" = "eslint" ]; then
        npx eslint $1
    elif [ "$LINTER" = "flake8" ]; then
        flake8 $1
    fi
}

run_formatter() {
    echo "โœจ Running formatter..."
    if [ "$FORMATTER" = "prettier" ]; then
        npx prettier --write $1
    elif [ "$FORMATTER" = "black" ]; then
        black $1
    fi
}

# Main execution
staged_files=$(git diff --cached --name-only --diff-filter=ACM | grep "\.$FILE_EXTENSION$")

if [ ! -z "$staged_files" ]; then
    run_linter "$staged_files"
    run_formatter "$staged_files"
fi

Project configuration:

# File: .githooks/config.sh

# Project-specific hook configuration
LINTER="eslint"
FORMATTER="prettier"
FILE_EXTENSION="jsx?"
SKIP_TESTS="false"
PROTECTED_BRANCHES="main master"

Bypassing Hooks

# Skip pre-commit and commit-msg hooks
git commit --no-verify -m "Emergency hotfix"

# Skip pre-push hooks  
git push --no-verify

# Skip all hooks for a single command
git -c core.hooksPath= commit -m "No hooks execution"

Testing Hooks

#!/bin/sh
# File: test/test-hooks.sh

echo "๐Ÿงช Testing Git hooks..."

# Test pre-commit hook
echo "Testing pre-commit hook..."
.githooks/pre-commit
pre_commit_exit=$?

if [ $pre_commit_exit -eq 0 ]; then
    echo "โœ… Pre-commit hook test passed"
else
    echo "โŒ Pre-commit hook test failed"
fi

# Test commit-msg hook
echo "Testing commit-msg hook..."
echo "feat: test commit message" > /tmp/test-commit-msg
.githooks/commit-msg /tmp/test-commit-msg
commit_msg_exit=$?

if [ $commit_msg_exit -eq 0 ]; then
    echo "โœ… Commit-msg hook test passed"
else
    echo "โŒ Commit-msg hook test failed"
fi

rm -f /tmp/test-commit-msg

Integration with External Tools

CI/CD Integration

GitHub Actions Integration:

# File: .github/workflows/validate-hooks.yml
name: Validate Git Hooks

on: [push, pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        run: npm install
      - name: Test hooks
        run: |
          chmod +x .githooks/*
          ./test/test-hooks.sh

Husky Integration

Modern JavaScript/Node.js projects:

# Install Husky
npm install --save-dev husky

# Enable Git hooks
npx husky install

# Add hooks
npx husky add .husky/pre-commit "npm run lint && npm test"
npx husky add .husky/commit-msg "npx commitlint --edit $1"

Package.json configuration:

{
  "scripts": {
    "prepare": "husky install",
    "lint": "eslint src/",
    "test": "jest"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ]
  }
}

Troubleshooting Common Issues

Hook Not Executing

Symptoms: Hook script exists but doesn't run

Solutions:

# Check if hook is executable
ls -la .git/hooks/pre-commit
# Should show -rwxr-xr-x permissions

# Make hook executable
chmod +x .git/hooks/pre-commit

# Check hooks path configuration
git config core.hooksPath
# Should show your custom hooks directory if configured

# Verify hook has correct shebang
head -1 .git/hooks/pre-commit
# Should be #!/bin/sh or #!/usr/bin/env bash

Hook Failing Unexpectedly

Debug hooks:

#!/bin/sh
# Add to top of hook for debugging

# Enable debug mode
set -x  # Print commands as they execute
set -e  # Exit on first error

# Log execution
exec > /tmp/git-hook.log 2>&1

echo "Hook started at $(date)"
echo "PWD: $(pwd)"
echo "PATH: $PATH"

# Your hook logic here

Performance Issues

Optimize slow hooks:

#!/bin/sh

# Cache results when possible
cache_file=".git/hooks-cache/lint-results"
if [ -f "$cache_file" ]; then
    cached_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
    current_time=$(date +%s)
    if [ $((current_time - cached_time)) -lt 300 ]; then
        echo "Using cached results (less than 5 minutes old)"
        exit 0
    fi
fi

# Run expensive operations only on changed files
changed_files=$(git diff --cached --name-only)
if [ -z "$changed_files" ]; then
    echo "No changed files, skipping checks"
    exit 0
fi

# Parallel execution
echo "$changed_files" | xargs -P 4 -I {} sh -c 'lint_file "$1"' _ {}

Hook Recipes and Examples

Automatic Code Documentation

#!/bin/sh
# File: .githooks/post-commit

# Auto-generate documentation when code changes
if git diff HEAD~1 --name-only | grep -q "src/"; then
    echo "๐Ÿ“ Generating documentation..."
    
    # For JavaScript projects
    if [ -f "jsdoc.json" ]; then
        npm run docs:generate
        git add docs/
        git commit --amend --no-edit --no-verify
    fi
    
    # For Python projects
    if [ -f "setup.py" ]; then
        sphinx-build -b html docs/ docs/_build/
    fi
fi

Automatic Version Bumping

#!/bin/sh
# File: .githooks/post-merge

# Auto-bump version after merging release branch
current_branch=$(git symbolic-ref --short HEAD)
merged_branch=$(git log -1 --merges --pretty=%s | grep -o 'Merge.*' | cut -d' ' -f2)

if [ "$current_branch" = "main" ] && echo "$merged_branch" | grep -q "release/"; then
    echo "๐Ÿท๏ธ  Auto-bumping version..."
    npm version patch --no-git-tag-version
    git add package.json package-lock.json
    git commit -m "chore: bump version after release merge"
fi

Database Migration Checks

#!/bin/sh
# File: .githooks/pre-push

# Check for database migrations before push
migration_files=$(git diff --name-only origin/main...HEAD | grep -E "migrations?/")

if [ ! -z "$migration_files" ]; then
    echo "๐Ÿ—ƒ๏ธ  Database migrations detected:"
    echo "$migration_files"
    
    read -p "Have you tested these migrations? (y/N): " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "โŒ Please test migrations before pushing"
        exit 1
    fi
fi

Best Practices Summary

Do's โœ…

  • Keep hooks fast - Use caching and only check changed files
  • Make hooks transparent - Log what they're doing
  • Test hooks thoroughly - Include hook testing in your CI/CD
  • Version control hooks - Share them with your team
  • Handle errors gracefully - Provide clear error messages
  • Document hook behavior - Explain what each hook does

Don'ts โŒ

  • Don't make hooks too complex - Keep them focused and simple
  • Don't ignore exit codes - Always return proper exit codes
  • Don't hardcode paths - Use relative paths and environment variables
  • Don't make hooks mandatory for everything - Allow bypassing when needed
  • Don't forget about performance - Long-running hooks frustrate developers
  • Don't forget cross-platform compatibility - Consider Windows/Mac/Linux differences

Next Steps

๐ŸŽฃ Congratulations! You now understand Git hooks comprehensively.

Continue your mastery:

  1. Explore Git Internals - Understand how Git works under the hood
  2. Learn Performance Optimization - Optimize Git for large repositories
  3. Master Git Security - Implement comprehensive security practices

Practice Exercises

  • Set up a pre-commit hook with linting and testing
  • Create a commit-msg hook for conventional commits
  • Implement a pre-push hook with branch protection
  • Share hooks with your team using a shared directory
  • Integrate hooks with your CI/CD pipeline
  • Create language-specific hook templates