Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions scripts/git-hooks/post-commit
Original file line number Diff line number Diff line change
@@ -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"
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sync file is created in the working directory but never committed or pushed. This could lead to untracked files accumulating in the repository. Consider using a different location outside the git repository or adding this to .gitignore.

Copilot uses AI. Check for mistakes.
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
147 changes: 147 additions & 0 deletions scripts/git-hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -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 <file> 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=""
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DUPLICATES variable is set within a while loop subshell and won't be accessible outside the loop. This means the duplicate detection check at line 106 will always pass even when duplicates are found.

Suggested change
DUPLICATES=""
DUPLICATES=()

Copilot uses AI. Check for mistakes.
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) " | \
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicate type check may fail when processing new files that don't exist in the current commit. Consider using git diff --cached to get staged content instead of git show ":$file".

Suggested change
git show ":$file" 2>/dev/null | grep -E "^public (class|struct|enum|protocol|actor) " | \
git diff --cached -- "$file" 2>/dev/null | grep -E "^public (class|struct|enum|protocol|actor) " | \

Copilot uses AI. Check for mistakes.
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:")
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The grep pattern uses ^$file: but git grep -l returns only filenames, not filename: format. This grep filter will never match and won't properly exclude the current file from duplicate detection.

Suggested change
existing=$(git grep -l "^public \(class\|struct\|enum\|protocol\|actor\) $type_name" -- '*.swift' | grep -v "^$file:")
existing=$(git grep -l "^public \(class\|struct\|enum\|protocol\|actor\) $type_name" -- '*.swift' | awk -v current_file="$file" '$0 != current_file')

Copilot uses AI. Check for mistakes.
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
77 changes: 77 additions & 0 deletions scripts/install-hooks.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"
Loading