Initial commit
This commit is contained in:
335
commands/message-generation/.scripts/message-validator.sh
Executable file
335
commands/message-generation/.scripts/message-validator.sh
Executable file
@@ -0,0 +1,335 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user