#!/usr/bin/env bash # PiFlash Pre-commit Hook # Validates staged changes before commit set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' print_status() { echo -e "${BLUE}[PiFlash]${NC} $1" } print_success() { echo -e "${GREEN}✅${NC} $1" } print_warning() { echo -e "${YELLOW}⚠️${NC} $1" } print_error() { echo -e "${RED}❌${NC} $1" } require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then print_error "Required command not found: $1" exit 1 fi } get_file_size() { local file="$1" stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null } cleanup_trailing_whitespace() { local file="$1" python3 - "$file" <<'PY' from pathlib import Path import sys path = Path(sys.argv[1]) data = path.read_text(encoding="utf-8", errors="ignore").splitlines() path.write_text("\n".join(line.rstrip() for line in data) + ("\n" if data else ""), encoding="utf-8") PY } validate_css_braces() { local file="$1" node - "$file" <<'NODE' const fs = require('fs'); const path = process.argv[2]; const css = fs.readFileSync(path, 'utf8'); const openBraces = (css.match(/\{/g) || []).length; const closeBraces = (css.match(/\}/g) || []).length; if (openBraces !== closeBraces) { console.error(`Mismatched braces in ${path}`); process.exit(1); } NODE } validate_json() { local file="$1" node - "$file" <<'NODE' const fs = require('fs'); const path = process.argv[2]; JSON.parse(fs.readFileSync(path, 'utf8')); NODE } scan_sensitive_content() { local file="$1" local content content="$(git show ":$file" 2>/dev/null || true)" if [[ -z "$content" ]]; then return 0 fi local patterns=( "password[[:space:]]*[:=][[:space:]]*['\"][^'\"]+['\"]" "api[_-]?key[[:space:]]*[:=][[:space:]]*['\"][^'\"]+['\"]" "secret[[:space:]]*[:=][[:space:]]*['\"][^'\"]+['\"]" "token[[:space:]]*[:=][[:space:]]*['\"][^'\"]+['\"]" "private[_-]?key" "BEGIN[[:space:]]+PRIVATE[[:space:]]+KEY" "ghp_[A-Za-z0-9_]+" "sk-[A-Za-z0-9]+" ) local pattern for pattern in "${patterns[@]}"; do if grep -iE "$pattern" <<<"$content" >/dev/null 2>&1; then print_error "Potential sensitive data found in staged content: $file" print_error "Matched pattern: $pattern" exit 1 fi done } print_status "Running pre-commit checks for PiFlash..." if ! git rev-parse --git-dir >/dev/null 2>&1; then print_error "Not in a git repository" exit 1 fi require_cmd git require_cmd grep require_cmd file STAGED_FILES=() while IFS= read -r -d '' file; do STAGED_FILES+=("$file") done < <(git diff --cached --name-only --diff-filter=ACM -z) if [[ ${#STAGED_FILES[@]} -eq 0 ]]; then print_warning "No staged files found" exit 0 fi print_status "Staged files: ${#STAGED_FILES[@]}" HTML_FILES=() CSS_FILES=() JS_FILES=() JSON_FILES=() for file in "${STAGED_FILES[@]}"; do case "$file" in *.html|*.htm) HTML_FILES+=("$file") ;; *.css|*.scss|*.sass) CSS_FILES+=("$file") ;; *.js|*.jsx|*.ts|*.tsx|*.mjs|*.cjs) JS_FILES+=("$file") ;; *.json) JSON_FILES+=("$file") ;; esac done print_status "Validating HTML files..." if [[ ${#HTML_FILES[@]} -eq 0 ]]; then print_status "No HTML files to validate" else for file in "${HTML_FILES[@]}"; do [[ -f "$file" ]] || continue if ! grep -q "" "$file"; then print_error "Missing DOCTYPE in $file" exit 1 fi if ! grep -q "" "$file"; then print_error "Invalid HTML structure in $file" exit 1 fi print_success "HTML validation passed for $file" done fi print_status "Validating CSS files..." if [[ ${#CSS_FILES[@]} -eq 0 ]]; then print_status "No CSS files to validate" else require_cmd node for file in "${CSS_FILES[@]}"; do [[ -f "$file" ]] || continue if ! validate_css_braces "$file" >/dev/null 2>&1; then print_error "CSS syntax error in $file" exit 1 fi print_success "CSS validation passed for $file" done fi print_status "Validating JavaScript/TypeScript files..." if [[ ${#JS_FILES[@]} -eq 0 ]]; then print_status "No JavaScript files to validate" else require_cmd node for file in "${JS_FILES[@]}"; do [[ -f "$file" ]] || continue if [[ "$file" =~ \.(js|mjs|cjs)$ ]]; then if ! node -c "$file" >/dev/null 2>&1; then print_error "JavaScript syntax error in $file" exit 1 fi else print_warning "Skipping syntax parse for $file (Node cannot directly parse TypeScript)" fi if grep -n "console\.log" "$file" >/dev/null 2>&1; then print_warning "console.log found in $file" fi if grep -niE "TODO|FIXME|HACK" "$file" >/dev/null 2>&1; then print_warning "TODO/FIXME/HACK comments found in $file" fi print_success "Script validation passed for $file" done fi print_status "Validating JSON files..." if [[ ${#JSON_FILES[@]} -eq 0 ]]; then print_status "No JSON files to validate" else require_cmd node for file in "${JSON_FILES[@]}"; do [[ -f "$file" ]] || continue if ! validate_json "$file" >/dev/null 2>&1; then print_error "Invalid JSON in $file" exit 1 fi print_success "JSON validation passed for $file" done fi print_status "Checking file sizes..." MAX_SIZE=1048576 for file in "${STAGED_FILES[@]}"; do [[ -f "$file" ]] || continue size="$(get_file_size "$file" 2>/dev/null || echo 0)" if [[ "$size" -gt "$MAX_SIZE" ]]; then print_error "File too large: $file ($(($size / 1024))KB > 1024KB)" exit 1 fi done print_success "File size check passed" print_status "Checking line endings..." for file in "${STAGED_FILES[@]}"; do [[ -f "$file" ]] || continue if file "$file" | grep -q "CRLF"; then print_warning "CRLF line endings found in $file" fi done print_success "Line ending check completed" print_status "Checking for trailing whitespace..." WHITESPACE_FILES=() for file in "${STAGED_FILES[@]}"; do [[ -f "$file" ]] || continue if grep -n '[[:blank:]]$' "$file" >/dev/null 2>&1; then WHITESPACE_FILES+=("$file") fi done if [[ ${#WHITESPACE_FILES[@]} -gt 0 ]]; then print_warning "Trailing whitespace found in ${#WHITESPACE_FILES[@]} file(s)" print_status "Fixing trailing whitespace and re-staging..." require_cmd python3 for file in "${WHITESPACE_FILES[@]}"; do cleanup_trailing_whitespace "$file" git add "$file" done print_success "Trailing whitespace fixed and re-staged" else print_success "No trailing whitespace found" fi if [[ -n "${1:-}" && -f "${1:-}" ]]; then print_status "Checking commit message..." commit_msg="$(head -n1 "$1" | tr -d '\r')" if [[ ${#commit_msg} -gt 72 ]]; then print_warning "Commit message is longer than 72 characters" fi if grep -qE '^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+' <<<"$commit_msg"; then print_success "Conventional commit format detected" else print_warning "Consider using conventional commit format" fi fi print_status "Scanning staged content for sensitive data..." for file in "${STAGED_FILES[@]}"; do scan_sensitive_content "$file" done print_success "Security scan completed" print_success "All pre-commit checks passed" print_status "Ready to commit ${#STAGED_FILES[@]} file(s)" exit 0