#!/usr/bin/env python3 """ Project progress tracker that generates status documentation from Git history. """ import os import sys import argparse from datetime import datetime from typing import Dict, Any, List, Optional # Add the current directory to the Python path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # Import our modules from config import Config from code_stats import analyze_code_stats from git_analyzer import GitAnalyzer from feature_markdown import ( enrich_features, emoji_status, extract_features, generate_features_md, ) from template_engine import TemplateEngine from changelog_generator import update_changelog_file from roadmap_generator import generate_roadmap def load_features(features_file: str) -> Dict[str, Dict[str, Any]]: """Load features from a Markdown file.""" # Use the improved extraction function return extract_features(features_file) def update_features( features: Dict[str, Dict[str, Any]], feature_updates: Dict[str, Dict[str, Any]], new_features: Dict[str, Dict[str, Any]], ) -> Dict[str, Dict[str, Any]]: """Update features with new information from Git history.""" updated_features = features.copy() # Update existing features for feature_key, update in feature_updates.items(): if feature_key in updated_features: updated_features[feature_key].update(update) # Add new features for feature_key, feature_data in new_features.items(): if feature_key not in updated_features: updated_features[feature_key] = feature_data return updated_features def format_changelog_entries(git_data: Dict[str, Any]) -> List[str]: """Format changelog entries from Git data.""" entries = [] # Add changelog entries for entry in git_data.get("changelog_entries", []): entries.append( f"- {entry['date']}: {entry['message']} ({entry['commit']})" ) # Add fix entries for fix in git_data.get("fixes", []): entries.append( f"- {fix['date']}: Fixed: {fix['message']} ({fix['commit']})" ) # Add untagged commits for commit in git_data.get("untagged_commits", []): entries.append( f"- {commit['date']}: {commit['message']} ({commit['commit']})" ) return entries def extract_known_issues(git_data: Dict[str, Any]) -> List[Dict[str, Any]]: """Extract known issues from Git data.""" issues = [] # Look for issues in the Git data for issue in git_data.get("issues", []): issues.append( {"description": issue.get("message", ""), "status": "open"} ) return issues def enrich_features( features: Dict[str, Dict[str, Any]] ) -> Dict[str, Dict[str, Any]]: """Add additional information to features for display purposes.""" enriched = {} for key, data in features.items(): enriched[key] = data.copy() status = data.get("status", "unknown") enriched[key]["status_emoji"] = emoji_status(status) # Add title-cased key for display enriched[key]["display_name"] = key.replace("_", " ").title() return enriched def main(): """Main function to run the progress tracker.""" parser = argparse.ArgumentParser( description="Track project progress and generate status documentation." ) parser.add_argument( "--config", default="scripts/track_progress/progress_config.json", help="Path to configuration file", ) parser.add_argument("--project", help="Project name (overrides config)") parser.add_argument( "--output-dir", help="Output directory (overrides config)" ) parser.add_argument("--repo", default=".", help="Repository path") parser.add_argument( "--verbose", "-v", action="store_true", help="Enable verbose output" ) args = parser.parse_args() verbose = args.verbose # Load configuration config = Config(args.config) # Override config with command-line arguments if args.project: config.set("project_name", args.project) if args.output_dir: config.set("output_dir", args.output_dir) # Ensure output directory exists output_dir = config.get("output_dir") os.makedirs(output_dir, exist_ok=True) # Initialize components git_analyzer = GitAnalyzer(args.repo) template_engine = TemplateEngine() # Get repository information repo_info = git_analyzer.get_repo_info() # Analyze Git history git_data = git_analyzer.analyze_commits( limit=config.get("git_log_limit"), untagged_limit=config.get("untagged_commit_limit"), ) # Analyze code statistics code_stats = analyze_code_stats( root_dir=args.repo, exclude_dirs=config.get("exclude_dirs"), source_extensions=config.get("source_extensions"), top_files_limit=config.get("top_files_limit"), ) # Load existing features features_file = config.get("features_file") if not os.path.isabs(features_file): # If it's not an absolute path, make it relative to the repo root, not the output dir features_file = os.path.join(args.repo, features_file) if verbose: print(f"Loading features from: {features_file}") features = load_features(features_file) if verbose: print(f"Loaded {len(features)} features") # Update features with Git data features = update_features( features, git_data.get("feature_updates", {}), git_data.get("new_features", {}), ) if verbose: print(f"Updated features count: {len(features)}") # Enrich features with emojis and other display information enriched_features = enrich_features(features) # Format changelog entries changelog_entries = format_changelog_entries(git_data) # Extract known issues known_issues = extract_known_issues(git_data) # Generate roadmap roadmap = generate_roadmap( git_data.get("milestones", {}), git_data.get("roadmap_items", {}) ) # Prepare template context context = { "project_name": config.get("project_name"), "current_date": datetime.now().strftime("%Y-%m-%d"), "repo_info": repo_info, "stats": code_stats, "features": enriched_features, "changelog_entries": changelog_entries, "known_issues": known_issues, "roadmap": roadmap, } # Generate status document status_template = config.get("templates", {}).get( "status", "status_template.md" ) status_content = template_engine.render(status_template, context) status_file = os.path.join(args.repo, output_dir, config.get("status_doc")) os.makedirs(os.path.dirname(status_file), exist_ok=True) with open(status_file, "w", encoding="utf-8") as f: f.write(status_content) # Generate features document using the improved function features_content = generate_features_md( features, config.get("project_name") ) # Ensure directory exists for features file os.makedirs(os.path.dirname(features_file), exist_ok=True) # Create backup of original features file if os.path.exists(features_file): backup_file = ( f"{features_file}.bak.{datetime.now().strftime('%Y%m%d%H%M%S')}" ) with open(features_file, "r", encoding="utf-8") as src: with open(backup_file, "w", encoding="utf-8") as dst: dst.write(src.read()) if verbose: print(f"Created backup: {backup_file}") # Write the new features file with open(features_file, "w", encoding="utf-8") as f: f.write(features_content) # Update changelog changelog_file = config.get( "changelog_file", "CHANGELOG.md" ) # Default to CHANGELOG.md if not os.path.isabs(changelog_file): # If it's a relative path, make it relative to the repo root, not the output dir changelog_file = os.path.join(args.repo, changelog_file) if verbose: print(f"Updating changelog file: {changelog_file}") # Ensure the directory exists changelog_dir = os.path.dirname(changelog_file) if changelog_dir: # Only create directory if there is one os.makedirs(changelog_dir, exist_ok=True) update_changelog_file( git_data.get("changelog_entries", []), changelog_file, max_entries=config.get("git_log_limit"), verbose=verbose, ) print(f"Progress tracking updated:") print(f"- Status document: {status_file}") print(f"- Features document: {features_file}") print(f"- Changelog: {changelog_file}") return 0 if __name__ == "__main__": sys.exit(main())