#!/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 .+$' # Set text colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color INVALID_COMMITS=0 # 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 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 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}') # 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:${NC} $COMMIT_SUBJECT" continue fi if ! [[ "$COMMIT_SUBJECT" =~ $PATTERN ]]; then echo -e "${RED}✗ Invalid commit message:${NC} $COMMIT_SUBJECT" echo -e "${RED} Commit:${NC} $COMMIT" echo -e "${YELLOW} Message should follow semantic commit format:${NC}" echo -e " type(scope): message" echo -e " ${YELLOW}where type is one of:${NC} feat, fix, docs, style, refactor, test, perf, build, ci, chore, revert" echo "" INVALID_COMMITS=$((INVALID_COMMITS + 1)) else echo -e "${GREEN}✓ Valid commit:${NC} $COMMIT_SUBJECT" fi done 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 "Examples:" 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