Skip to content

Latest commit

 

History

History
395 lines (313 loc) · 7.29 KB

File metadata and controls

395 lines (313 loc) · 7.29 KB

Pre-commit Hook Development Guide

Table of Contents

  1. Introduction
  2. Hook Architecture
  3. Writing Your First Hook
  4. Best Practices
  5. Testing Hooks
  6. Security Guidelines
  7. Common Patterns
  8. Troubleshooting

Introduction

Pre-commit hooks are scripts that run automatically before a commit is made to ensure code quality, consistency, and security. This guide will help you develop robust, secure, and efficient hooks for this repository.

Hook Architecture

Directory Structure

hooks/
├── ci/           # CI/CD related hooks
├── commits/      # Commit message hooks
├── configs/      # Configuration file hooks
├── nix/          # Nix-specific hooks
├── terraform/    # Terraform hooks
├── web/          # Web development hooks
│   ├── css/      # CSS linting
│   ├── js/       # JavaScript tools
│   └── scss/     # SCSS linting
└── ...           # Other categories

Hook Anatomy

Every hook should follow this basic structure:

#!/usr/bin/env bash

# 1. Set error handling
set -euo pipefail

# 2. Define help text
show_help() {
  cat << EOF
Hook: your-hook-name
Purpose: Brief description of what this hook does
Usage: your-hook.sh [options] [files...]

Options:
  -h, --help    Show this help message
  -v, --verbose Enable verbose output
EOF
}

# 3. Check dependencies
check_dependencies() {
  if ! command -v required-tool &> /dev/null; then
    echo "Error: required-tool is not installed"
    echo "Install with: [installation command]"
    exit 1
  fi
}

# 4. Main logic
main() {
  # Parse arguments
  while [[ $# -gt 0 ]]; do
    case $1 in
      -h|--help)
        show_help
        exit 0
        ;;
      -v|--verbose)
        VERBOSE=1
        shift
        ;;
      *)
        FILES+=("$1")
        shift
        ;;
    esac
  done

  # Check dependencies
  check_dependencies

  # Process files
  for file in "${FILES[@]}"; do
    process_file "$file"
  done
}

# 5. Run main
main "$@"

Writing Your First Hook

Step 1: Create the Hook File

# Create hook file
touch hooks/category/my-hook.sh
chmod +x hooks/category/my-hook.sh

Step 2: Implement Core Functionality

#!/usr/bin/env bash

set -euo pipefail

# Simple example: Check file size
MAX_SIZE=1048576  # 1MB

for file in "$@"; do
  if [[ -f "$file" ]]; then
    size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file")
    if [[ $size -gt $MAX_SIZE ]]; then
      echo "Error: $file exceeds maximum size ($size > $MAX_SIZE bytes)"
      exit 1
    fi
  fi
done

echo "All files pass size check"

Step 3: Add Error Handling

# Trap errors and clean up
cleanup() {
  local exit_code=$?
  # Clean up temporary files
  rm -f "$TEMP_FILE"
  exit $exit_code
}

trap cleanup EXIT ERR

Best Practices

1. Input Validation

Always validate and sanitize inputs:

# Good: Quote variables
process_file "$file"

# Bad: Unquoted variable
process_file $file

# Good: Validate file exists
if [[ ! -f "$file" ]]; then
  echo "Error: File not found: $file"
  exit 1
fi

2. Dependency Management

Check for required tools:

# Check if in npm project
if [[ -f "package.json" ]]; then
  # Use local installation
  npx eslint "$@"
else
  # Install globally or fail gracefully
  if ! command -v eslint &> /dev/null; then
    echo "Installing eslint..."
    npm install -g eslint
  fi
  eslint "$@"
fi

3. Exit Codes

Use consistent exit codes:

  • 0: Success
  • 1: General error
  • 2: Misuse of shell command
  • 126: Command cannot execute
  • 127: Command not found

4. Progress Feedback

