Initial commit: ProjectTracker infrastructure
Multi-repository state tracker and coordinator for managing 18+ Git projects. Core Components: - tools/scan.sh: Bash script to scan all repos, pull updates, detect conflicts - tools/analyze.py: Python generator for human-readable summaries - tools/config.json: Repository tracking configuration - README.md: Complete documentation Features: - State tracking (uncommitted changes, new commits, conflicts) - Activity monitoring (3-week lookback window) - Automatic pulling with conflict detection - Categorization (META, CONSTANT, WIP, CONCEPT, PAUSE, DONE) - Summary generation with prioritization analyze.py created in Session 7 - Cycle 2 (ClaudeSelf external exploration). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
13234b0e8b
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
# Environment variables (NEVER commit tokens/secrets)
|
||||
.env
|
||||
|
||||
# Planning outputs (generated files)
|
||||
.planning/state.json
|
||||
.planning/scan.log
|
||||
.planning/conflicts.txt
|
||||
.planning/summary.md
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
tmpclaude-*
|
||||
*.tmp
|
||||
196
README.md
Normal file
196
README.md
Normal file
@ -0,0 +1,196 @@
|
||||
# ProjectTracker
|
||||
|
||||
**Multi-repository state tracker and coordinator for managing multiple Git projects.**
|
||||
|
||||
ProjectTracker scans all tracked repositories, detects changes, pulls updates, identifies conflicts, and generates human-readable summaries of the entire ecosystem state.
|
||||
|
||||
## Purpose
|
||||
|
||||
Managing 15+ active Git repositories manually is tedious and error-prone. ProjectTracker automates:
|
||||
|
||||
- **State Tracking**: Which repos have new commits, uncommitted changes, conflicts
|
||||
- **Activity Monitoring**: Which projects are active vs dormant
|
||||
- **Conflict Detection**: Automatic merge conflict identification
|
||||
- **Categorization**: Organize projects by status (WIP, CONSTANT, PAUSE, etc.)
|
||||
- **Reporting**: Generate readable summaries for quick ecosystem overview
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
ProjectTracker/
|
||||
├── tools/
|
||||
│ ├── config.json # Repository configuration and tracking settings
|
||||
│ ├── scan.sh # Main scanner script (bash)
|
||||
│ └── analyze.py # Summary generator (Python)
|
||||
├── projects/
|
||||
│ ├── META/ # Meta-projects (planning, infrastructure)
|
||||
│ ├── CONSTANT/ # Ongoing/maintenance projects
|
||||
│ ├── WIP/ # Active development projects
|
||||
│ ├── CONCEPT/ # Early-stage ideas
|
||||
│ ├── PAUSE/ # Paused projects
|
||||
│ └── DONE/ # Completed projects
|
||||
├── .planning/ # Generated state files (auto-created)
|
||||
│ ├── state.json # Machine-readable repo state
|
||||
│ ├── summary.md # Human-readable summary
|
||||
│ ├── scan.log # Detailed scan logs
|
||||
│ └── conflicts.txt # List of repos with conflicts
|
||||
├── .env # Gitea credentials and configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### 1. Environment Variables (`.env`)
|
||||
|
||||
```bash
|
||||
GITEA_URL=https://git.etheryale.com
|
||||
GITEA_TOKEN=your_token_here
|
||||
```
|
||||
|
||||
**Note**: Use `wget` instead of `curl` for API calls due to proxy compatibility.
|
||||
|
||||
### 2. Repository Tracking (`tools/config.json`)
|
||||
|
||||
Define which repos to track, exclude, and categorize:
|
||||
|
||||
```json
|
||||
{
|
||||
"repos_root": "C:/Users/alexi/Documents/projects",
|
||||
"tracked_repos": ["civjdr", "ChineseClass", "seogeneratorserver", ...],
|
||||
"excluded_repos": ["couple-repo", "vba-mcp-demo", ...],
|
||||
"project_mapping": {
|
||||
"civjdr": "projects/CONSTANT/civjdr.md",
|
||||
"ChineseClass": "projects/CONSTANT/ChineseClass.md",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Scan Options**:
|
||||
- `auto_pull`: Automatically pull updates (default: true)
|
||||
- `commits_lookback_days`: Activity window for "recent" commits (default: 21)
|
||||
- `detect_uncommitted_changes`: Check for unstaged/uncommitted files (default: true)
|
||||
|
||||
## Usage
|
||||
|
||||
### Full Scan and Summary
|
||||
|
||||
```bash
|
||||
cd ProjectTracker
|
||||
bash tools/scan.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Fetch updates from all tracked repos
|
||||
2. Pull changes (if `auto_pull: true`)
|
||||
3. Detect conflicts, uncommitted changes, ahead/behind status
|
||||
4. Generate `.planning/state.json` with full repo state
|
||||
5. Run `analyze.py` to generate `.planning/summary.md`
|
||||
|
||||
### Generate Summary Only
|
||||
|
||||
If you already have a `state.json` and want to regenerate the summary:
|
||||
|
||||
```bash
|
||||
python tools/analyze.py
|
||||
```
|
||||
|
||||
### View Summary
|
||||
|
||||
```bash
|
||||
cat .planning/summary.md
|
||||
```
|
||||
|
||||
## Output Files
|
||||
|
||||
### state.json (Machine-Readable)
|
||||
|
||||
JSON snapshot of all repos with detailed metrics:
|
||||
|
||||
```json
|
||||
{
|
||||
"last_scan": "2026-01-18T14:30:00+00:00",
|
||||
"repos": {
|
||||
"civjdr": {
|
||||
"last_commit": "abc1234",
|
||||
"branch": "main",
|
||||
"commits_3w": 8,
|
||||
"new_commits_since_last_scan": 2,
|
||||
"has_uncommitted_changes": false,
|
||||
"has_conflict": false,
|
||||
"needs_attention": true,
|
||||
...
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"total_repos": 18,
|
||||
"active_3w": 12,
|
||||
"needs_attention": 5,
|
||||
"conflicts": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### summary.md (Human-Readable)
|
||||
|
||||
Markdown report with:
|
||||
- **Global Stats**: Total repos, active count, attention needed, conflicts
|
||||
- **Needs Attention**: Prioritized list (conflicts → uncommitted → new commits → behind)
|
||||
- **By Category**: Repos organized by status (META, CONSTANT, WIP, etc.)
|
||||
- **Recent Activity**: Commit counts for active repos (last 3 weeks)
|
||||
|
||||
Example:
|
||||
```markdown
|
||||
## ⚠️ Needs Attention
|
||||
|
||||
- **seogeneratorserver** (develop) - ❌ CONFLICT | 📝 7 uncommitted | 🆕 5 new
|
||||
Last: Update SEO templates (3 days ago)
|
||||
- **ChineseClass** (main) - 📝 3 uncommitted | 🆕 2 new
|
||||
Last: Add vocabulary list (2 days ago)
|
||||
```
|
||||
|
||||
## Project Categorization
|
||||
|
||||
Projects are organized by development status:
|
||||
|
||||
- **META**: Infrastructure, planning, coordination projects
|
||||
- **CONSTANT**: Ongoing/maintenance projects (no end date)
|
||||
- **WIP**: Active development (work in progress)
|
||||
- **CONCEPT**: Early-stage ideas, prototypes
|
||||
- **PAUSE**: Temporarily paused projects
|
||||
- **DONE**: Completed projects (archived)
|
||||
|
||||
Each project has a corresponding `.md` file in `projects/<CATEGORY>/` with:
|
||||
- Description
|
||||
- Current status
|
||||
- Repository URL
|
||||
- Next steps
|
||||
- Notes
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Daily/Weekly**: Run `scan.sh` to check ecosystem state
|
||||
2. **Review**: Check `summary.md` for repos needing attention
|
||||
3. **Act**: Address conflicts, commit changes, pull updates
|
||||
4. **Repeat**: Keep repos synchronized and tracked
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Bash**: For `scan.sh` (Git Bash on Windows, native on Linux/Mac)
|
||||
- **Python 3.7+**: For `analyze.py`
|
||||
- **jq**: JSON processor for bash script parsing
|
||||
- **Git**: Obviously :)
|
||||
|
||||
## Notes
|
||||
|
||||
- Scanner automatically fetches before pulling
|
||||
- Conflicts are never auto-resolved (manual intervention required)
|
||||
- `state.json` tracks previous scan state to detect "new" commits
|
||||
- All timestamps use ISO 8601 format
|
||||
- UTF-8 encoding support for emoji indicators on all platforms
|
||||
|
||||
---
|
||||
|
||||
**Created**: 2026-01-18 (Cycle 2 - External Exploration)
|
||||
**Purpose**: Coordinate multi-project ecosystem efficiently
|
||||
**Maintained**: Active
|
||||
30
projects/WIP/WrittingFantasy.md
Normal file
30
projects/WIP/WrittingFantasy.md
Normal file
@ -0,0 +1,30 @@
|
||||
# WrittingFantasy
|
||||
|
||||
**Statut:** WIP (Work In Progress)
|
||||
**Type:** Projet d'écriture fantasy
|
||||
**Repository:** git@git.etheryale.com:StillHammer/WrittingFantasy.git
|
||||
|
||||
## Description
|
||||
|
||||
Projet d'écriture fantasy avec worldbuilding complet, personnages, et arcs narratifs.
|
||||
|
||||
## Structure
|
||||
|
||||
- `worldbuilding/` - Univers, géographie, magie, cultures
|
||||
- `characters/` - Fiches personnages
|
||||
- `plots/` - Intrigues et arcs narratifs
|
||||
- `chapters/` - Chapitres et brouillons
|
||||
- `notes/` - Notes et idées
|
||||
|
||||
## Prochaines Étapes
|
||||
|
||||
1. Créer le repository sur Gitea (git.etheryale.com)
|
||||
2. Pusher le commit initial
|
||||
3. Développer le worldbuilding de base
|
||||
4. Créer les fiches des personnages principaux
|
||||
5. Écrire les premiers chapitres
|
||||
|
||||
## Notes
|
||||
|
||||
- Repository créé localement le 2025-12-17
|
||||
- En attente de création sur Gitea (serveur API injoignable temporairement)
|
||||
207
tools/analyze.py
Normal file
207
tools/analyze.py
Normal file
@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ProjectTracker - Summary Generator
|
||||
Reads state.json and generates human-readable summary.md
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# Force UTF-8 encoding for Windows console (emoji support)
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
if hasattr(sys.stdout, 'reconfigure'):
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
# Constants
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
PROJECT_TRACKER = SCRIPT_DIR.parent
|
||||
STATE_FILE = PROJECT_TRACKER / ".planning" / "state.json"
|
||||
CONFIG_FILE = SCRIPT_DIR / "config.json"
|
||||
SUMMARY_FILE = PROJECT_TRACKER / ".planning" / "summary.md"
|
||||
|
||||
# Categories order for display
|
||||
CATEGORIES_ORDER = ["META", "CONSTANT", "WIP", "CONCEPT", "PAUSE", "DONE"]
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
"""Load JSON file"""
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_category(project_file: str) -> str:
|
||||
"""Extract category from project file path
|
||||
|
||||
Example: projects/WIP/repo.md -> WIP
|
||||
"""
|
||||
if not project_file:
|
||||
return "UNCATEGORIZED"
|
||||
|
||||
parts = project_file.split('/')
|
||||
if len(parts) >= 2 and parts[0] == "projects":
|
||||
return parts[1]
|
||||
|
||||
return "UNCATEGORIZED"
|
||||
|
||||
|
||||
def format_repo_status(repo_name: str, data: Dict) -> str:
|
||||
"""Format single repo status line with indicators"""
|
||||
indicators = []
|
||||
|
||||
# Priority order: conflict > uncommitted > new commits > behind
|
||||
if data.get("has_conflict"):
|
||||
indicators.append("❌ CONFLICT")
|
||||
if data.get("has_uncommitted_changes"):
|
||||
count = data.get("uncommitted_count", 0)
|
||||
indicators.append(f"📝 {count} uncommitted")
|
||||
if data.get("new_commits_since_last_scan", 0) > 0:
|
||||
count = data["new_commits_since_last_scan"]
|
||||
indicators.append(f"🆕 {count} new")
|
||||
if data.get("behind", 0) > 0:
|
||||
count = data["behind"]
|
||||
indicators.append(f"⬆️ {count} behind")
|
||||
|
||||
status = " | ".join(indicators) if indicators else "✅"
|
||||
|
||||
# Format output
|
||||
branch = data.get("branch", "unknown")
|
||||
msg = data.get("last_commit_msg", "No commits")
|
||||
date = data.get("last_commit_date", "unknown")
|
||||
|
||||
return f"- **{repo_name}** ({branch}) - {status}\n Last: {msg} ({date})"
|
||||
|
||||
|
||||
def categorize_repos(repos: Dict, config: Dict) -> Dict[str, List[Tuple[str, Dict]]]:
|
||||
"""Organize repos by category"""
|
||||
categorized = {cat: [] for cat in CATEGORIES_ORDER}
|
||||
categorized["UNCATEGORIZED"] = []
|
||||
|
||||
for repo_name, data in repos.items():
|
||||
category = get_category(data.get("project_file", ""))
|
||||
if category in categorized:
|
||||
categorized[category].append((repo_name, data))
|
||||
else:
|
||||
categorized["UNCATEGORIZED"].append((repo_name, data))
|
||||
|
||||
return categorized
|
||||
|
||||
|
||||
def generate_summary():
|
||||
"""Main function: generate summary.md from state.json"""
|
||||
|
||||
# Load data
|
||||
state = load_json(STATE_FILE)
|
||||
config = load_json(CONFIG_FILE)
|
||||
|
||||
repos = state["repos"]
|
||||
stats = state["stats"]
|
||||
last_scan = state["last_scan"]
|
||||
|
||||
# Categorize
|
||||
categorized = categorize_repos(repos, config)
|
||||
|
||||
# Start building markdown
|
||||
md = []
|
||||
md.append("# ProjectTracker - Summary\n")
|
||||
md.append(f"**Last Scan:** {last_scan}\n")
|
||||
md.append("---\n")
|
||||
|
||||
# Global Stats
|
||||
md.append("## 📊 Global Stats\n")
|
||||
md.append(f"- **Total Repos:** {stats['total_repos']}")
|
||||
md.append(f"- **Active (3w):** {stats['active_3w']}")
|
||||
md.append(f"- **Needs Attention:** {stats['needs_attention']}")
|
||||
md.append(f"- **Conflicts:** {stats['conflicts']}\n")
|
||||
|
||||
# Needs Attention Section
|
||||
attention_repos = [
|
||||
(name, data) for name, data in repos.items()
|
||||
if data.get("needs_attention")
|
||||
]
|
||||
|
||||
if attention_repos:
|
||||
md.append("## ⚠️ Needs Attention\n")
|
||||
|
||||
# Sort by priority: conflicts > uncommitted > new commits > behind
|
||||
attention_repos.sort(key=lambda x: (
|
||||
-int(x[1].get("has_conflict", False)),
|
||||
-int(x[1].get("has_uncommitted_changes", False)),
|
||||
-x[1].get("new_commits_since_last_scan", 0),
|
||||
-x[1].get("behind", 0)
|
||||
))
|
||||
|
||||
for repo_name, data in attention_repos:
|
||||
md.append(format_repo_status(repo_name, data))
|
||||
md.append("")
|
||||
|
||||
# By Category Section
|
||||
md.append("## 📂 By Category\n")
|
||||
|
||||
for category in CATEGORIES_ORDER:
|
||||
repos_in_cat = categorized[category]
|
||||
if repos_in_cat:
|
||||
md.append(f"### {category} ({len(repos_in_cat)})\n")
|
||||
for repo_name, data in sorted(repos_in_cat):
|
||||
md.append(format_repo_status(repo_name, data))
|
||||
md.append("")
|
||||
|
||||
# Uncategorized (if any)
|
||||
if categorized["UNCATEGORIZED"]:
|
||||
count = len(categorized["UNCATEGORIZED"])
|
||||
md.append(f"### UNCATEGORIZED ({count})\n")
|
||||
for repo_name, data in sorted(categorized["UNCATEGORIZED"]):
|
||||
md.append(format_repo_status(repo_name, data))
|
||||
md.append("")
|
||||
|
||||
# Recent Activity Section
|
||||
active_repos = [
|
||||
(name, data) for name, data in repos.items()
|
||||
if data.get("commits_3w", 0) > 0
|
||||
]
|
||||
|
||||
if active_repos:
|
||||
md.append("## 🔥 Recent Activity (3 weeks)\n")
|
||||
|
||||
# Sort by commit count (descending)
|
||||
active_repos.sort(key=lambda x: -x[1]["commits_3w"])
|
||||
|
||||
for repo_name, data in active_repos:
|
||||
commits = data["commits_3w"]
|
||||
plural = 's' if commits > 1 else ''
|
||||
md.append(f"- **{repo_name}**: {commits} commit{plural}")
|
||||
md.append("")
|
||||
|
||||
# Footer
|
||||
md.append("---\n")
|
||||
md.append("*Generated by ProjectTracker/tools/analyze.py*")
|
||||
|
||||
# Write summary
|
||||
summary_content = "\n".join(md)
|
||||
SUMMARY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(SUMMARY_FILE, 'w', encoding='utf-8') as f:
|
||||
f.write(summary_content)
|
||||
|
||||
# Console output
|
||||
print(f"✅ Summary generated: {SUMMARY_FILE}")
|
||||
print(f"📊 {stats['total_repos']} repos scanned, {stats['needs_attention']} need attention")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
generate_summary()
|
||||
except FileNotFoundError as e:
|
||||
print(f"❌ Error: File not found - {e}")
|
||||
print("💡 Run scan.sh first to generate state.json")
|
||||
exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Error: Invalid JSON - {e}")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
exit(1)
|
||||
69
tools/config.json
Normal file
69
tools/config.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"repos_root": "C:/Users/alexi/Documents/projects",
|
||||
|
||||
"tracked_repos": [
|
||||
"aissia",
|
||||
"groveengine",
|
||||
"AlwaysOnRecorder",
|
||||
"civjdr",
|
||||
"ChineseClass",
|
||||
"Class_generator",
|
||||
"confluent",
|
||||
"maicivy",
|
||||
"mobilecommand",
|
||||
"secondvoice",
|
||||
"seogeneratorserver",
|
||||
"vba-mcp-monorepo",
|
||||
"vba-mcp-server",
|
||||
"videotoMP3Transcriptor",
|
||||
"warfactoryracine",
|
||||
"wechat-homework-bot",
|
||||
"freelance_planning",
|
||||
"WrittingFantasy"
|
||||
],
|
||||
|
||||
"excluded_repos": [
|
||||
"couple-repo",
|
||||
"knowledgemapperrepo",
|
||||
"vba-mcp-demo",
|
||||
"ProjectTracker"
|
||||
],
|
||||
|
||||
"project_mapping": {
|
||||
"aissia": "projects/WIP/aissia.md",
|
||||
"groveengine": "projects/WIP/groveengine.md",
|
||||
"AlwaysOnRecorder": "projects/WIP/AlwaysOnRecorder.md",
|
||||
"civjdr": "projects/CONSTANT/civjdr.md",
|
||||
"ChineseClass": "projects/CONSTANT/ChineseClass.md",
|
||||
"Class_generator": "projects/CONSTANT/Class_generator.md",
|
||||
"confluent": "projects/WIP/confluent.md",
|
||||
"maicivy": "projects/WIP/maicivy.md",
|
||||
"mobilecommand": "projects/CONCEPT/mobilecommand.md",
|
||||
"secondvoice": "projects/PAUSE/secondvoice.md",
|
||||
"seogeneratorserver": "projects/WIP/seogeneratorserver.md",
|
||||
"vba-mcp-monorepo": "projects/WIP/vba-mcp-monorepo.md",
|
||||
"vba-mcp-server": "projects/WIP/vba-mcp-server.md",
|
||||
"videotoMP3Transcriptor": "projects/DONE/videotoMP3Transcriptor.md",
|
||||
"warfactoryracine": "projects/PAUSE/warfactoryracine.md",
|
||||
"wechat-homework-bot": "projects/WIP/wechat-homework-bot.md",
|
||||
"freelance_planning": "projects/META/freelance_planning.md",
|
||||
"WrittingFantasy": "projects/WIP/WrittingFantasy.md"
|
||||
},
|
||||
|
||||
"scan_options": {
|
||||
"auto_pull": true,
|
||||
"fetch_before_pull": true,
|
||||
"check_ahead_behind": true,
|
||||
"commits_lookback_days": 21,
|
||||
"detect_uncommitted_changes": true,
|
||||
"timeout_per_repo_seconds": 30
|
||||
},
|
||||
|
||||
"output_options": {
|
||||
"state_file": ".planning/state.json",
|
||||
"log_file": ".planning/scan.log",
|
||||
"conflicts_file": ".planning/conflicts.txt",
|
||||
"summary_file": ".planning/summary.md"
|
||||
}
|
||||
}
|
||||
208
tools/scan.sh
Normal file
208
tools/scan.sh
Normal file
@ -0,0 +1,208 @@
|
||||
#!/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
|
||||
Loading…
Reference in New Issue
Block a user