TechLead
Lesson 6 of 9
5 min read
Advanced Git

Git Hooks

Automate workflows with client-side and server-side hooks for validation, testing, and deployment.

Git Hooks Overview

Git hooks are scripts that run automatically at certain points in the Git workflow. They're powerful for enforcing standards and automating tasks.

Hook Types

Client-side Hooks:
├── pre-commit      # Before commit is created
├── prepare-commit-msg  # Before commit message editor
├── commit-msg      # Validate commit message
├── post-commit     # After commit is created
├── pre-rebase      # Before rebase starts
├── post-rewrite    # After commit is rewritten
├── post-checkout   # After checkout
├── post-merge      # After merge
└── pre-push        # Before push

Server-side Hooks:
├── pre-receive     # Before accepting push
├── update          # Per-branch before update
└── post-receive    # After push is accepted

pre-commit Hook

#!/bin/bash
# .git/hooks/pre-commit

# Run linter
npm run lint
if [ $? -ne 0 ]; then
  echo "Linting failed. Fix errors before committing."
  exit 1
fi

# Run tests
npm test
if [ $? -ne 0 ]; then
  echo "Tests failed. Fix tests before committing."
  exit 1
fi

# Check for console.log
if git diff --cached | grep -E "console\.log" > /dev/null; then
  echo "Warning: console.log found in staged changes"
  # exit 1  # Uncomment to block commit
fi

# Check for large files
for file in $(git diff --cached --name-only); do
  size=$(wc -c < "$file" 2>/dev/null || echo 0)
  if [ $size -gt 1000000 ]; then
    echo "Error: $file is larger than 1MB"
    exit 1
  fi
done

exit 0

commit-msg Hook

#!/bin/bash
# .git/hooks/commit-msg

commit_msg_file=$1
commit_msg=$(cat "$commit_msg_file")

# Conventional commits pattern
pattern="^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}"

if ! echo "$commit_msg" | grep -qE "$pattern"; then
  echo "Error: Commit message doesn't follow conventional commits"
  echo "Format: type(scope): description"
  echo "Types: feat, fix, docs, style, refactor, test, chore"
  exit 1
fi

# Check minimum length
msg_length=${#commit_msg}
if [ $msg_length -lt 10 ]; then
  echo "Error: Commit message too short (min 10 chars)"
  exit 1
fi

exit 0

pre-push Hook

#!/bin/bash
# .git/hooks/pre-push

remote="$1"
url="$2"

# Prevent push to main
current_branch=$(git branch --show-current)
if [ "$current_branch" = "main" ]; then
  echo "Error: Direct push to main is not allowed"
  echo "Please create a pull request instead"
  exit 1
fi

# Run full test suite before push
npm run test:all
if [ $? -ne 0 ]; then
  echo "Tests failed. Fix before pushing."
  exit 1
fi

# Check for WIP commits
if git log @{u}.. --oneline | grep -i "wip" > /dev/null; then
  echo "Warning: WIP commits detected. Continue? (y/n)"
  read -r response
  if [ "$response" != "y" ]; then
    exit 1
  fi
fi

exit 0

Using Husky

# Install Husky
npm install husky -D

# Initialize
npx husky init

# Add hook
echo "npm test" > .husky/pre-commit

# Add commit-msg hook
cat > .husky/commit-msg << 'EOF'
#!/bin/sh
npx --no -- commitlint --edit $1
EOF

Husky with lint-staged

// package.json
{
  "scripts": {
    "prepare": "husky"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss}": [
      "stylelint --fix"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}
# .husky/pre-commit
#!/bin/sh
npx lint-staged

Commitlint Configuration

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',
        'fix',
        'docs',
        'style',
        'refactor',
        'perf',
        'test',
        'build',
        'ci',
        'chore',
        'revert',
      ],
    ],
    'subject-min-length': [2, 'always', 10],
    'subject-max-length': [2, 'always', 72],
    'body-max-line-length': [2, 'always', 100],
  },
};

Server-side Hook Example

#!/bin/bash
# hooks/pre-receive (server-side)

while read oldrev newrev refname; do
  # Block force push
  if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
    if ! git merge-base --is-ancestor "$oldrev" "$newrev"; then
      echo "Error: Force push detected and blocked"
      exit 1
    fi
  fi

  # Block push to protected branches
  branch=$(echo "$refname" | sed 's/refs\/heads\///')
  if [[ "$branch" =~ ^(main|production)$ ]]; then
    echo "Error: Direct push to $branch is blocked"
    exit 1
  fi
done

exit 0

Sharing Hooks

# Store hooks in repository
mkdir -p .githooks

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

# Add to project setup
# In package.json or Makefile:
# "prepare": "git config core.hooksPath .githooks"

Common Hook Patterns

  • Pre-commit: Lint, format, run unit tests
  • Commit-msg: Validate message format
  • Pre-push: Full test suite, prevent WIP
  • Post-checkout: Install dependencies
  • Post-merge: Warn about dependency changes

Continue Learning