|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +# Function to print error message and exit with failure status |
| 4 | +print_error() { |
| 5 | + echo "Error: $1" >&2 |
| 6 | + exit 1 |
| 7 | +} |
| 8 | + |
| 9 | +# Function to validate the commit message |
| 10 | +validate_commit_message() { |
| 11 | + local message="$(head -n1 $1)" |
| 12 | + |
| 13 | + echo "$message" |
| 14 | + |
| 15 | + # Regular expressions for different parts of the commit message |
| 16 | + local type_regex="^(feat|deps|fix|docs|style|refactor|perf|test|build|ci|chore|revert|poc)" |
| 17 | + local scope_regex="\([[:alnum:]-]+\)" |
| 18 | + local breaking_change="!" |
| 19 | + local description_regex=": .+" |
| 20 | + |
| 21 | + # Check if message is empty |
| 22 | + if [ -z "$message" ]; then |
| 23 | + print_error "Commit message cannot be empty" |
| 24 | + fi |
| 25 | + |
| 26 | + # Split message into lines |
| 27 | + IFS=$'\n' read -rd '' -a lines <<<"$message" |
| 28 | + local first_line="${lines[0]}" |
| 29 | + |
| 30 | + # Validate first line format |
| 31 | + if ! [[ "$first_line" =~ $type_regex ]]; then |
| 32 | + print_error "Commit message must start with a valid type (feat, fix, docs, etc.)" |
| 33 | + fi |
| 34 | + |
| 35 | + # Extract the prefix (everything before the colon) |
| 36 | + local prefix="${first_line%%:*}" |
| 37 | + |
| 38 | + # Check if scope is present and valid (if included) |
| 39 | + if [[ "$prefix" =~ \( ]]; then |
| 40 | + if ! [[ "$prefix" =~ $type_regex$scope_regex(!)?$ ]]; then |
| 41 | + print_error "Invalid scope format in commit message" |
| 42 | + fi |
| 43 | + fi |
| 44 | + |
| 45 | + # Check for breaking change indicator |
| 46 | + if [[ "$prefix" =~ \!$ ]] && ! [[ "$prefix" =~ $type_regex($scope_regex)?\!$ ]]; then |
| 47 | + print_error "Invalid breaking change format in commit message" |
| 48 | + fi |
| 49 | + |
| 50 | + # Validate description presence |
| 51 | + if ! [[ "$first_line" =~ $description_regex ]]; then |
| 52 | + print_error "Commit message must have a description after the type/scope" |
| 53 | + fi |
| 54 | + |
| 55 | + # Validate body format if present |
| 56 | + if [ ${#lines[@]} -gt 1 ]; then |
| 57 | + # Check if there's a blank line between subject and body |
| 58 | + if [ -n "${lines[1]}" ]; then |
| 59 | + print_error "There must be a blank line between subject and body" |
| 60 | + fi |
| 61 | + |
| 62 | + # Validate footer format if present |
| 63 | + for ((i=2; i<${#lines[@]}; i++)); do |
| 64 | + local line="${lines[i]}" |
| 65 | + # Skip empty lines |
| 66 | + [ -z "$line" ] && continue |
| 67 | + |
| 68 | + # Check for footer format (if it looks like a footer) |
| 69 | + if [[ "$line" =~ ^[A-Z][A-Z0-9-]*:[[:space:]] ]] || [[ "$line" =~ ^BREAKING[[:space:]]CHANGE:[[:space:]] ]]; then |
| 70 | + # Valid footer format found, continue to next line |
| 71 | + continue |
| 72 | + fi |
| 73 | + |
| 74 | + # If line starts with what looks like a footer token but doesn't match the format, it's invalid |
| 75 | + if [[ "$line" =~ ^[A-Z][A-Z0-9-]*[^:].*$ ]] || [[ "$line" =~ ^BREAKING[[:space:]]CHANGE[^:].*$ ]]; then |
| 76 | + print_error "Invalid footer format: $line" |
| 77 | + fi |
| 78 | + done |
| 79 | + fi |
| 80 | + |
| 81 | + echo "Commit message is valid!" |
| 82 | + exit 0 |
| 83 | +} |
| 84 | + |
| 85 | +# Main execution |
| 86 | +if [ -z "$1" ]; then |
| 87 | + # If no argument provided, try to read from .git/COMMIT_EDITMSG |
| 88 | + if [ -f .git/COMMIT_EDITMSG ]; then |
| 89 | + commit_msg=$(cat .git/COMMIT_EDITMSG) |
| 90 | + else |
| 91 | + print_error "No commit message provided and .git/COMMIT_EDITMSG not found" |
| 92 | + fi |
| 93 | +else |
| 94 | + commit_msg="$1" |
| 95 | +fi |
| 96 | + |
| 97 | +validate_commit_message "$commit_msg" |
0 commit comments