#!/bin/bash # ProjectTracker - Multi-repo scan script # Scans all tracked repos, pulls updates, generates state.json set -euo pipefail # Paths SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_TRACKER="$(cd "$SCRIPT_DIR/.." && pwd)" CONFIG_FILE="$SCRIPT_DIR/config.json" # Check jq availability if ! command -v jq &> /dev/null; then echo "ERROR: jq is required but not installed." echo "Install: https://stedolan.github.io/jq/download/" exit 1 fi # Load config REPOS_ROOT=$(jq -r '.repos_root' "$CONFIG_FILE") LOOKBACK_DAYS=$(jq -r '.scan_options.commits_lookback_days' "$CONFIG_FILE") AUTO_PULL=$(jq -r '.scan_options.auto_pull' "$CONFIG_FILE") # Output files STATE_FILE="$PROJECT_TRACKER/.planning/state.json" LOG_FILE="$PROJECT_TRACKER/.planning/scan.log" CONFLICTS_FILE="$PROJECT_TRACKER/.planning/conflicts.txt" # Ensure .planning exists mkdir -p "$PROJECT_TRACKER/.planning" # Init log echo "===== Scan started: $(date -Iseconds) =====" > "$LOG_FILE" > "$CONFLICTS_FILE" echo "🔍 ProjectTracker - Multi-repo scan" echo "Repos root: $REPOS_ROOT" echo "" # Read tracked repos into array mapfile -t TRACKED_REPOS < <(jq -r '.tracked_repos[]' "$CONFIG_FILE") # Start building state.json cat > "$STATE_FILE" << 'EOF_HEADER' { "last_scan": "TIMESTAMP_PLACEHOLDER", "repos": { EOF_HEADER FIRST_REPO=true CONFLICT_COUNT=0 SCANNED_COUNT=0 ATTENTION_COUNT=0 # Scan each repo for repo in "${TRACKED_REPOS[@]}"; do REPO_PATH="$REPOS_ROOT/$repo" # Check if repo exists if [ ! -d "$REPO_PATH/.git" ]; then echo "⚠️ $repo - NOT A GIT REPO (skipping)" | tee -a "$LOG_FILE" continue fi echo -n "📂 $repo... " | tee -a "$LOG_FILE" cd "$REPO_PATH" # Fetch origin git fetch origin &>> "$LOG_FILE" || true # Pull if enabled HAS_CONFLICT=false if [ "$AUTO_PULL" = "true" ]; then PULL_OUTPUT=$(git pull 2>&1) || true echo "$PULL_OUTPUT" >> "$LOG_FILE" if echo "$PULL_OUTPUT" | grep -q "CONFLICT\|Automatic merge failed"; then HAS_CONFLICT=true CONFLICT_COUNT=$((CONFLICT_COUNT + 1)) echo "⚠️ CONFLICT: $repo" | tee -a "$CONFLICTS_FILE" fi fi # Collect repo info CURRENT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") LAST_COMMIT_MSG=$(git log -1 --format="%s" 2>/dev/null || echo "No commits") LAST_COMMIT_DATE=$(git log -1 --format="%ar" 2>/dev/null || echo "unknown") # Ahead/behind tracking branch TRACKING_BRANCH=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "") if [ -n "$TRACKING_BRANCH" ]; then AHEAD=$(git rev-list --count HEAD..$TRACKING_BRANCH 2>/dev/null || echo 0) BEHIND=$(git rev-list --count $TRACKING_BRANCH..HEAD 2>/dev/null || echo 0) else AHEAD=0 BEHIND=0 fi # Uncommitted changes if git diff-index --quiet HEAD -- 2>/dev/null; then UNCOMMITTED=false UNCOMMITTED_COUNT=0 else UNCOMMITTED=true UNCOMMITTED_COUNT=$(git status --short 2>/dev/null | wc -l) fi # Recent commits COMMITS_3W=$(git rev-list --count --since="$LOOKBACK_DAYS days ago" HEAD 2>/dev/null || echo 0) # Load previous commit from existing state (if exists) PREV_COMMIT=$(jq -r ".repos.\"$repo\".last_commit // \"\"" "$STATE_FILE" 2>/dev/null || echo "") if [ -n "$PREV_COMMIT" ] && [ "$PREV_COMMIT" != "$CURRENT_COMMIT" ] && [ "$PREV_COMMIT" != "null" ]; then NEW_COMMITS=$(git rev-list --count $PREV_COMMIT..$CURRENT_COMMIT 2>/dev/null || echo 0) else NEW_COMMITS=0 fi # Determine needs_attention NEEDS_ATTENTION=false if [ "$NEW_COMMITS" -gt 0 ] || [ "$UNCOMMITTED" = "true" ] || [ "$BEHIND" -gt 0 ] || [ "$HAS_CONFLICT" = "true" ]; then NEEDS_ATTENTION=true ATTENTION_COUNT=$((ATTENTION_COUNT + 1)) fi # Project file mapping PROJECT_FILE=$(jq -r ".project_mapping.\"$repo\" // \"\"" "$CONFIG_FILE") # Add comma if not first repo if [ "$FIRST_REPO" = false ]; then echo "," >> "$STATE_FILE" fi FIRST_REPO=false # Write JSON entry (properly escaped) LAST_COMMIT_MSG_ESCAPED=$(echo "$LAST_COMMIT_MSG" | sed 's/\\/\\\\/g; s/"/\\"/g') cat >> "$STATE_FILE" << EOF "$repo": { "last_commit": "$CURRENT_COMMIT", "branch": "$CURRENT_BRANCH", "last_commit_msg": "$LAST_COMMIT_MSG_ESCAPED", "last_commit_date": "$LAST_COMMIT_DATE", "commits_3w": $COMMITS_3W, "new_commits_since_last_scan": $NEW_COMMITS, "ahead": $AHEAD, "behind": $BEHIND, "has_uncommitted_changes": $UNCOMMITTED, "uncommitted_count": $UNCOMMITTED_COUNT, "has_conflict": $HAS_CONFLICT, "needs_attention": $NEEDS_ATTENTION, "project_file": "$PROJECT_FILE" } EOF SCANNED_COUNT=$((SCANNED_COUNT + 1)) # Status indicator if [ "$HAS_CONFLICT" = "true" ]; then echo "❌ CONFLICT" elif [ "$NEEDS_ATTENTION" = "true" ]; then echo "⚠️ needs attention" else echo "✅" fi done # Close repos object and add stats cat >> "$STATE_FILE" << EOF }, "stats": { "total_repos": $SCANNED_COUNT, "active_3w": 0, "needs_attention": $ATTENTION_COUNT, "conflicts": $CONFLICT_COUNT } } EOF # Calculate active repos count ACTIVE_COUNT=$(jq '[.repos[] | select(.commits_3w > 0)] | length' "$STATE_FILE") jq ".stats.active_3w = $ACTIVE_COUNT" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE" # Update timestamp TIMESTAMP=$(date -Iseconds) jq ".last_scan = \"$TIMESTAMP\"" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE" # Summary echo "" echo "===== Scan completed: $(date -Iseconds) =====" | tee -a "$LOG_FILE" echo "✅ Scanned: $SCANNED_COUNT repos" | tee -a "$LOG_FILE" echo "🔥 Active (3w): $ACTIVE_COUNT repos" | tee -a "$LOG_FILE" echo "⚠️ Needs attention: $ATTENTION_COUNT repos" | tee -a "$LOG_FILE" echo "❌ Conflicts: $CONFLICT_COUNT repos" | tee -a "$LOG_FILE" echo "" echo "📊 State saved to: $STATE_FILE" echo "📝 Log saved to: $LOG_FILE" # Generate summary if [ -f "$SCRIPT_DIR/analyze.py" ]; then echo "" echo "Generating summary..." python3 "$SCRIPT_DIR/analyze.py" fi