From 54add127b6bd2c9c431536c7a8d7acc3b84ff0c4 Mon Sep 17 00:00:00 2001 From: drunkonjava Date: Thu, 31 Jul 2025 18:24:37 -0400 Subject: [PATCH] feat(hooks): Add Git hooks installation system Add system for managing and installing Git hooks: - Pre-commit hook with updated limits (30 files, 800 lines) - Post-commit auto-push hook for single source of truth - Install script for easy setup - Hooks stored in version control for consistency Changes to pre-commit hook: - Updated documentation to mention hybrid workflow - Added branch awareness (shows hint for feature branches) - Maintained all existing checks (size, protected files, duplicates) This ensures all developers use the same hooks and can easily update them when the workflow evolves. Part 5 of the hybrid workflow implementation. Co-authored-by: Claude --- scripts/git-hooks/post-commit | 52 ++++++++++++ scripts/git-hooks/pre-commit | 147 ++++++++++++++++++++++++++++++++++ scripts/install-hooks.sh | 77 ++++++++++++++++++ 3 files changed, 276 insertions(+) create mode 100755 scripts/git-hooks/post-commit create mode 100755 scripts/git-hooks/pre-commit create mode 100755 scripts/install-hooks.sh diff --git a/scripts/git-hooks/post-commit b/scripts/git-hooks/post-commit new file mode 100755 index 00000000..e921cdb6 --- /dev/null +++ b/scripts/git-hooks/post-commit @@ -0,0 +1,52 @@ +#!/bin/bash +# Post-commit hook to automatically push to remote and handle Git LFS +# This ensures local changes are immediately reflected on GitHub + +# First, run Git LFS post-commit if available +command -v git-lfs >/dev/null 2>&1 && git lfs post-commit "$@" + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if auto-push is disabled via environment variable +if [ "$DISABLE_AUTO_PUSH" = "true" ]; then + exit 0 +fi + +echo -e "${YELLOW}🔄 Auto-pushing to remote (Single Source of Truth)${NC}" + +# Get current branch +BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null) +if [ -z "$BRANCH" ]; then + echo -e "${RED}❌ Not on a branch, skipping auto-push${NC}" + exit 0 +fi + +# Check if we have a remote +if ! git remote | grep -q origin; then + echo -e "${RED}❌ No remote 'origin' found, skipping auto-push${NC}" + exit 0 +fi + +# Push to remote with force-with-lease (safer than force) +if git push origin "$BRANCH" --force-with-lease 2>&1; then + echo -e "${GREEN}✅ Successfully pushed $BRANCH to origin${NC}" + + # Update sync timestamp + SYNC_FILE=".github/sync-status/local-push.json" + mkdir -p "$(dirname "$SYNC_FILE")" + cat > "$SYNC_FILE" << EOF +{ + "timestamp": "$(date -u +'%Y-%m-%d %H:%M:%S UTC')", + "branch": "$BRANCH", + "commit": "$(git rev-parse HEAD)", + "message": "$(git log -1 --pretty=%B)" +} +EOF +else + echo -e "${RED}❌ Failed to push to origin${NC}" + echo -e "${YELLOW}You may need to manually push with: git push origin $BRANCH --force${NC}" +fi \ No newline at end of file diff --git a/scripts/git-hooks/pre-commit b/scripts/git-hooks/pre-commit new file mode 100755 index 00000000..3bc99238 --- /dev/null +++ b/scripts/git-hooks/pre-commit @@ -0,0 +1,147 @@ +#!/bin/bash +# Pre-commit hook to enforce Claude Code standards and check for duplicate types +# Part of the hybrid workflow - enforces commit size limits and code quality + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Always check Claude standards +echo -e "${BLUE}🤖 Checking Claude Code commit standards...${NC}" + + # 1. Check commit size + MAX_FILES=30 + MAX_LINES=800 + + # Get current branch + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + + # Count staged files + STAGED_FILES=$(git diff --cached --name-only | wc -l) + STAGED_LINES=$(git diff --cached --numstat | awk '{sum+=$1+$2} END {print sum}') + + if [ $STAGED_FILES -gt $MAX_FILES ]; then + echo -e "${RED}❌ Too many files in commit: $STAGED_FILES (max: $MAX_FILES)${NC}" + echo -e "${YELLOW}💡 Break this into smaller, incremental commits${NC}" + echo -e "${YELLOW} Run: git reset HEAD to unstage some files${NC}" + exit 1 + fi + + if [ ${STAGED_LINES:-0} -gt $MAX_LINES ]; then + echo -e "${RED}❌ Too many lines changed: $STAGED_LINES (max: $MAX_LINES)${NC}" + echo -e "${YELLOW}💡 Make smaller, more focused changes${NC}" + echo -e "${YELLOW} Consider splitting this into multiple commits${NC}" + exit 1 + fi + + # 2. Check for protected files + PROTECTED_PATTERNS=( + ".github/workflows/claude-commit-enforcement.yml" + ".github/CODEOWNERS" + "Package.resolved" + "project.pbxproj" + ) + + for pattern in "${PROTECTED_PATTERNS[@]}"; do + if git diff --cached --name-only | grep -q "$pattern"; then + echo -e "${RED}❌ Attempting to modify protected file: $pattern${NC}" + echo -e "${YELLOW}💡 These files should only be modified via PR review${NC}" + echo -e "${YELLOW} Create a PR to modify protected files${NC}" + exit 1 + fi + done + + # 3. Check for tests when adding code + CODE_FILES=$(git diff --cached --name-only | grep -E '\.(swift|js|ts|py)$' | grep -v -E '(Test|Spec|test|spec)\.') + if [ -n "$CODE_FILES" ]; then + TEST_FILES=$(git diff --cached --name-only | grep -E '(Test|Spec|test|spec)\.') + if [ -z "$TEST_FILES" ]; then + echo -e "${YELLOW}âš ī¸ Adding code without tests${NC}" + echo -e "${YELLOW}💡 Consider adding tests in the same commit${NC}" + fi + fi + + # 4. Check time since last commit + LAST_COMMIT_TIME=$(git log -1 --format=%at 2>/dev/null || echo 0) + CURRENT_TIME=$(date +%s) + TIME_DIFF=$((CURRENT_TIME - LAST_COMMIT_TIME)) + THIRTY_MINUTES=1800 + + if [ $LAST_COMMIT_TIME -ne 0 ] && [ $TIME_DIFF -gt $THIRTY_MINUTES ]; then + MINUTES=$((TIME_DIFF / 60)) + echo -e "${YELLOW}âš ī¸ It's been $MINUTES minutes since your last commit${NC}" + echo -e "${YELLOW}💡 Remember to commit frequently (every ~30 minutes)${NC}" + fi + + echo -e "${GREEN}✅ Claude standards check passed${NC}" + +# Original duplicate type check +echo "Running duplicate type check..." + +# Get staged Swift files +STAGED_SWIFT_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.swift$') + +if [ -n "$STAGED_SWIFT_FILES" ]; then + # Check for new duplicate types in staged files + DUPLICATES="" + for file in $STAGED_SWIFT_FILES; do + # Extract public type names from staged content + git show ":$file" 2>/dev/null | grep -E "^public (class|struct|enum|protocol|actor) " | \ + sed -E 's/^public (class|struct|enum|protocol|actor) ([A-Za-z0-9_]+).*/\2/' | \ + while read -r type_name; do + # Check if this type already exists in other files + existing=$(git grep -l "^public \(class\|struct\|enum\|protocol\|actor\) $type_name" -- '*.swift' | grep -v "^$file:") + if [ -n "$existing" ]; then + echo -e "${RED}ERROR: Duplicate type '$type_name' found!${NC}" + echo " Defined in staged file: $file" + echo " Already exists in: $(echo "$existing" | head -1 | cut -d: -f1)" + DUPLICATES="$DUPLICATES$type_name " + fi + done + done + + if [ -n "$DUPLICATES" ]; then + echo "" + echo -e "${RED}Commit blocked: Duplicate type names detected${NC}" + echo "" + echo "To fix this issue:" + echo "1. Add a module prefix to your type names" + echo "2. Or make the type internal if it doesn't need to be public" + echo "3. Or move the type to a Foundation module if it's truly shared" + echo "" + echo "See docs/SPM_PRODUCT_NAMES.md for naming conventions" + exit 1 + fi +fi + +# Run quick duplicate check on entire codebase +if command -v ./scripts/check-duplicate-types-fast.sh >/dev/null 2>&1; then + echo "Running full duplicate type scan..." + if ! ./scripts/check-duplicate-types-fast.sh >/dev/null 2>&1; then + echo "" + echo -e "${YELLOW}Warning: Existing duplicate types detected in codebase${NC}" + echo "Run './scripts/check-duplicate-types-fast.sh' for details" + echo "" + fi +fi + +echo -e "${GREEN}✓ No new duplicate types introduced${NC}" + +# Suggest commit message format +echo -e "${BLUE}📝 Remember to use conventional commit format:${NC}" +echo -e " ${YELLOW}type(scope): description${NC}" +echo -e " Examples:" +echo -e " - feat(inventory): Add item search" +echo -e " - fix(ui): Resolve button alignment" +echo -e " - test(search): Add unit tests" + +# Show current branch info +if [[ "$CURRENT_BRANCH" != "main" ]] && [[ "$CURRENT_BRANCH" != "dev" ]]; then + echo -e "${BLUE}â„šī¸ Branch: ${YELLOW}$CURRENT_BRANCH${NC}" + echo -e " Target ${YELLOW}dev${NC} for most PRs, ${YELLOW}main${NC} for critical fixes" +fi + +exit 0 \ No newline at end of file diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100755 index 00000000..83c4cd57 --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# Install Git hooks for the hybrid workflow + +set -euo pipefail + +# Color codes +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Script directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" +HOOKS_DIR="$PROJECT_ROOT/.git/hooks" +TEMPLATE_DIR="$PROJECT_ROOT/scripts/git-hooks" + +# Function to install a hook +install_hook() { + local hook_name=$1 + local source_file="$TEMPLATE_DIR/$hook_name" + local dest_file="$HOOKS_DIR/$hook_name" + + if [[ -f "$source_file" ]]; then + # Backup existing hook if it exists and is different + if [[ -f "$dest_file" ]] && ! diff -q "$source_file" "$dest_file" >/dev/null 2>&1; then + echo -e "${YELLOW}âš ī¸ Backing up existing $hook_name to $hook_name.backup${NC}" + cp "$dest_file" "$dest_file.backup" + fi + + # Copy new hook + cp "$source_file" "$dest_file" + chmod +x "$dest_file" + echo -e "${GREEN}✅ Installed $hook_name hook${NC}" + else + echo -e "${YELLOW}âš ī¸ No template found for $hook_name${NC}" + fi +} + +# Main installation +main() { + echo -e "${BLUE}🔧 Installing Git hooks for hybrid workflow${NC}" + echo "" + + # Check if we're in a git repository + if [[ ! -d "$HOOKS_DIR" ]]; then + echo -e "${RED}❌ Error: Not in a git repository${NC}" + echo "Run this script from the project root" + exit 1 + fi + + # Create template directory if it doesn't exist + mkdir -p "$TEMPLATE_DIR" + + # Install hooks + install_hook "pre-commit" + install_hook "post-commit" + install_hook "commit-msg" + + echo "" + echo -e "${GREEN}✨ Git hooks installed successfully!${NC}" + echo "" + echo -e "${BLUE}Hooks provide:${NC}" + echo " â€ĸ Commit size enforcement (30 files, 800 lines)" + echo " â€ĸ Conventional commit format checking" + echo " â€ĸ Duplicate type prevention" + echo " â€ĸ Auto-push to remote (post-commit)" + echo " â€ĸ Protected file warnings" + echo "" + echo -e "${YELLOW}💡 To bypass hooks temporarily:${NC}" + echo " git commit --no-verify" + echo "" +} + +# Run main +main "$@" \ No newline at end of file