Skip to content

Git Hooks

Git hook system for automatic code indexing, maintaining synchronized knowledge base across projects.

Overview

Git hooks automatically trigger indexing when code changes:

  • post-commit: Index newly committed changes
  • post-merge: Index merged changes from pull/merge
  • post-checkout: Handle branch switches

Hook Installation

Automatic Installation

Hooks are automatically installed when:

bash
# Creating new project with Wikit
wdg create my-site --init-wikit

# Adding repository to project
wdg my-site repo add https://github.com/client/repo

# Connecting central repository
wdg repo connect my-site client-repo

Manual Installation

bash
# Copy hooks to repository
cp hooks/* /path/to/repository/.git/hooks/

# Make executable
chmod +x /path/to/repository/.git/hooks/*

Hook Implementations

post-commit Hook

Indexes files changed in the latest commit:

bash
#!/bin/bash
# .git/hooks/post-commit

# Get repository info
REPO_PATH=$(git rev-parse --show-toplevel)
REPO_NAME=$(basename "$REPO_PATH")
PROJECT_NAME=$(basename $(dirname $(dirname "$REPO_PATH")))

# Get changed files
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)

# Filter indexable files
INDEXABLE=$(echo "$CHANGED_FILES" | grep -E '\.(php|js|jsx|scss|css|json)$')

if [ -z "$INDEXABLE" ]; then
    echo "No indexable files changed"
    exit 0
fi

# Get commit info
COMMIT_HASH=$(git rev-parse HEAD)
COMMIT_MSG=$(git log -1 --pretty=%B)
AUTHOR=$(git log -1 --pretty=%an)

echo "Indexing ${PROJECT_NAME}/${REPO_NAME}..."
echo "Commit: ${COMMIT_HASH:0:7} - $COMMIT_MSG"
echo "Files: $(echo "$INDEXABLE" | wc -l)"

# Trigger indexing
python3 ${WDG_HOME}/indexer/index_changes.py \
    --project "$PROJECT_NAME" \
    --repository "$REPO_NAME" \
    --files "$INDEXABLE" \
    --commit "$COMMIT_HASH" \
    --author "$AUTHOR"

echo "✓ Indexing complete"

post-merge Hook

Indexes changes after git pull/merge:

bash
#!/bin/bash
# .git/hooks/post-merge

# Get merge info
SQUASH_MERGE=$1  # 1 if squash merge, 0 otherwise
MERGE_HEAD=$(cat .git/ORIG_HEAD 2>/dev/null || echo "HEAD^")

# Get changed files
CHANGED_FILES=$(git diff --name-only $MERGE_HEAD HEAD)

# Filter indexable files
INDEXABLE=$(echo "$CHANGED_FILES" | grep -E '\.(php|js|jsx|scss|css|json)$')

if [ -z "$INDEXABLE" ]; then
    echo "No indexable files changed in merge"
    exit 0
fi

echo "Post-merge indexing..."
echo "Files changed: $(echo "$INDEXABLE" | wc -l)"

# Trigger indexing
python3 ${WDG_HOME}/indexer/index_changes.py \
    --project "$PROJECT_NAME" \
    --repository "$REPO_NAME" \
    --files "$INDEXABLE" \
    --merge-head "$MERGE_HEAD"

echo "✓ Merge indexing complete"

post-checkout Hook

Handles branch switches and updates:

bash
#!/bin/bash
# .git/hooks/post-checkout

PREV_HEAD=$1
NEW_HEAD=$2
BRANCH_CHECKOUT=$3  # 1 if branch checkout, 0 if file checkout

# Only run on branch checkout
if [ "$BRANCH_CHECKOUT" != "1" ]; then
    exit 0
fi

# Get changed files between branches
CHANGED_FILES=$(git diff --name-only $PREV_HEAD $NEW_HEAD)

# Filter indexable files
INDEXABLE=$(echo "$CHANGED_FILES" | grep -E '\.(php|js|jsx|scss|css|json)$')

if [ -z "$INDEXABLE" ]; then
    exit 0
fi

echo "Branch switch detected, updating index..."

# Trigger indexing
python3 ${WDG_HOME}/indexer/index_changes.py \
    --project "$PROJECT_NAME" \
    --repository "$REPO_NAME" \
    --files "$INDEXABLE" \
    --prev-head "$PREV_HEAD" \
    --new-head "$NEW_HEAD"

echo "✓ Index updated for new branch"

Hook Configuration

Environment Variables

bash
# .git/hooks/config
PROJECT_NAME="my-site"
REPO_NAME="custom-theme"
INDEXER_PATH="${WDG_HOME}/indexer"
LOG_FILE="${WDG_HOME}/logs/indexing.log"

Hook Settings

bash
# Enable/disable hooks
ENABLE_POST_COMMIT=true
ENABLE_POST_MERGE=true
ENABLE_POST_CHECKOUT=true

# Indexing settings
BATCH_SIZE=10
ASYNC_INDEXING=true
LOG_LEVEL=INFO

Indexing Script

The hooks call index_changes.py:

python
#!/usr/bin/env python3
# indexer/index_changes.py

import argparse
import sys
from indexer import Indexer

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--project', required=True)
    parser.add_argument('--repository', required=True)
    parser.add_argument('--files', required=True)
    parser.add_argument('--commit', required=False)
    parser.add_argument('--author', required=False)
    parser.add_argument('--merge-head', required=False)
    args = parser.parse_args()

    # Parse file list
    files = args.files.split('\n')
    files = [f.strip() for f in files if f.strip()]

    # Initialize indexer
    indexer = Indexer(args.project)

    # Index changed files
    try:
        indexer.index_files(
            repository=args.repository,
            files=files,
            commit_hash=args.commit,
            author=args.author
        )
        print(f"✓ Indexed {len(files)} files")
    except Exception as e:
        print(f"✗ Indexing failed: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == '__main__':
    main()

Advanced Features

Conditional Indexing

bash
# Only index on specific branches
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)

if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "develop" ]; then
    echo "Skipping indexing on feature branch"
    exit 0
fi

Throttling

bash
# Only index if last index was > 1 minute ago
LAST_INDEX_FILE=".git/last-index"
CURRENT_TIME=$(date +%s)

if [ -f "$LAST_INDEX_FILE" ]; then
    LAST_INDEX=$(cat "$LAST_INDEX_FILE")
    TIME_DIFF=$((CURRENT_TIME - LAST_INDEX))

    if [ $TIME_DIFF -lt 60 ]; then
        echo "Skipping (indexed $TIME_DIFF seconds ago)"
        exit 0
    fi
fi

# Run indexing...

# Update timestamp
echo "$CURRENT_TIME" > "$LAST_INDEX_FILE"

Background Indexing

bash
# Run indexing in background to avoid blocking
{
    python3 /path/to/indexer/index_changes.py \
        --project "$PROJECT_NAME" \
        --files "$INDEXABLE" \
        >> "$LOG_FILE" 2>&1
} &

echo "Indexing started in background..."

Logging

Hook Logs

bash
# Log to file
exec >> ${WDG_HOME}/logs/hooks.log 2>&1

echo "$(date '+%Y-%m-%d %H:%M:%S') - post-commit hook triggered"
echo "Files changed: $CHANGED_FILES"

Indexing Logs

python
import logging

logging.basicConfig(
    filename='${WDG_HOME}/logs/indexing.log',
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger('indexer')
logger.info(f"Indexing {len(files)} files for {project_name}")

Troubleshooting

Hooks Not Running

bash
# Check if hooks are executable
ls -la .git/hooks/

# Make executable
chmod +x .git/hooks/post-commit
chmod +x .git/hooks/post-merge
chmod +x .git/hooks/post-checkout

# Test hook manually
.git/hooks/post-commit

Indexing Failures

bash
# Check indexing logs
tail -f ${WDG_HOME}/logs/indexing.log

# Test indexer directly
python3 indexer/index_changes.py \
    --project my-site \
    --repository custom-theme \
    --files "functions.php"

Hook Debugging

Add debug output to hooks:

bash
#!/bin/bash
set -x  # Print commands as they execute

# Rest of hook...

Hook Management Commands

Disable Hooks Temporarily

bash
# Rename hooks to disable
mv .git/hooks/post-commit .git/hooks/post-commit.disabled

# Re-enable
mv .git/hooks/post-commit.disabled .git/hooks/post-commit

Skip Hooks for Single Commit

bash
git commit --no-verify -m "Skip hooks for this commit"

Reinstall Hooks

bash
# Via CLI
wdg repo hooks install my-site custom-theme

# Manual
cp hooks/* /path/to/repo/.git/hooks/
chmod +x /path/to/repo/.git/hooks/*

Best Practices

  1. Keep hooks fast - Run indexing in background if needed
  2. Add logging - Track what gets indexed and when
  3. Error handling - Don't block commits on indexing failures
  4. Conditional logic - Skip indexing when appropriate
  5. Version control - Keep hook templates in repository

Custom Hooks

Creating Custom Hook

bash
#!/bin/bash
# .git/hooks/pre-push

# Run linting before push
echo "Running linter..."
wdg theme lint $(basename $(pwd))

if [ $? -ne 0 ]; then
    echo "Linting failed! Fix errors before pushing."
    exit 1
fi

echo "✓ Linting passed"

Shared Hook Logic

bash
# hooks/common.sh
get_project_name() {
    basename $(dirname $(dirname $(git rev-parse --show-toplevel)))
}

get_repo_name() {
    basename $(git rev-parse --show-toplevel)
}

# Source in hooks
source "$(dirname $0)/common.sh"
PROJECT=$(get_project_name)

See Also:

Released under the MIT License.