Files
gh-dhofheinz-open-plugins-p…/commands/commit-best-practices/.scripts/pre-commit-check.sh
2025-11-29 18:20:25 +08:00

323 lines
9.5 KiB
Bash
Executable File

#!/usr/bin/env bash
################################################################################
# Pre-Commit Validation Script
#
# Purpose: Run comprehensive pre-commit checks to ensure code quality
# Version: 1.0.0
# Usage: ./pre-commit-check.sh [quick:true|false]
# Returns: JSON with validation results
# Exit Codes:
# 0 = All checks passed
# 1 = One or more checks failed
# 2 = Script execution error
################################################################################
set -euo pipefail
# Default to full validation
QUICK_MODE="${1:-quick:false}"
QUICK_MODE="${QUICK_MODE#quick:}"
# Initialize results
OVERALL_STATUS="pass"
declare -A RESULTS
################################################################################
# Check if tests pass
################################################################################
check_tests() {
local status="skip"
local message="Tests skipped in quick mode"
if [[ "$QUICK_MODE" != "true" ]]; then
# Detect test framework and run tests
if [[ -f "package.json" ]] && grep -q "\"test\":" package.json; then
if npm test &>/dev/null; then
status="pass"
message="All tests passed"
else
status="fail"
message="Tests failing"
OVERALL_STATUS="fail"
fi
elif [[ -f "pytest.ini" ]] || [[ -f "setup.py" ]]; then
if python -m pytest &>/dev/null; then
status="pass"
message="All tests passed"
else
status="fail"
message="Tests failing"
OVERALL_STATUS="fail"
fi
elif [[ -f "Cargo.toml" ]]; then
if cargo test &>/dev/null; then
status="pass"
message="All tests passed"
else
status="fail"
message="Tests failing"
OVERALL_STATUS="fail"
fi
elif [[ -f "go.mod" ]]; then
if go test ./... &>/dev/null; then
status="pass"
message="All tests passed"
else
status="fail"
message="Tests failing"
OVERALL_STATUS="fail"
fi
else
status="skip"
message="No test framework detected"
fi
fi
echo "{\"status\": \"$status\", \"message\": \"$message\"}"
}
################################################################################
# Check if lint passes
################################################################################
check_lint() {
local status="skip"
local message="Linting skipped in quick mode"
if [[ "$QUICK_MODE" != "true" ]]; then
# Detect linter and run
if [[ -f "package.json" ]] && (grep -q "eslint" package.json || [[ -f ".eslintrc.json" ]]); then
if npx eslint . --max-warnings 0 &>/dev/null; then
status="pass"
message="Linting passed"
else
status="fail"
message="Linting errors found"
OVERALL_STATUS="fail"
fi
elif command -v pylint &>/dev/null; then
if pylint $(git diff --cached --name-only --diff-filter=ACM | grep '\.py$') &>/dev/null; then
status="pass"
message="Linting passed"
else
status="fail"
message="Linting errors found"
OVERALL_STATUS="fail"
fi
elif command -v clippy &>/dev/null; then
if cargo clippy -- -D warnings &>/dev/null; then
status="pass"
message="Linting passed"
else
status="fail"
message="Linting errors found"
OVERALL_STATUS="fail"
fi
else
status="skip"
message="No linter detected"
fi
fi
echo "{\"status\": \"$status\", \"message\": \"$message\"}"
}
################################################################################
# Check for debug code in staged files
################################################################################
check_debug_code() {
local count=0
local locations=()
# Get staged diff
local diff_output
diff_output=$(git diff --cached)
# Search for debug patterns in added lines only
local debug_patterns=(
'console\.log'
'console\.debug'
'console\.error'
'debugger'
'print\('
'println!'
'pdb\.set_trace'
'binding\.pry'
'byebug'
'Debug\.Log'
)
for pattern in "${debug_patterns[@]}"; do
while IFS= read -r line; do
if [[ -n "$line" ]]; then
((count++))
locations+=("\"$line\"")
fi
done < <(echo "$diff_output" | grep "^+" | grep -v "^+++" | grep -E "$pattern" || true)
done
local status="pass"
local message="No debug code found"
if [[ $count -gt 0 ]]; then
status="fail"
message="Found $count debug statement(s)"
OVERALL_STATUS="fail"
fi
# Format locations array
local locations_json="[]"
if [[ ${#locations[@]} -gt 0 ]]; then
locations_json="[$(IFS=,; echo "${locations[*]}")]"
fi
echo "{\"status\": \"$status\", \"message\": \"$message\", \"count\": $count, \"locations\": $locations_json}"
}
################################################################################
# Check for TODOs in staged files
################################################################################
check_todos() {
local count=0
local locations=()
# Get staged diff
local diff_output
diff_output=$(git diff --cached)
# Search for TODO patterns in added lines only
local todo_patterns=(
'TODO'
'FIXME'
'XXX'
'HACK'
)
for pattern in "${todo_patterns[@]}"; do
while IFS= read -r line; do
if [[ -n "$line" ]]; then
((count++))
locations+=("\"$line\"")
fi
done < <(echo "$diff_output" | grep "^+" | grep -v "^+++" | grep -E "$pattern" || true)
done
local status="pass"
local message="No TODOs in staged code"
if [[ $count -gt 0 ]]; then
status="warn"
message="Found $count TODO/FIXME comment(s)"
# TODOs are warning, not failure (project decision)
fi
# Format locations array
local locations_json="[]"
if [[ ${#locations[@]} -gt 0 ]]; then
locations_json="[$(IFS=,; echo "${locations[*]}")]"
fi
echo "{\"status\": \"$status\", \"message\": \"$message\", \"count\": $count, \"locations\": $locations_json}"
}
################################################################################
# Check for merge conflict markers
################################################################################
check_merge_markers() {
local count=0
local locations=()
# Get staged files
local staged_files
staged_files=$(git diff --cached --name-only --diff-filter=ACM || true)
if [[ -n "$staged_files" ]]; then
# Search for conflict markers in staged files
while IFS= read -r file; do
if [[ -f "$file" ]]; then
local markers
markers=$(grep -n -E '^(<<<<<<<|=======|>>>>>>>)' "$file" || true)
if [[ -n "$markers" ]]; then
while IFS= read -r marker; do
((count++))
locations+=("\"$file:$marker\"")
done <<< "$markers"
fi
fi
done <<< "$staged_files"
fi
local status="pass"
local message="No merge markers found"
if [[ $count -gt 0 ]]; then
status="fail"
message="Found $count merge conflict marker(s)"
OVERALL_STATUS="fail"
fi
# Format locations array
local locations_json="[]"
if [[ ${#locations[@]} -gt 0 ]]; then
locations_json="[$(IFS=,; echo "${locations[*]}")]"
fi
echo "{\"status\": \"$status\", \"message\": \"$message\", \"count\": $count, \"locations\": $locations_json}"
}
################################################################################
# Main execution
################################################################################
main() {
# Verify we're in a git repository
if ! git rev-parse --git-dir &>/dev/null; then
echo "{\"error\": \"Not a git repository\"}"
exit 2
fi
# Check if there are staged changes
if ! git diff --cached --quiet 2>/dev/null; then
: # Has staged changes, continue
else
echo "{\"error\": \"No staged changes to validate\"}"
exit 2
fi
# Run all checks
local tests_result
local lint_result
local debug_result
local todos_result
local markers_result
tests_result=$(check_tests)
lint_result=$(check_lint)
debug_result=$(check_debug_code)
todos_result=$(check_todos)
markers_result=$(check_merge_markers)
# Build JSON output
cat <<EOF
{
"status": "$OVERALL_STATUS",
"quick_mode": $([[ "$QUICK_MODE" == "true" ]] && echo "true" || echo "false"),
"checks": {
"tests": $tests_result,
"lint": $lint_result,
"debug_code": $debug_result,
"todos": $todos_result,
"merge_markers": $markers_result
}
}
EOF
# Exit with appropriate code
if [[ "$OVERALL_STATUS" == "fail" ]]; then
exit 1
else
exit 0
fi
}
# Execute main function
main "$@"