Automate Your Workflow with Git Hooks

David Childs
โ€ข

Git hooks are powerful scripts that Git executes before or after events such as commit, push, and receive. They're your secret weapon for automating repetitive tasks and enforcing code quality standards.

Understanding Git Hooks

Git hooks are scripts that live in the .git/hooks directory of every Git repository. They're triggered automatically by Git events and can be written in any scripting language.

Types of Git Hooks

Client-Side Hooks

  • pre-commit: Runs before a commit is created
  • prepare-commit-msg: Runs before the commit message editor is fired up
  • commit-msg: Validates the commit message
  • post-commit: Runs after a commit is created
  • pre-push: Runs before code is pushed to remote
  • pre-rebase: Runs before a rebase

Server-Side Hooks

  • pre-receive: Runs when receiving a push
  • update: Similar to pre-receive but runs once per branch
  • post-receive: Runs after a push is received

Setting Up Your First Hook

Basic Pre-Commit Hook

Create .git/hooks/pre-commit:

#!/bin/sh
# Run tests before commit

echo "Running tests..."
npm test

if [ $? -ne 0 ]; then
    echo "Tests failed. Commit aborted."
    exit 1
fi

echo "Tests passed. Proceeding with commit."

Make it executable:

chmod +x .git/hooks/pre-commit

Practical Git Hook Examples

1. Code Quality Pre-Commit Hook

#!/bin/sh
# .git/hooks/pre-commit
# Enforce code quality standards

# Run ESLint
echo "๐Ÿ” Running ESLint..."
npx eslint --fix .
if [ $? -ne 0 ]; then
    echo "โŒ ESLint found errors. Please fix them before committing."
    exit 1
fi

# Run Prettier
echo "๐Ÿ’… Running Prettier..."
npx prettier --write .
git add -A

# Check for console.log statements
echo "๐Ÿ” Checking for console.log statements..."
if git diff --cached | grep -E "\+.*console\.log"; then
    echo "โš ๏ธ  Warning: console.log statements detected"
    read -p "Continue with commit? (y/n) " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
    fi
fi

echo "โœ… All checks passed!"

2. Commit Message Validation

#!/bin/sh
# .git/hooks/commit-msg
# Enforce conventional commit format

commit_regex='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,50}'

if ! grep -qE "$commit_regex" "$1"; then
    echo "โŒ Invalid commit message format!"
    echo "๐Ÿ“ Format: <type>(<scope>): <subject>"
    echo "๐Ÿ“ Example: feat(auth): add login functionality"
    echo ""
    echo "Types: feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert"
    exit 1
fi

3. Pre-Push Protection

#!/bin/sh
# .git/hooks/pre-push
# Prevent pushing to protected branches

protected_branches="main master production"
current_branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')

for branch in $protected_branches; do
    if [ "$current_branch" = "$branch" ]; then
        echo "โŒ Direct push to $branch branch is not allowed!"
        echo "Please create a feature branch and open a pull request."
        exit 1
    fi
done

# Run tests before push
echo "๐Ÿงช Running tests before push..."
npm test
if [ $? -ne 0 ]; then
    echo "โŒ Tests failed. Push aborted."
    exit 1
fi

echo "โœ… Push validation passed!"

4. Auto-Update Dependencies Post-Merge

#!/bin/sh
# .git/hooks/post-merge
# Auto-install dependencies after merge

changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"

check_run() {
    echo "$changed_files" | grep -E "$1" > /dev/null 2>&1
}

# JavaScript/Node.js
if check_run "package.json"; then
    echo "๐Ÿ“ฆ package.json changed, running npm install..."
    npm install
fi

# Python
if check_run "requirements.txt"; then
    echo "๐Ÿ requirements.txt changed, updating pip packages..."
    pip install -r requirements.txt
fi

# Ruby
if check_run "Gemfile"; then
    echo "๐Ÿ’Ž Gemfile changed, running bundle install..."
    bundle install
fi

Managing Hooks with Husky

Husky makes Git hooks shareable and easier to manage:

Installation

npm install --save-dev husky
npx husky install

Configuration

// package.json
{
  "scripts": {
    "prepare": "husky install"
  }
}

Adding Hooks

# Add pre-commit hook
npx husky add .husky/pre-commit "npm test"

# Add commit-msg hook
npx husky add .husky/commit-msg 'npx commitlint --edit "$1"'

Advanced Hook Patterns

1. Conditional Hooks

#!/bin/sh
# Only run on specific file changes

files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.jsx\?$')

if [ -z "$files" ]; then
    exit 0
fi

echo "$files" | xargs npx eslint

2. Branch-Specific Hooks

#!/bin/sh
# Different rules for different branches

branch=$(git rev-parse --abbrev-ref HEAD)

if [ "$branch" = "develop" ]; then
    npm run test:unit
elif [ "$branch" = "release" ]; then
    npm run test:all
    npm run build
fi

3. Security Scanning

#!/bin/sh
# Scan for secrets before commit

if which gitleaks > /dev/null; then
    gitleaks protect --verbose --redact --staged
    if [ $? -ne 0 ]; then
        echo "โŒ Potential secrets detected! Commit aborted."
        exit 1
    fi
fi

Hook Templates and Sharing

Creating a Hooks Directory

# Create shared hooks directory
mkdir .githooks

# Configure Git to use it
git config core.hooksPath .githooks

# Add to repository
git add .githooks
git commit -m "Add shared Git hooks"

Team Setup Script

#!/bin/sh
# setup-hooks.sh

echo "Setting up Git hooks..."
git config core.hooksPath .githooks
chmod +x .githooks/*
echo "โœ… Git hooks configured!"

Best Practices

1. Keep Hooks Fast

# Run only on changed files
files=$(git diff --cached --name-only)
if [ -z "$files" ]; then
    exit 0
fi

2. Provide Bypass Options

# Allow emergency commits
if [ "$SKIP_HOOKS" = "1" ]; then
    echo "โš ๏ธ  Skipping hooks (SKIP_HOOKS=1)"
    exit 0
fi

3. Give Clear Feedback

# Informative messages
echo "๐Ÿ” Checking code style..."
echo "โœ… Code style check passed"
echo "โŒ Code style check failed:"
echo "   - Line 42: Missing semicolon"
echo "   Run 'npm run fix' to auto-fix"

Troubleshooting Common Issues

Hook Not Executing

# Check permissions
ls -la .git/hooks/
# Fix: chmod +x .git/hooks/pre-commit

# Check shebang
head -n 1 .git/hooks/pre-commit
# Should be: #!/bin/sh or #!/bin/bash

Bypassing Hooks

# Skip pre-commit and commit-msg hooks
git commit --no-verify -m "Emergency fix"

# Skip pre-push hook
git push --no-verify

Debugging Hooks

#!/bin/sh
# Add debug output
set -x  # Enable debug mode
echo "Current directory: $(pwd)"
echo "Git root: $(git rev-parse --show-toplevel)"
echo "Changed files: $(git diff --cached --name-only)"

Conclusion

Git hooks are a powerful way to automate your workflow and maintain code quality. Start with simple hooks and gradually add more sophisticated checks as your team grows comfortable with them.

Remember: hooks should help, not hinder. If they become a bottleneck, revisit and optimize them. The goal is to catch issues early while maintaining developer productivity.

With the right hooks in place, you'll spend less time on repetitive tasks and more time writing great code.

Share this article

DC

David Childs

Consulting Systems Engineer with over 10 years of experience building scalable infrastructure and helping organizations optimize their technology stack.

Related Articles