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 Hooks | Server-Side Hooks |
---|---|
Run on developer machines | Run on Git server/repository host |
Can be bypassed with --no-verify | Cannot be bypassed |
Personal workflow automation | Team/project enforcement |
Examples: pre-commit, post-commit | Examples: 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:
- Explore Git Internals - Understand how Git works under the hood
- Learn Performance Optimization - Optimize Git for large repositories
- 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