285 lines
8.6 KiB
Python
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())
|