#!/bin/bash
# Validate commit messages follow Conventional Commits format
# Usage: ./scripts/validate_commits.sh [commit-ref]
set -e
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Valid commit types
VALID_TYPES="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert"
# Get commit to validate (default: HEAD)
COMMIT=${1:-HEAD}
echo "======================================================================="
echo "COMMIT MESSAGE VALIDATION"
echo "======================================================================="
echo ""
validate_commit_message() {
local commit_ref=$1
local commit_msg=$(git log --format=%B -n 1 "$commit_ref")
local first_line=$(echo "$commit_msg" | head -n 1)
echo "Validating: $commit_ref"
echo "Message: $first_line"
echo ""
local valid=true
# 1. Check format: type(scope): description or type: description
if ! echo "$first_line" | grep -qE "^($VALID_TYPES)(\(.+\))?: .+"; then
echo -e "${RED}✗ FAILED: Invalid format${NC}"
echo " Expected: <type>(<scope>): <description>"
echo " Or: <type>: <description>"
echo " Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert"
valid=false
else
echo -e "${GREEN}✓ Format is valid${NC}"
fi
# 2. Extract components
if echo "$first_line" | grep -qE "^($VALID_TYPES)\(.+\): .+"; then
type=$(echo "$first_line" | sed -E "s/^($VALID_TYPES)\(.+\): .+/\1/")
scope=$(echo "$first_line" | sed -E "s/^$VALID_TYPES\((.+)\): .+/\1/")
description=$(echo "$first_line" | sed -E "s/^$VALID_TYPES\(.+\): (.+)/\1/")
else
type=$(echo "$first_line" | sed -E "s/^($VALID_TYPES): .+/\1/")
scope=""
description=$(echo "$first_line" | sed -E "s/^$VALID_TYPES: (.+)/\1/")
fi
# 3. Check description length (max 72 chars from type to end)
local line_length=${#first_line}
if [ "$line_length" -gt 72 ]; then
echo -e "${RED}✗ FAILED: Subject too long${NC}"
echo " Length: $line_length (max 72)"
echo " Shorten description to fit within 72 characters"
valid=false
else
echo -e "${GREEN}✓ Subject length OK ($line_length/72)${NC}"
fi
# 4. Check description starts with lowercase
local first_char=$(echo "$description" | cut -c1)
if ! echo "$first_char" | grep -q "^[a-z]"; then
echo -e "${YELLOW}⚠ WARNING: Description should start with lowercase${NC}"
echo " Current: '$description'"
echo " Suggested: '$(echo "$first_char" | tr '[:upper:]' '[:lower:]')$(echo "$description" | cut -c2-)'"
else
echo -e "${GREEN}✓ Description starts with lowercase${NC}"
fi
# 5. Check description doesn't end with period
if echo "$description" | grep -q '\.$'; then
echo -e "${YELLOW}⚠ WARNING: Description should not end with period${NC}"
echo " Remove trailing period"
else
echo -e "${GREEN}✓ Description has no trailing period${NC}"
fi
# 6. Check for imperative mood (common mistakes)
if echo "$description" | grep -qE "^(added|adds|adding|fixed|fixes|fixing|updated|updates|updating)"; then
echo -e "${YELLOW}⚠ WARNING: Use imperative mood${NC}"
echo " Current: '$description'"
echo " Use: 'add', 'fix', 'update' (not 'added', 'adds', 'adding')"
else
echo -e "${GREEN}✓ Imperative mood${NC}"
fi
# 7. Check for breaking changes
if echo "$commit_msg" | grep -qi "BREAKING CHANGE:"; then
echo -e "${YELLOW}⚠ BREAKING CHANGE detected${NC}"
if ! echo "$first_line" | grep -q "!"; then
echo -e "${YELLOW} Consider adding ! to type: ${type}!${scope:+($scope)}: $description${NC}"
fi
fi
echo ""
if [ "$valid" = true ]; then
echo -e "${GREEN}✓✓✓ COMMIT MESSAGE VALID ✓✓✓${NC}"
return 0
else
echo -e "${RED}✗✗✗ COMMIT MESSAGE INVALID ✗✗✗${NC}"
echo ""
echo "Fix the message with:"
echo " git commit --amend"
return 1
fi
}
# If no argument provided, validate HEAD
if [ $# -eq 0 ]; then
echo "Validating HEAD commit..."
echo ""
validate_commit_message HEAD
exit $?
fi
# If argument is a range (e.g., main..HEAD), validate all commits in range
if echo "$COMMIT" | grep -q "\.\."; then
echo "Validating commit range: $COMMIT"
echo ""
all_valid=true
for commit in $(git rev-list "$COMMIT"); do
validate_commit_message "$commit"
echo "-----------------------------------------------------------------------"
echo ""
if [ $? -ne 0 ]; then
all_valid=false
fi
done
if [ "$all_valid" = true ]; then
exit 0
else
exit 1
fi
fi
# Validate single commit
validate_commit_message "$COMMIT"
exit $?