| #!/usr/bin/env bash |
|
|
| |
| |
|
|
| 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 "<!DOCTYPE html>" "$file"; then |
| print_error "Missing DOCTYPE in $file" |
| exit 1 |
| fi |
|
|
| if ! grep -q "<html" "$file" || ! grep -q "</html>" "$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 |