From 7dc765f55cbaeebaeb01d3e8adf320011e63a7a7 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Mon, 7 Apr 2025 19:03:31 +0200 Subject: [PATCH] feat(prereceive): add commit validation and statistics tracking --- hooks/pre-recieve.sh | 171 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 162 insertions(+), 9 deletions(-) diff --git a/hooks/pre-recieve.sh b/hooks/pre-recieve.sh index 4c5d717..26b15ce 100644 --- a/hooks/pre-recieve.sh +++ b/hooks/pre-recieve.sh @@ -16,9 +16,62 @@ 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 @@ -34,33 +87,133 @@ while read -r OLD_REV NEW_REV REF_NAME; do 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:${NC} $COMMIT_SUBJECT" + 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} $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 "" + 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 commit:${NC} $COMMIT_SUBJECT" + 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 @@ -68,7 +221,7 @@ 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 -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"