Mastering Git Hooks: Automating Code Quality and Workflow Enforcement
Introduction
Git hooks are powerful automation tools that execute custom scripts at specific points in your Git workflow. As developers, we often face challenges maintaining code quality, enforcing coding standards, and ensuring tests pass before commits reach the repository. Git hooks provide an elegant solution to automate these processes, catching issues early and maintaining project consistency.
In this comprehensive guide, we'll explore different types of Git hooks, implement practical examples, and learn how to create a robust automated workflow that enhances your development process.
Understanding Git Hooks
Git hooks are scripts that Git executes automatically when certain events occur. They're stored in the .git/hooks/ directory of your repository and can be written in any scripting language your system supports - shell scripts, Python, Node.js, or even compiled binaries.
Types of Git Hooks
Git hooks fall into two main categories:
- Client-side hooks: Triggered by operations like committing and merging
- Server-side hooks: Run on network operations like receiving pushed commits
The most commonly used client-side hooks include:
pre-commit: Runs before commit message editorprepare-commit-msg: Runs before commit message editor but after default message creationcommit-msg: Validates commit messagespre-push: Runs before push operation
Setting Up Your First Git Hook
Let's start with a simple pre-commit hook that ensures code formatting and runs basic checks:
#!/bin/bash
# .git/hooks/pre-commit
echo "Running pre-commit checks..."
# Check if we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "Error: Not in a git repository"
exit 1
fi
# Get list of staged files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E "\.(js|jsx|ts|tsx)$")
if [[ "$STAGED_FILES" = "" ]]; then
echo "No JavaScript/TypeScript files to check"
exit 0
fi
# Run ESLint on staged files
echo "Running ESLint..."
for FILE in $STAGED_FILES; do
npx eslint "$FILE"
if [[ $? -ne 0 ]]; then
echo "ESLint failed for $FILE"
exit 1
fi
done
# Run Prettier check
echo "Checking code formatting..."
for FILE in $STAGED_FILES; do
npx prettier --check "$FILE"
if [[ $? -ne 0 ]]; then
echo "Code formatting issues found in $FILE"
echo "Run: npx prettier --write $FILE"
exit 1
fi
done
echo "All pre-commit checks passed!"
exit 0Make the hook executable:
chmod +x .git/hooks/pre-commitAdvanced Hook: Automated Testing and Code Coverage
Let's create a more sophisticated pre-push hook that runs tests and checks code coverage:
#!/bin/bash
# .git/hooks/pre-push
protected_branch='main'
current_branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')
echo "Pre-push checks starting..."
# Run unit tests
echo "Running unit tests..."
npm test -- --coverage --watchAll=false
if [[ $? -ne 0 ]]; then
echo "Tests failed. Push aborted."
exit 1
fi
# Check code coverage threshold
echo "Checking code coverage..."
COVERAGE_THRESHOLD=80
COVERAGE=$(npm test -- --coverage --watchAll=false --silent | grep "All files" | awk '{print $10}' | sed 's/%//')
if (( $(echo "$COVERAGE < $COVERAGE_THRESHOLD" | bc -l) )); then
echo "Code coverage ($COVERAGE%) is below threshold ($COVERAGE_THRESHOLD%)"
exit 1
fi
# Additional checks for protected branches
if [[ $current_branch = $protected_branch ]]; then
echo "Pushing to protected branch '$protected_branch'..."
# Run integration tests
echo "Running integration tests..."
npm run test:integration
if [[ $? -ne 0 ]]; then
echo "Integration tests failed. Push to $protected_branch aborted."
exit 1
fi
# Check for TODO comments
echo "Checking for TODO comments..."
TODO_COUNT=$(git diff origin/$protected_branch --name-only | xargs grep -l "TODO" | wc -l)
if [[ $TODO_COUNT -gt 0 ]]; then
echo "Warning: Found TODO comments in files being pushed to $protected_branch"
echo "Consider resolving these before pushing to production branch."
fi
fi
echo "All pre-push checks passed!"
exit 0Commit Message Validation Hook
Enforce consistent commit message formatting with a commit-msg hook:
#!/bin/bash
# .git/hooks/commit-msg
# Conventional Commits format: type(scope): description
# Example: feat(auth): add JWT token validation
commit_regex='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}'
error_msg="Aborting commit. Your commit message is invalid.
Valid format: type(scope): description
Types: feat, fix, docs, style, refactor, test, chore
Example: feat(auth): add user authentication"
if ! grep -qE "$commit_regex" "$1"; then
echo "$error_msg" >&2
exit 1
fi
# Check commit message length
if [[ $(head -n1 "$1" | wc -c) -gt 72 ]]; then
echo "Commit message too long. Keep it under 72 characters." >&2
exit 1
fi
echo "Commit message format is valid."
exit 0Sharing Git Hooks with Your Team
Since hooks in .git/hooks/ aren't version controlled, use this approach to share hooks:
- Create a
scripts/hooks/directory in your repository - Store your hooks there
- Create a setup script:
#!/bin/bash
# scripts/setup-hooks.sh
HOOKS_DIR=".git/hooks"
SCRIPTS_HOOKS_DIR="scripts/hooks"
echo "Setting up Git hooks..."
# Copy hooks and make them executable
for hook in $SCRIPTS_HOOKS_DIR/*; do
hook_name=$(basename "$hook")
cp "$hook" "$HOOKS_DIR/$hook_name"
chmod +x "$HOOKS_DIR/$hook_name"
echo "Installed $hook_name hook"
done
echo "Git hooks setup complete!"Package.json Integration
Add hook setup to your package.json:
{
"scripts": {
"postinstall": "./scripts/setup-hooks.sh",
"setup-hooks": "./scripts/setup-hooks.sh"
}
}Best Practices and Performance Tips
Keep Hooks Fast
- Only check staged files, not the entire codebase
- Use parallel processing when possible
- Cache results of expensive operations
- Provide clear progress indicators for long-running hooks
Error Handling
- Always provide clear error messages
- Include suggestions for fixing issues
- Use appropriate exit codes (0 for success, non-zero for failure)
- Log hook execution for debugging
Flexibility
Allow developers to bypass hooks when necessary:
# Skip pre-commit hooks
git commit --no-verify -m "Emergency hotfix"
# Skip pre-push hooks
git push --no-verifyConclusion
Git hooks are invaluable tools for maintaining code quality and enforcing development workflows. By automating routine checks, you ensure consistency across your team and catch issues before they reach production. Start with simple hooks and gradually build more sophisticated automation as your project grows.
Remember to keep hooks fast, provide clear feedback, and make them easy for your team to set up and maintain. With proper implementation, Git hooks become an invisible guardian of your codebase quality.
Related Posts
Mastering Git Workflows: From Chaos to Collaboration Excellence
Transform your team's development process with proven Git workflow strategies that eliminate conflicts and boost productivity.
Mastering Git Workflows: A Complete Guide to Branching Strategies for Teams
Learn proven Git branching strategies that will streamline your team's development process and eliminate merge conflicts.
Mastering Git Workflows: From Chaos to Clean Collaboration
Transform your team's development process with proven Git workflows that eliminate merge conflicts and streamline collaboration.