285 lines
8.6 KiB
Python

#!/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())