Files
gh-dhofheinz-open-plugins-p…/commands/message-generation/.scripts/message-validator.sh
2025-11-29 18:20:25 +08:00

336 lines
8.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Script: message-validator.sh
# Purpose: Validate commit message against conventional commits standard
# Author: Git Commit Assistant Plugin
# Version: 1.0.0
#
# Usage:
# export MESSAGE="feat: add feature"
# export STRICT_MODE=false
# export MAX_SUBJECT=50
# export MAX_LINE=72
# ./message-validator.sh
#
# Environment Variables:
# MESSAGE - Commit message to validate (required)
# STRICT_MODE - Enable strict validation (default: false)
# MAX_SUBJECT - Maximum subject length (default: 50)
# MAX_LINE - Maximum body line length (default: 72)
#
# Returns:
# Validation report to stdout
# Exit code indicates validation status
#
# Exit Codes:
# 0 - Valid message
# 1 - Invalid message (warnings in normal mode, any issue in strict mode)
# 2 - Error (missing input, malformed message)
set -euo pipefail
# Default values
STRICT_MODE="${STRICT_MODE:-false}"
MAX_SUBJECT="${MAX_SUBJECT:-50}"
MAX_LINE="${MAX_LINE:-72}"
# Validation counters
declare -a ERRORS=()
declare -a WARNINGS=()
declare -a SUGGESTIONS=()
# Score tracking
SCORE=100
# Validate MESSAGE is provided
if [ -z "${MESSAGE:-}" ]; then
echo "ERROR: MESSAGE environment variable is required" >&2
exit 2
fi
# Valid commit types
VALID_TYPES="feat fix docs style refactor perf test build ci chore revert"
# Function to check commit type
validate_type() {
local subject="$1"
# Extract type (before colon or parenthesis)
local type=$(echo "$subject" | grep -oP '^[a-z]+' || echo "")
if [ -z "$type" ]; then
ERRORS+=("No commit type found")
SCORE=$((SCORE - 20))
return 1
fi
# Check if type is valid
if ! echo "$VALID_TYPES" | grep -qw "$type"; then
ERRORS+=("Invalid commit type: '$type'")
ERRORS+=("Valid types: $VALID_TYPES")
SCORE=$((SCORE - 20))
return 1
fi
return 0
}
# Function to check scope format
validate_scope() {
local subject="$1"
# Check if scope exists
if echo "$subject" | grep -qP '^\w+\([^)]+\):'; then
local scope=$(echo "$subject" | grep -oP '^\w+\(\K[^)]+')
# Scope should be lowercase alphanumeric with hyphens
if ! echo "$scope" | grep -qP '^[a-z0-9-]+$'; then
WARNINGS+=("Scope should be lowercase alphanumeric with hyphens: '$scope'")
SCORE=$((SCORE - 5))
fi
fi
}
# Function to validate subject line
validate_subject() {
local subject="$1"
# Check format: type(scope): description or type: description
if ! echo "$subject" | grep -qP '^[a-z]+(\([a-z0-9-]+\))?: .+'; then
ERRORS+=("Subject does not match conventional commits format")
ERRORS+=("Expected: <type>(<scope>): <description> or <type>: <description>")
SCORE=$((SCORE - 30))
return 1
fi
# Validate type
validate_type "$subject"
# Validate scope if present
validate_scope "$subject"
# Extract description (after ": ")
local description=$(echo "$subject" | sed 's/^[^:]*: //')
# Check length
local length=${#subject}
if [ "$length" -gt "$MAX_SUBJECT" ]; then
if [ "$length" -gt 72 ]; then
ERRORS+=("Subject exceeds hard limit of 72 characters ($length chars)")
SCORE=$((SCORE - 30))
else
WARNINGS+=("Subject exceeds recommended $MAX_SUBJECT characters ($length chars)")
SUGGESTIONS+=("Consider shortening subject or moving details to body")
SCORE=$((SCORE - 10))
fi
fi
# Check for capital letter after colon
if echo "$description" | grep -qP '^[A-Z]'; then
WARNINGS+=("Description should not start with capital letter")
SUGGESTIONS+=("Use lowercase after colon: '$(echo "${description:0:1}" | tr '[:upper:]' '[:lower:]')${description:1}'")
SCORE=$((SCORE - 5))
fi
# Check for period at end
if [[ "$description" =~ \.$ ]]; then
WARNINGS+=("Subject should not end with period")
SUGGESTIONS+=("Remove period at end")
SCORE=$((SCORE - 3))
fi
# Check for imperative mood (simple heuristics)
if echo "$description" | grep -qP '\b(added|fixed|updated|removed|changed|improved|created|deleted)\b'; then
WARNINGS+=("Use imperative mood (add, fix, update) not past tense")
SCORE=$((SCORE - 5))
fi
if echo "$description" | grep -qP '\b(adds|fixes|updates|removes|changes|improves|creates|deletes)\b'; then
WARNINGS+=("Use imperative mood (add, fix, update) not present tense")
SCORE=$((SCORE - 5))
fi
# Check for vague descriptions
if echo "$description" | grep -qiP '\b(update|change|fix|improve)\s+(code|file|stuff|thing)\b'; then
SUGGESTIONS+=("Be more specific in description")
SCORE=$((SCORE - 5))
fi
return 0
}
# Function to validate body
validate_body() {
local body="$1"
if [ -z "$body" ]; then
return 0 # Body is optional
fi
# Check line lengths
while IFS= read -r line; do
local length=${#line}
if [ "$length" -gt "$MAX_LINE" ]; then
WARNINGS+=("Body line exceeds $MAX_LINE characters ($length chars)")
SCORE=$((SCORE - 3))
fi
done <<< "$body"
# Check for imperative mood in body
if echo "$body" | grep -qP '\b(added|fixed|updated|removed|changed|improved|created|deleted)\b'; then
SUGGESTIONS+=("Consider using imperative mood in body")
fi
return 0
}
# Function to validate footer
validate_footer() {
local footer="$1"
if [ -z "$footer" ]; then
return 0 # Footer is optional
fi
# Check for BREAKING CHANGE format
if echo "$footer" | grep -qi "breaking change"; then
if ! echo "$footer" | grep -q "^BREAKING CHANGE:"; then
ERRORS+=("Use 'BREAKING CHANGE:' (uppercase, singular) not 'breaking change'")
SCORE=$((SCORE - 15))
fi
fi
# Check for issue references
if echo "$footer" | grep -qiP '\b(close|fix|resolve)[sd]?\b'; then
# Check format
if ! echo "$footer" | grep -qP '^(Closes|Fixes|Resolves|Refs) #[0-9]'; then
WARNINGS+=("Issue references should use proper format: 'Closes #123'")
SUGGESTIONS+=("Capitalize keyword and use # prefix for issue numbers")
SCORE=$((SCORE - 5))
fi
fi
return 0
}
# Function to check overall structure
validate_structure() {
local message="$1"
# Count lines
local line_count=$(echo "$message" | wc -l)
# Split message into parts
local subject=$(echo "$message" | head -1)
local rest=$(echo "$message" | tail -n +2)
# Validate subject
validate_subject "$subject"
# If multi-line, check for blank line after subject
if [ "$line_count" -gt 1 ]; then
local second_line=$(echo "$message" | sed -n '2p')
if [ -n "$second_line" ]; then
ERRORS+=("Blank line required between subject and body")
SCORE=$((SCORE - 10))
fi
# Extract body (after blank line, before footer)
local body=""
local footer=""
local in_footer=false
while IFS= read -r line; do
# Check if line is footer token
if echo "$line" | grep -qP '^(BREAKING CHANGE:|Closes|Fixes|Resolves|Refs|Reviewed-by|Signed-off-by)'; then
in_footer=true
fi
if [ "$in_footer" = true ]; then
footer="${footer}${line}\n"
else
body="${body}${line}\n"
fi
done <<< "$rest"
# Remove leading blank line from body
body=$(echo -e "$body" | sed '1{/^$/d;}')
# Validate body and footer
validate_body "$body"
validate_footer "$footer"
fi
}
# Main validation logic
main() {
echo "COMMIT MESSAGE VALIDATION"
echo "═══════════════════════════════════════════════"
echo ""
echo "MESSAGE:"
echo "───────────────────────────────────────────────"
echo "$MESSAGE"
echo ""
# Perform validation
validate_structure "$MESSAGE"
# Calculate final status
local status="VALID"
if [ "${#ERRORS[@]}" -gt 0 ]; then
status="INVALID"
elif [ "$STRICT_MODE" = "true" ] && [ "${#WARNINGS[@]}" -gt 0 ]; then
status="INVALID"
fi
# Display results
echo "VALIDATION RESULTS:"
echo "───────────────────────────────────────────────"
if [ "${#ERRORS[@]}" -gt 0 ]; then
echo "✗ ERRORS:"
for error in "${ERRORS[@]}"; do
echo " - $error"
done
echo ""
fi
if [ "${#WARNINGS[@]}" -gt 0 ]; then
echo "⚠ WARNINGS:"
for warning in "${WARNINGS[@]}"; do
echo " - $warning"
done
echo ""
fi
if [ "${#SUGGESTIONS[@]}" -gt 0 ]; then
echo "💡 SUGGESTIONS:"
for suggestion in "${SUGGESTIONS[@]}"; do
echo " - $suggestion"
done
echo ""
fi
if [ "${#ERRORS[@]}" -eq 0 ] && [ "${#WARNINGS[@]}" -eq 0 ]; then
echo "✓ All checks passed"
echo ""
fi
echo "STATUS: $status"
echo "SCORE: $SCORE/100"
echo "STRICT MODE: $STRICT_MODE"
echo ""
echo "═══════════════════════════════════════════════"
# Exit based on status
if [ "$status" = "INVALID" ]; then
exit 1
else
exit 0
fi
}
# Run main
main