Giao diện
Git Hooks - Automation That Guards Your Code 🪝
ROLE: HPN (DevOps Automation).
AUDIENCE: Engineers muốn tự động hóa quy trình và enforce standards.
Git Hooks là các scripts tự động chạy trước/sau các Git events. Chúng là gateway guardians - chặn code xấu trước khi nó vào repository, enforce conventions, và tự động hóa các task lặp đi lặp lại.
🎯 Mục tiêu
Sau module này, bạn sẽ:
- Hiểu Git Hooks hoạt động thế nào
- Thiết lập
pre-commitđể chặn code không đạt chuẩn - Enforce Conventional Commits với
commit-msghook - Chạy tests tự động trước khi push với
pre-push - Viết hooks thực tế bằng Bash và Python
📍 Hook Basics
Hooks nằm ở đâu?
.git/hooks/
├── pre-commit.sample
├── commit-msg.sample
├── pre-push.sample
├── post-merge.sample
└── ... (nhiều samples khác)Kích hoạt Hook
bash
# Remove .sample extension và chmod +x
cd .git/hooks
cp pre-commit.sample pre-commit
chmod +x pre-commitHook Lifecycle
🔑 Key Hooks Reference
Client-side Hooks
| Hook | Trigger | Use Case |
|---|---|---|
pre-commit | Trước khi commit | Lint, format, security checks |
prepare-commit-msg | Sau khi message được tạo | Auto-add ticket numbers |
commit-msg | Sau khi user nhập message | Validate message format |
post-commit | Sau khi commit hoàn tất | Notifications, logging |
pre-push | Trước khi push | Run tests, type checks |
post-checkout | Sau checkout/switch | Install dependencies |
post-merge | Sau merge | Rebuild, update deps |
Server-side Hooks
| Hook | Trigger | Use Case |
|---|---|---|
pre-receive | Trước khi chấp nhận push | Policy enforcement |
update | Mỗi branch được push | Per-branch rules |
post-receive | Sau khi push hoàn tất | CI/CD trigger, deploy |
🛡️ pre-commit - The First Line of Defense
Purpose
Chạy trước mỗi commit. Nếu script exit với code khác 0, commit bị chặn.
Use Cases
- Linting (ESLint, Pylint, Flake8)
- Formatting (Prettier, Black)
- Security scanning (detect secrets, private keys)
- Code quality (no console.log, no TODO in production)
Example 1: Block TODO Comments
bash
#!/bin/bash
# .git/hooks/pre-commit
# Block commits containing TODO or FIXME
echo "🔍 Checking for TODO/FIXME comments..."
# Get staged files (only the ones being committed)
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|py|go|java)$')
if [ -z "$STAGED_FILES" ]; then
exit 0
fi
# Search for TODO/FIXME in staged files
FOUND_ISSUES=0
for FILE in $STAGED_FILES; do
# Check staged content, not working directory
if git show ":$FILE" | grep -inE "(TODO|FIXME)" > /dev/null 2>&1; then
echo "❌ Found TODO/FIXME in: $FILE"
git show ":$FILE" | grep -inE "(TODO|FIXME)" | head -5
FOUND_ISSUES=1
fi
done
if [ $FOUND_ISSUES -eq 1 ]; then
echo ""
echo "⛔ Commit blocked! Remove TODO/FIXME comments first."
echo "💡 Hoặc dùng: git commit --no-verify để bypass (không khuyến khích)"
exit 1
fi
echo "✅ No TODO/FIXME found. Proceeding..."
exit 0Example 2: Detect Private Keys & Secrets
bash
#!/bin/bash
# .git/hooks/pre-commit
# Security: Block commits containing secrets
echo "🔐 Scanning for secrets and private keys..."
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$STAGED_FILES" ]; then
exit 0
fi
# Patterns to detect
PATTERNS=(
# AWS
"AKIA[0-9A-Z]{16}"
# Private keys
"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----"
# Generic secrets
"(password|secret|api_key|apikey|access_token)\s*[:=]\s*['\"][^'\"]{8,}['\"]"
# GitHub tokens
"ghp_[a-zA-Z0-9]{36}"
"gho_[a-zA-Z0-9]{36}"
)
FOUND_SECRETS=0
for FILE in $STAGED_FILES; do
for PATTERN in "${PATTERNS[@]}"; do
if git show ":$FILE" 2>/dev/null | grep -iE "$PATTERN" > /dev/null 2>&1; then
echo "🚨 SECURITY WARNING in: $FILE"
echo " Pattern matched: $PATTERN"
FOUND_SECRETS=1
fi
done
done
if [ $FOUND_SECRETS -eq 1 ]; then
echo ""
echo "⛔ COMMIT BLOCKED - Secrets detected!"
echo "📝 Actions:"
echo " 1. Remove the secrets from your code"
echo " 2. Use environment variables instead"
echo " 3. Add sensitive files to .gitignore"
exit 1
fi
echo "✅ No secrets detected. Proceeding..."
exit 0Example 3: Python Version with Comprehensive Checks
python
#!/usr/bin/env python3
"""
.git/hooks/pre-commit
Comprehensive pre-commit hook in Python
"""
import subprocess
import sys
import re
# Configuration
BLOCKED_PATTERNS = [
(r'TODO|FIXME|XXX', 'Unfinished work marker'),
(r'console\.log\(', 'Debug console.log'),
(r'debugger;', 'Debugger statement'),
(r'binding\.pry', 'Ruby debugger'),
(r'import pdb', 'Python debugger'),
]
SECRET_PATTERNS = [
(r'AKIA[0-9A-Z]{16}', 'AWS Access Key'),
(r'-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', 'Private Key'),
(r'ghp_[a-zA-Z0-9]{36}', 'GitHub Personal Token'),
(r'sk-[a-zA-Z0-9]{48}', 'OpenAI API Key'),
]
def get_staged_files():
"""Get list of staged files."""
result = subprocess.run(
['git', 'diff', '--cached', '--name-only', '--diff-filter=ACM'],
capture_output=True, text=True
)
return [f for f in result.stdout.strip().split('\n') if f]
def get_staged_content(filepath):
"""Get the staged content of a file."""
result = subprocess.run(
['git', 'show', f':{filepath}'],
capture_output=True, text=True
)
return result.stdout
def check_patterns(content, patterns, filepath):
"""Check content against patterns."""
issues = []
for line_num, line in enumerate(content.split('\n'), 1):
for pattern, description in patterns:
if re.search(pattern, line, re.IGNORECASE):
issues.append({
'file': filepath,
'line': line_num,
'description': description,
'content': line.strip()[:60]
})
return issues
def main():
print("🔍 Running pre-commit checks...")
staged_files = get_staged_files()
if not staged_files:
print("✅ No files to check.")
return 0
all_issues = []
security_issues = []
for filepath in staged_files:
content = get_staged_content(filepath)
# Check for blocked patterns
issues = check_patterns(content, BLOCKED_PATTERNS, filepath)
all_issues.extend(issues)
# Check for secrets
secrets = check_patterns(content, SECRET_PATTERNS, filepath)
security_issues.extend(secrets)
# Report security issues (higher priority)
if security_issues:
print("\n🚨 SECURITY ISSUES DETECTED:")
for issue in security_issues:
print(f" ❌ {issue['file']}:{issue['line']}")
print(f" {issue['description']}")
print("\n⛔ Commit BLOCKED - Remove secrets before committing!")
return 1
# Report code quality issues
if all_issues:
print("\n⚠️ Code quality issues:")
for issue in all_issues:
print(f" ⚡ {issue['file']}:{issue['line']} - {issue['description']}")
print(f"\n⛔ Commit BLOCKED - Fix {len(all_issues)} issue(s) first.")
print("💡 Use 'git commit --no-verify' to bypass (not recommended)")
return 1
print(f"✅ All {len(staged_files)} files passed checks!")
return 0
if __name__ == '__main__':
sys.exit(main())
)]📝 commit-msg - Enforce Message Standards
Conventional Commits Format
<type>(<scope>): <description>
[optional body]
[optional footer(s)]Types: feat, fix, docs, style, refactor, test, chore
Bash Implementation
#!/bin/bash
.git/hooks/commit-msg
Enforce Conventional Commits
COMMIT_MSG_FILE=
Conventional Commits regex
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(([a-z0-9-]+))?: .{1,72}$"
Get first line only
FIRST_LINE=
if ! echo "
Check description length (max 72 chars for first line)
DESC_LENGTH=${#FIRST_LINE} if [
echo "✅ Commit message valid: $FIRST_LINE" exit 0 } exit 0
### Python Implementation
```python
#!/usr/bin/env python3
"""
.git/hooks/commit-msg
Enforce Conventional Commits with Python
"""
import sys
import re
VALID_TYPES = [
'feat', 'fix', 'docs', 'style', 'refactor',
'test', 'chore', 'perf', 'ci', 'build', 'revert'
]
PATTERN = re.compile(
r'^(?P<type>' + '|'.join(VALID_TYPES) + r')'
r'(?:\((?P<scope>[a-z0-9-]+)\))?'
r': '
r'(?P<description>.{1,72})$'
)
def main():
commit_msg_file = sys.argv[1]
with open(commit_msg_file, 'r') as f:
commit_msg = f.read()
first_line = commit_msg.split('\n')[0]
# Skip merge commits
if first_line.startswith('Merge'):
return 0
match = PATTERN.match(first_line)
if not match:
print("\n❌ Invalid commit message format!")
print(f"\n Your message: \"{first_line}\"")
print("\n📝 Expected: <type>(<scope>): <description>")
print(f"\n🏷️ Valid types: {', '.join(VALID_TYPES)}")
print("\n📌 Examples:")
print(" feat(auth): add OAuth2 login")
print(" fix: resolve memory leak")
return 1
print(f"✅ Valid: [{match.group('type')}] {match.group('description')}")
return 0
if __name__ == '__main__':
sys.exit(main())🚀 pre-push - The Final Gate
Run Tests Before Push
bash
#!/bin/bash
# .git/hooks/pre-push
# Run tests before allowing push
echo "🧪 Running tests before push..."
# Determine project type and run appropriate tests
if [ -f "package.json" ]; then
echo "📦 Node.js project detected"
npm test
TEST_EXIT=$?
elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
echo "🐍 Python project detected"
python -m pytest -q
TEST_EXIT=$?
elif [ -f "go.mod" ]; then
echo "🐹 Go project detected"
go test ./...
TEST_EXIT=$?
else
echo "⚠️ No test configuration found. Skipping."
TEST_EXIT=0
fi
if [ $TEST_EXIT -ne 0 ]; then
echo ""
echo "⛔ Push BLOCKED - Tests failed!"
echo "💡 Fix failing tests before pushing."
exit 1
fi
echo "✅ All tests passed. Pushing..."
exit 0Prevent Push to Protected Branches
bash
#!/bin/bash
# .git/hooks/pre-push
# Block direct push to main/master
PROTECTED_BRANCHES="^(main|master|production)$"
while read local_ref local_sha remote_ref remote_sha; do
# Extract branch name
BRANCH=$(echo "$remote_ref" | sed 's|refs/heads/||')
if echo "$BRANCH" | grep -qE "$PROTECTED_BRANCHES"; then
echo "⛔ Direct push to '$BRANCH' is NOT allowed!"
echo "📝 Please create a Pull Request instead."
exit 1
fi
done
exit 0🔧 Sharing Hooks with Team
Problem
.git/hooks/ không được tracked bởi Git → Không thể share với team.
Solution: Tracked Hooks Directory
bash
# 1. Tạo thư mục hooks trong repo
mkdir -p .githooks
# 2. Copy hooks vào đó
cp .git/hooks/pre-commit .githooks/
# 3. Config Git để dùng thư mục này
git config core.hooksPath .githooks
# 4. Commit thư mục
git add .githooks
git commit -m "chore: add shared Git hooks"Team Setup Script
bash
#!/bin/bash
# setup-hooks.sh
# Team members run this after clone
echo "🔧 Setting up Git hooks..."
git config core.hooksPath .githooks
# Make hooks executable
chmod +x .githooks/*
echo "✅ Git hooks configured!"
echo "📍 Hooks location: .githooks/"🛠️ Modern Alternative: Husky (Node.js)
Nếu dùng Node.js, Husky là cách hiện đại hơn:
bash
# Install
npm install husky --save-dev
# Enable Git hooks
npx husky install
# Add pre-commit hook
npx husky add .husky/pre-commit "npm run lint"
# Add commit-msg hook
npx husky add .husky/commit-msg 'npx commitlint --edit "$1"'📊 Quick Reference
| Hook | Exit 0 | Exit Non-Zero | Bypass |
|---|---|---|---|
pre-commit | Allow commit | Block commit | --no-verify |
commit-msg | Accept message | Reject message | --no-verify |
pre-push | Allow push | Block push | --no-verify |
💡 Key Takeaways
HPN'S INSIGHT
"Hooks là automation đầu tiên bạn nên setup. Chúng chặn 90% lỗi ngớ ngẩn trước khi chúng vào history."
- Exit code quyết định tất cả: 0 = pass, non-zero = block
- Check staged content: Dùng
git show :file, không phải working directory - Share hooks với team: Dùng
core.hooksPathhoặc Husky - Escape hatch:
--no-verifyđể bypass (dùng có trách nhiệm) - Layer your defenses: pre-commit (lint) → commit-msg (format) → pre-push (tests)
PRODUCTION RULE
Mọi repo production phải có ít nhất:
pre-commit: Lint + Secret scanningcommit-msg: Conventional Commitspre-push: Tests phải pass