#!/bin/bash # Semantic commit pattern: # feat|fix|docs|style|refactor|test|perf|build|ci|chore|revert(optional scope): message # Allow dots (.), uppercase letters, and other common filename characters in scope PATTERN='^(feat|fix|docs|style|refactor|test|perf|build|ci|chore|revert)(\([a-zA-Z0-9\-\.\_]+\))?: .{1,}$' # Pattern for merge commits from pull requests - simplified to catch all formats PR_MERGE_PATTERN='^Merge pull request' # Pattern for standard Git merge commits GIT_MERGE_PATTERN='^Merge (branch|remote-tracking branch) .+ into .+$' # Counters for reporting TOTAL_COMMITS=0 VALID_COMMITS=0 INVALID_COMMITS=0 MERGE_COMMITS=0 # Check if colors should be used USE_COLORS=1 if [ "$1" = "--no-color" ] || [ "$1" = "-n" ]; then USE_COLORS=0 fi # Define color functions that check if colors should be used color() { if [ $USE_COLORS -eq 1 ] && [ -t 1 ]; then case "$1" in "red") echo -en "\033[0;31m" ;; "green") echo -en "\033[0;32m" ;; "yellow") echo -en "\033[0;33m" ;; "blue") echo -en "\033[0;34m" ;; "reset") echo -en "\033[0m" ;; esac fi } # Function to check if current directory is a Git repository check_git_repo() { if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then color "red" echo "Error: This is not a Git repository" color "reset" exit 1 fi } # Ask for commit range or use default get_commit_range() { color "yellow" echo "Enter commit range to check (default: all commits):" color "reset" echo "Examples:" echo " - HEAD~10..HEAD (last 10 commits)" echo " - main..feature (commits in feature branch not in main)" echo " - Press Enter for all commits" read -r RANGE if [ -z "$RANGE" ]; then RANGE="--all" fi color "yellow" echo "Checking commits in range: " color "reset" echo "$RANGE" } # Initialize check_git_repo get_commit_range # Get all commit hashes if [ "$RANGE" = "--all" ]; then COMMITS=$(git log --format="%H") else COMMITS=$(git log "$RANGE" --format="%H") fi # Check each commit message for COMMIT in $COMMITS; do TOTAL_COMMITS=$((TOTAL_COMMITS + 1)) COMMIT_MSG=$(git log --format=%B -n 1 "$COMMIT") COMMIT_SUBJECT=$(echo "$COMMIT_MSG" | head -n 1) COMMIT_DATE=$(git show -s --format=%ci "$COMMIT") COMMIT_AUTHOR=$(git show -s --format="%an <%ae>" "$COMMIT") PARENT_COUNT=$(git rev-list --parents -n 1 "$COMMIT" | awk '{print NF - 1}') echo color "yellow" echo -n "Commit: " color "reset" echo "${COMMIT:0:8} - $COMMIT_DATE - $COMMIT_AUTHOR" echo " $COMMIT_SUBJECT" # Check if it's a merge commit if [ $PARENT_COUNT -gt 1 ] && (echo "$COMMIT_SUBJECT" | grep -E "$PR_MERGE_PATTERN" >/dev/null || echo "$COMMIT_SUBJECT" | grep -E "$GIT_MERGE_PATTERN" >/dev/null); then color "blue" echo -n " ➜ Merge commit (exempt from validation)" color "reset" echo MERGE_COMMITS=$((MERGE_COMMITS + 1)) continue fi # Validate against semantic pattern if echo "$COMMIT_SUBJECT" | grep -E "$PATTERN" >/dev/null; then color "green" echo -n " ✓ Valid semantic commit" color "reset" echo VALID_COMMITS=$((VALID_COMMITS + 1)) else color "red" echo -n " ✗ Invalid commit message" color "reset" echo color "yellow" echo -n " Should follow pattern:" color "reset" echo " type(scope): message" INVALID_COMMITS=$((INVALID_COMMITS + 1)) fi done # Print summary echo color "yellow" echo "========== Validation Summary ==========" color "reset" echo "Total commits checked: ${TOTAL_COMMITS}" color "green" echo -n "Valid semantic commits: ${VALID_COMMITS}" color "reset" echo color "blue" echo -n "Merge commits (exempted): ${MERGE_COMMITS}" color "reset" echo color "red" echo -n "Invalid semantic commits: ${INVALID_COMMITS}" color "reset" echo # Print percentage if [ $TOTAL_COMMITS -gt 0 ]; then VALID_PERCENT=$((($VALID_COMMITS + $MERGE_COMMITS) * 100 / $TOTAL_COMMITS)) echo "Compliance rate: ${VALID_PERCENT}%" if [ $INVALID_COMMITS -gt 0 ]; then echo color "yellow" echo "Recommendations:" color "reset" echo "- Review invalid commits and consider amending them for consistency" echo "- Run this script periodically to ensure commit message standards" else echo color "green" echo "All commits comply with semantic commit standards!" color "reset" fi fi exit 0