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