Provide clear feedback:

# Show progress for long operations
echo "Processing ${#FILES[@]} files..."

for i in "${!FILES[@]}"; do
  echo "[$((i+1))/${#FILES[@]}] Processing ${FILES[$i]}"
  process_file "${FILES[$i]}"
done

Testing Hooks

1. Create Test File

Create tests/category/test_my_hook.bats:

#!/usr/bin/env bats

load ../test_helper

setup() {
  setup_test_env
}

teardown() {
  teardown_test_env
}

@test "hook exists and is executable" {
  run test -x "$ORIGINAL_DIR/hooks/category/my-hook.sh"
  [ "$status" -eq 0 ]
}

@test "hook validates input correctly" {
  create_test_file "test.txt" "content"

  run "$ORIGINAL_DIR/hooks/category/my-hook.sh" "test.txt"
  [ "$status" -eq 0 ]
}

@test "hook fails on invalid input" {
  run "$ORIGINAL_DIR/hooks/category/my-hook.sh" "nonexistent.txt"
  [ "$status" -eq 1 ]
  [[ "$output" =~ "not found" ]]
}

2. Run Tests

# Run single test
bats tests/category/test_my_hook.bats

# Run all tests
./tests/run_tests.sh

Security Guidelines

1. Command Injection Prevention

# Bad: Direct interpolation
eval "command $user_input"

# Good: Use arrays
cmd=("command" "$user_input")
"${cmd[@]}"

2. Path Traversal Protection

# Validate paths
realpath=$(readlink -f "$file")
if [[ ! "$realpath" =~ ^"$(pwd)" ]]; then
  echo "Error: Path traversal attempt"
  exit 1
fi

3. Temporary File Security

# Create secure temp files
TEMP_FILE=$(mktemp)
trap 'rm -f "$TEMP_FILE"' EXIT

# Set restrictive permissions
chmod 600 "$TEMP_FILE"

Common Patterns

1. Processing Multiple Files

# Process files in parallel
process_files() {
  local pids=()

  for file in "$@"; do
    process_file "$file" &
    pids+=($!)
  done

  # Wait for all processes
  for pid in "${pids[@]}"; do
    wait "$pid" || exit_code=$?
  done

  return ${exit_code:-0}
}

2. Configuration Handling

# Load configuration
CONFIG_FILE="${HOME}/.hookconfig"
if [[ -f "$CONFIG_FILE" ]]; then
  source "$CONFIG_FILE"
fi

# Use defaults
MAX_SIZE="${MAX_SIZE:-1048576}"
VERBOSE="${VERBOSE:-0}"

3. Cross-Platform Compatibility

# Handle different stat commands
get_file_size() {
  local file=$1
  if [[ "$OSTYPE" == "darwin"* ]]; then
    stat -f%z "$file"
  else
    stat -c%s "$file"
  fi
}

Troubleshooting

Common Issues

  1. Hook not executable

    chmod +x hooks/category/my-hook.sh
  2. Command not found

    # Add to PATH or use full path
    export PATH="/usr/local/bin:$PATH"
  3. Permission denied

    # Check file permissions
    ls -la hooks/category/my-hook.sh

Debugging Tips

  1. Enable debug mode

    set -x  # Print commands as executed
  2. Add logging

    debug() {
      [[ $VERBOSE -eq 1 ]] && echo "DEBUG: $*" >&2
    }
  3. Test in isolation

    # Test hook directly
    ./hooks/category/my-hook.sh test-file.txt

Contributing

When contributing a new hook:

  1. Follow the directory structure
  2. Include comprehensive tests
  3. Document dependencies
  4. Add usage examples
  5. Ensure cross-platform compatibility
  6. Run all tests before submitting

Examples

See existing hooks for examples:

  • Simple: hooks/configs/yamlfmt.sh
  • Complex: hooks/witness.sh
  • With dependencies: hooks/nix/nix-build.sh

Resources