commit 6ca6480c962cf9a7a981b34461680372364f5da2 Author: Jan Klattenhoff Date: Mon Apr 7 17:26:28 2025 +0200 feat: add commit message validation scripts diff --git a/hooks/pre-recieve.sh b/hooks/pre-recieve.sh new file mode 100644 index 0000000..4c5d717 --- /dev/null +++ b/hooks/pre-recieve.sh @@ -0,0 +1,78 @@ +#!/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 diff --git a/verify-commits.sh b/verify-commits.sh new file mode 100644 index 0000000..0306241 --- /dev/null +++ b/verify-commits.sh @@ -0,0 +1,169 @@ +#!/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