#!/bin/bash # Semantic commit pattern - fixed to properly handle hyphens in scope # Moved the hyphen to the end of the character class to avoid interpretation issues 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 .+$' # Set text colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # No Color # Statistics tracking INVALID_COMMITS=0 VALID_COMMITS=0 MERGE_COMMITS=0 TOTAL_COMMITS=0 # Track commit types for summary declare -A COMMIT_TYPES=( ["feat"]=0 ["fix"]=0 ["docs"]=0 ["style"]=0 ["refactor"]=0 ["test"]=0 ["perf"]=0 ["build"]=0 ["ci"]=0 ["chore"]=0 ["revert"]=0 ) # Track authors declare -A AUTHORS=() # Track files changed TOTAL_FILES_CHANGED=0 TOTAL_INSERTIONS=0 TOTAL_DELETIONS=0 # Function to draw a horizontal bar draw_bar() { local value=$1 local max=$2 local width=30 local bar_width=$((value * width / max)) # Ensure minimum width of 1 if value > 0 if [ "$value" -gt 0 ] && [ "$bar_width" -eq 0 ]; then bar_width=1 fi printf "[%${bar_width}s%$((width - bar_width))s] %d\n" | tr ' ' '#' | tr ' ' '.' } # Function to extract commit type get_commit_type() { local subject="$1" echo "$subject" | grep -oP '^(feat|fix|docs|style|refactor|test|perf|build|ci|chore|revert)' || echo "unknown" } # ASCII box drawing for commit stats draw_box() { local title="$1" local width=60 local padding=$(((width - ${#title}) / 2)) echo -e "${BOLD}┌$(printf '─%.0s' $(seq 1 $width))┐${NC}" echo -e "${BOLD}│$(printf ' %.0s' $(seq 1 $padding))$title$(printf ' %.0s' $(seq 1 $((width - padding - ${#title}))))│${NC}" echo -e "${BOLD}└$(printf '─%.0s' $(seq 1 $width))┘${NC}" } # Read stdin (format: ) while read -r OLD_REV NEW_REV REF_NAME; do # Skip if it's a branch deletion if [[ "$NEW_REV" = "0000000000000000000000000000000000000000" ]]; then continue fi # Skip if it's a new branch (no old revision) if [[ "$OLD_REV" = "0000000000000000000000000000000000000000" ]]; then OLD_REV=$(git rev-list --max-parents=0 "$NEW_REV") fi echo -e "${YELLOW}Checking commits in $REF_NAME${NC}" # Get branch stats from git BRANCH_COMMITS=$(git rev-list --count "$REF_NAME") BRANCH_AGE=$(git log -1 --format="%cr" "$REF_NAME") echo -e "${CYAN}→ Branch has $BRANCH_COMMITS total commits (created $BRANCH_AGE)${NC}" echo "" # Get all commit hashes between old and new revision COMMITS=$(git rev-list "$OLD_REV".."$NEW_REV") # 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) PARENT_COUNT=$(git rev-list --parents -n 1 "$COMMIT" | awk '{print NF - 1}') AUTHOR=$(git log -1 --format="%an" "$COMMIT") COMMIT_DATE=$(git log -1 --format="%cd" --date=relative "$COMMIT") SHORT_HASH=${COMMIT:0:8} # Get commit stats STATS=$(git show --stat --format="" "$COMMIT") FILES_CHANGED=$(echo "$STATS" | grep -c "^[ ]*") INSERTIONS=$(echo "$STATS" | grep -o '[0-9]* insertion' | cut -d' ' -f1 | awk '{sum+=$1} END {print sum+0}') DELETIONS=$(echo "$STATS" | grep -o '[0-9]* deletion' | cut -d' ' -f1 | awk '{sum+=$1} END {print sum+0}') # Update stats TOTAL_FILES_CHANGED=$((TOTAL_FILES_CHANGED + FILES_CHANGED)) TOTAL_INSERTIONS=$((TOTAL_INSERTIONS + INSERTIONS)) TOTAL_DELETIONS=$((TOTAL_DELETIONS + DELETIONS)) # Update author stats if [[ -n "${AUTHORS[$AUTHOR]}" ]]; then AUTHORS[$AUTHOR]=$((AUTHORS[$AUTHOR] + 1)) else AUTHORS[$AUTHOR]=1 fi echo -e "${MAGENTA}Commit #$TOTAL_COMMITS:${NC} ${BOLD}$SHORT_HASH${NC} (by $AUTHOR, $COMMIT_DATE)" echo -e " Changed: ${CYAN}$FILES_CHANGED files${NC} ${GREEN}+$INSERTIONS${NC}/${RED}-$DELETIONS${NC} lines" echo " $COMMIT_SUBJECT" # Skip validation for merge commits (both PR merges and standard Git merges) if [[ $PARENT_COUNT -gt 1 ]] && ([[ "$COMMIT_SUBJECT" =~ $PR_MERGE_PATTERN ]] || [[ "$COMMIT_SUBJECT" =~ $GIT_MERGE_PATTERN ]]); then echo -e " ${BLUE}➜ Skipping merge commit validation${NC}" MERGE_COMMITS=$((MERGE_COMMITS + 1)) continue fi if ! [[ "$COMMIT_SUBJECT" =~ $PATTERN ]]; then echo -e " ${RED}✗ Invalid commit message${NC}" echo -e " ${YELLOW} Should follow pattern:${NC} type(scope): message" INVALID_COMMITS=$((INVALID_COMMITS + 1)) else echo -e " ${GREEN}✓ Valid semantic commit${NC}" VALID_COMMITS=$((VALID_COMMITS + 1)) # Count commit type COMMIT_TYPE=$(get_commit_type "$COMMIT_SUBJECT") if [[ -n "${COMMIT_TYPES[$COMMIT_TYPE]}" ]]; then COMMIT_TYPES[$COMMIT_TYPE]=$((COMMIT_TYPES[$COMMIT_TYPE] + 1)) fi fi echo "" done # Add push summary draw_box "COMMIT STATISTICS" echo -e "${BOLD}Total commits in this push:${NC} $TOTAL_COMMITS" echo -e "${GREEN}Valid semantic commits: ${NC} $VALID_COMMITS" echo -e "${BLUE}Merge commits: ${NC} $MERGE_COMMITS" echo -e "${RED}Invalid commits: ${NC} $INVALID_COMMITS" echo "" if [ $TOTAL_COMMITS -gt 0 ]; then VALID_PERCENT=$(((VALID_COMMITS + MERGE_COMMITS) * 100 / TOTAL_COMMITS)) echo -e "${BOLD}Semantic compliance:${NC} $VALID_PERCENT%" # Print bar printf "[" for ((i = 0; i < VALID_PERCENT / 5; i++)); do printf "${GREEN}#${NC}" done for ((i = VALID_PERCENT / 5; i < 20; i++)); do printf "${RED}.${NC}" done printf "] $VALID_PERCENT%%\n" echo "" fi # Commit type distribution if we have valid commits if [ $VALID_COMMITS -gt 0 ]; then draw_box "COMMIT TYPE DISTRIBUTION" for type in "${!COMMIT_TYPES[@]}"; do count=${COMMIT_TYPES[$type]} if [ $count -gt 0 ]; then # Calculate percentage percent=$((count * 100 / VALID_COMMITS)) printf "%-10s " "$type:" draw_bar $count $VALID_COMMITS echo " $count ($percent%)" fi done echo "" fi # Author distribution if [ ${#AUTHORS[@]} -gt 0 ]; then draw_box "AUTHOR DISTRIBUTION" for author in "${!AUTHORS[@]}"; do count=${AUTHORS[$author]} percent=$((count * 100 / TOTAL_COMMITS)) printf "%-20s " "$author:" draw_bar $count $TOTAL_COMMITS echo " $count ($percent%)" done echo "" fi # File stats draw_box "CHANGE STATISTICS" echo -e "Files changed: $TOTAL_FILES_CHANGED" echo -e "Lines added: ${GREEN}+$TOTAL_INSERTIONS${NC}" echo -e "Lines removed: ${RED}-$TOTAL_DELETIONS${NC}" echo -e "Net change: $((TOTAL_INSERTIONS - TOTAL_DELETIONS)) lines" echo "" done # If any commits were invalid, reject the push if [[ $INVALID_COMMITS -gt 0 ]]; then echo -e "${RED}Push rejected: $INVALID_COMMITS commit(s) have invalid messages.${NC}" echo -e "${YELLOW}Please rewrite your commit messages to follow the semantic commit format:${NC}" echo "type(scope): message" echo -e "\nExamples:" echo " feat(auth): add login with Google" echo " fix: correct calculation in billing module" echo " style(release.yml): format permissions section in YAML" exit 1 fi exit 0