feat(prereceive): add commit validation and statistics tracking

This commit is contained in:
Jan K9f 2025-04-07 19:03:31 +02:00
parent c8359d1f75
commit 7dc765f55c
Signed by: jank
GPG key ID: B9F475106B20F144

View file

@ -16,9 +16,62 @@ RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[0;33m' YELLOW='\033[0;33m'
BLUE='\033[0;34m' BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Statistics tracking
INVALID_COMMITS=0 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: <old-value> <new-value> <ref-name>) # Read stdin (format: <old-value> <new-value> <ref-name>)
while read -r OLD_REV NEW_REV REF_NAME; do 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}" 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 # Get all commit hashes between old and new revision
COMMITS=$(git rev-list "$OLD_REV".."$NEW_REV") COMMITS=$(git rev-list "$OLD_REV".."$NEW_REV")
# Check each commit message # Check each commit message
for COMMIT in $COMMITS; do for COMMIT in $COMMITS; do
TOTAL_COMMITS=$((TOTAL_COMMITS + 1))
COMMIT_MSG=$(git log --format=%B -n 1 "$COMMIT") COMMIT_MSG=$(git log --format=%B -n 1 "$COMMIT")
COMMIT_SUBJECT=$(echo "$COMMIT_MSG" | head -n 1) COMMIT_SUBJECT=$(echo "$COMMIT_MSG" | head -n 1)
PARENT_COUNT=$(git rev-list --parents -n 1 "$COMMIT" | awk '{print NF - 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) # 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 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 continue
fi fi
if ! [[ "$COMMIT_SUBJECT" =~ $PATTERN ]]; then if ! [[ "$COMMIT_SUBJECT" =~ $PATTERN ]]; then
echo -e "${RED}✗ Invalid commit message:${NC} $COMMIT_SUBJECT" echo -e " ${RED}✗ Invalid commit message${NC}"
echo -e "${RED} Commit:${NC} $COMMIT" echo -e " ${YELLOW} Should follow pattern:${NC} type(scope): message"
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)) INVALID_COMMITS=$((INVALID_COMMITS + 1))
else 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 fi
echo ""
done 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 done
# If any commits were invalid, reject the push # 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 "${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 -e "${YELLOW}Please rewrite your commit messages to follow the semantic commit format:${NC}"
echo "type(scope): message" echo "type(scope): message"
echo "Examples:" echo -e "\nExamples:"
echo " feat(auth): add login with Google" echo " feat(auth): add login with Google"
echo " fix: correct calculation in billing module" echo " fix: correct calculation in billing module"
echo " style(release.yml): format permissions section in YAML" echo " style(release.yml): format permissions section in YAML"