fix: update CI workflow for repohealth-cli with separate lint, test, and build jobs
This commit is contained in:
7
repohealth-cli/src/repohealth/reporters/__init__.py
Normal file
7
repohealth-cli/src/repohealth/reporters/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Reporting modules for different output formats."""
|
||||
|
||||
from repohealth.reporters.html_reporter import HTMLReporter
|
||||
from repohealth.reporters.json_reporter import JSONReporter
|
||||
from repohealth.reporters.terminal import TerminalReporter
|
||||
|
||||
__all__ = ["TerminalReporter", "JSONReporter", "HTMLReporter"]
|
||||
348
repohealth-cli/src/repohealth/reporters/html_reporter.py
Normal file
348
repohealth-cli/src/repohealth/reporters/html_reporter.py
Normal file
@@ -0,0 +1,348 @@
|
||||
"""HTML reporter using Jinja2 templates."""
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, Template
|
||||
|
||||
from repohealth.models.result import RepositoryResult
|
||||
|
||||
|
||||
class HTMLReporter:
|
||||
"""Reporter for HTML output with visualizations."""
|
||||
|
||||
RISK_COLORS = {
|
||||
"critical": "#dc3545",
|
||||
"high": "#fd7e14",
|
||||
"medium": "#ffc107",
|
||||
"low": "#28a745",
|
||||
"unknown": "#6c757d"
|
||||
}
|
||||
|
||||
def __init__(self, template_dir: Optional[str] = None):
|
||||
"""Initialize the reporter.
|
||||
|
||||
Args:
|
||||
template_dir: Directory containing Jinja2 templates.
|
||||
"""
|
||||
if template_dir:
|
||||
self.template_dir = Path(template_dir)
|
||||
else:
|
||||
self.template_dir = Path(__file__).parent / "templates"
|
||||
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader(str(self.template_dir)),
|
||||
autoescape=True
|
||||
)
|
||||
|
||||
def generate(self, result: RepositoryResult) -> str:
|
||||
"""Generate HTML output from a result.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to convert.
|
||||
|
||||
Returns:
|
||||
HTML string.
|
||||
"""
|
||||
template = self.env.get_template("report.html")
|
||||
return template.render(
|
||||
result=result,
|
||||
risk_colors=self.RISK_COLORS,
|
||||
generated_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
|
||||
)
|
||||
|
||||
def save(self, result: RepositoryResult, file_path: str) -> None:
|
||||
"""Save HTML output to a file.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to save.
|
||||
file_path: Path to output file.
|
||||
"""
|
||||
html_content = self.generate(result)
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
self._copy_assets(Path(file_path).parent)
|
||||
|
||||
def _copy_assets(self, output_dir: Path) -> None:
|
||||
"""Copy CSS/JS assets to output directory.
|
||||
|
||||
Args:
|
||||
output_dir: Directory to copy assets to.
|
||||
"""
|
||||
assets_dir = output_dir / "assets"
|
||||
assets_dir.mkdir(exist_ok=True)
|
||||
|
||||
template_assets = self.template_dir / "assets"
|
||||
if template_assets.exists():
|
||||
for asset in template_assets.iterdir():
|
||||
dest = assets_dir / asset.name
|
||||
dest.write_text(asset.read_text())
|
||||
|
||||
def generate_charts_data(self, result: RepositoryResult) -> dict:
|
||||
"""Generate data for JavaScript charts.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to analyze.
|
||||
|
||||
Returns:
|
||||
Dictionary with chart data.
|
||||
"""
|
||||
risk_summary = result.risk_summary
|
||||
|
||||
risk_distribution = {
|
||||
"labels": ["Critical", "High", "Medium", "Low"],
|
||||
"data": [
|
||||
risk_summary.get("critical", 0),
|
||||
risk_summary.get("high", 0),
|
||||
risk_summary.get("medium", 0),
|
||||
risk_summary.get("low", 0)
|
||||
],
|
||||
"colors": [
|
||||
self.RISK_COLORS["critical"],
|
||||
self.RISK_COLORS["high"],
|
||||
self.RISK_COLORS["medium"],
|
||||
self.RISK_COLORS["low"]
|
||||
]
|
||||
}
|
||||
|
||||
def get_hotspot_attr(h, attr, default=None):
|
||||
"""Get attribute from hotspot dict or object."""
|
||||
if isinstance(h, dict):
|
||||
return h.get(attr, default)
|
||||
return getattr(h, attr, default)
|
||||
|
||||
top_hotspots = [
|
||||
{
|
||||
"file": get_hotspot_attr(h, "file_path", "")[:30],
|
||||
"author": get_hotspot_attr(h, "top_author", "")[:20],
|
||||
"share": round(get_hotspot_attr(h, "top_author_share", 0) * 100, 1),
|
||||
"risk": get_hotspot_attr(h, "risk_level", "unknown")
|
||||
}
|
||||
for h in result.hotspots[:10]
|
||||
]
|
||||
|
||||
file_data = [
|
||||
{
|
||||
"name": f.get("path", "")[:30],
|
||||
"commits": f.get("total_commits", 0),
|
||||
"authors": f.get("num_authors", 0),
|
||||
"bus_factor": round(f.get("bus_factor", 1), 2),
|
||||
"risk": f.get("risk_level", "unknown")
|
||||
}
|
||||
for f in sorted(
|
||||
result.files,
|
||||
key=lambda x: (
|
||||
{"critical": 0, "high": 1, "medium": 2, "low": 3}.get(
|
||||
x.get("risk_level"), 4
|
||||
),
|
||||
-x.get("bus_factor", 1)
|
||||
)
|
||||
)[:20]
|
||||
]
|
||||
|
||||
return {
|
||||
"risk_distribution": risk_distribution,
|
||||
"top_hotspots": top_hotspots,
|
||||
"file_data": file_data,
|
||||
"summary": {
|
||||
"bus_factor": round(result.overall_bus_factor, 2),
|
||||
"gini": round(result.gini_coefficient, 3),
|
||||
"files": result.files_analyzed,
|
||||
"authors": result.unique_authors
|
||||
}
|
||||
}
|
||||
|
||||
def create_inline_template(self) -> Template:
|
||||
"""Create an inline template for standalone HTML reports.
|
||||
|
||||
Returns:
|
||||
Jinja2 Template with inline CSS/JS.
|
||||
"""
|
||||
template_str = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Repository Health Report</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 10px; margin-bottom: 20px; }
|
||||
.header h1 { font-size: 2em; margin-bottom: 10px; }
|
||||
.meta { opacity: 0.9; font-size: 0.9em; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 20px; }
|
||||
.card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.card h2 { color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
|
||||
.stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
|
||||
.stat:last-child { border-bottom: none; }
|
||||
.stat-label { color: #666; }
|
||||
.stat-value { font-weight: bold; }
|
||||
.badge { padding: 4px 12px; border-radius: 20px; font-size: 0.8em; font-weight: bold; color: white; }
|
||||
.badge-critical { background: #dc3545; }
|
||||
.badge-high { background: #fd7e14; }
|
||||
.badge-medium { background: #ffc107; color: #333; }
|
||||
.badge-low { background: #28a745; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
|
||||
th { background: #f8f9fa; font-weight: 600; }
|
||||
tr:hover { background: #f8f9fa; }
|
||||
.chart-container { position: relative; height: 250px; margin: 20px 0; }
|
||||
.suggestion { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid #667eea; }
|
||||
.suggestion-priority-critical { border-left-color: #dc3545; }
|
||||
.suggestion-priority-high { border-left-color: #fd7e14; }
|
||||
.suggestion-priority-medium { border-left-color: #ffc107; }
|
||||
.progress-bar { background: #e9ecef; border-radius: 10px; overflow: hidden; height: 20px; }
|
||||
.progress-fill { height: 100%; border-radius: 10px; transition: width 0.3s; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Repository Health Report</h1>
|
||||
<p class="meta">{{ result.repository_path }}</p>
|
||||
<p class="meta">Generated: {{ generated_at }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>Summary</h2>
|
||||
<div class="stat"><span class="stat-label">Files Analyzed</span><span class="stat-value">{{ result.files_analyzed }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Total Commits</span><span class="stat-value">{{ result.total_commits }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Unique Authors</span><span class="stat-value">{{ result.unique_authors }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Bus Factor</span><span class="stat-value">{{ "%.2f"|format(result.overall_bus_factor) }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Gini Coefficient</span><span class="stat-value">{{ "%.3f"|format(result.gini_coefficient) }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Risk Distribution</h2>
|
||||
<div class="stat"><span class="stat-label">Critical</span><span class="stat-value"><span class="badge badge-critical">{{ result.risk_summary.get('critical', 0) }}</span></span></div>
|
||||
<div class="stat"><span class="stat-label">High</span><span class="stat-value"><span class="badge badge-high">{{ result.risk_summary.get('high', 0) }}</span></span></div>
|
||||
<div class="stat"><span class="stat-label">Medium</span><span class="stat-value"><span class="badge badge-medium">{{ result.risk_summary.get('medium', 0) }}</span></span></div>
|
||||
<div class="stat"><span class="stat-label">Low</span><span class="stat-value"><span class="badge badge-low">{{ result.risk_summary.get('low', 0) }}</span></span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Risk by Percentage</h2>
|
||||
<p style="margin-bottom: 10px;">Critical: {{ "%.1f"|format(result.risk_summary.get('percentage_critical', 0)) }}%</p>
|
||||
<div class="progress-bar"><div class="progress-fill" style="width: {{ result.risk_summary.get('percentage_critical', 0) }}%; background: #dc3545;"></div></div>
|
||||
<p style="margin: 10px 0 5px;">High: {{ "%.1f"|format(result.risk_summary.get('percentage_high', 0)) }}%</p>
|
||||
<div class="progress-bar"><div class="progress-fill" style="width: {{ result.risk_summary.get('percentage_high', 0) }}%; background: #fd7e14;"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>Risk Distribution Chart</h2>
|
||||
<div class="chart-container">
|
||||
<canvas id="riskChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Top Knowledge Hotspots</h2>
|
||||
<table>
|
||||
<thead><tr><th>File</th><th>Author</th><th>Share</th><th>Risk</th></tr></thead>
|
||||
<tbody>
|
||||
{% for hotspot in result.hotspots[:10] %}
|
||||
<tr>
|
||||
<td>{{ hotspot.file_path[:30] }}</td>
|
||||
<td>{{ hotspot.top_author[:15] }}</td>
|
||||
<td>{{ "%.0f"|format(hotspot.top_author_share * 100) }}%</td>
|
||||
<td><span class="badge badge-{{ hotspot.risk_level }}">{{ hotspot.risk_level }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if result.suggestions %}
|
||||
<div class="card">
|
||||
<h2>Diversification Suggestions</h2>
|
||||
{% for suggestion in result.suggestions %}
|
||||
<div class="suggestion suggestion-priority-{{ suggestion.priority }}">
|
||||
<strong>{{ suggestion.priority|upper }}</strong>: {{ suggestion.action }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<h2>All Analyzed Files</h2>
|
||||
<table>
|
||||
<thead><tr><th>File</th><th>Commits</th><th>Authors</th><th>Bus Factor</th><th>Risk</th></tr></thead>
|
||||
<tbody>
|
||||
{% for file in result.files[:30] %}
|
||||
<tr>
|
||||
<td>{{ file.path[:40] }}</td>
|
||||
<td>{{ file.total_commits }}</td>
|
||||
<td>{{ file.num_authors }}</td>
|
||||
<td>{{ "%.2f"|format(file.bus_factor) }}</td>
|
||||
<td><span class="badge badge-{{ file.risk_level }}">{{ file.risk_level }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const riskData = {
|
||||
labels: ['Critical', 'High', 'Medium', 'Low'],
|
||||
datasets: [{
|
||||
data: [
|
||||
{{ result.risk_summary.get('critical', 0) }},
|
||||
{{ result.risk_summary.get('high', 0) }},
|
||||
{{ result.risk_summary.get('medium', 0) }},
|
||||
{{ result.risk_summary.get('low', 0) }}
|
||||
],
|
||||
backgroundColor: ['#dc3545', '#fd7e14', '#ffc107', '#28a745']
|
||||
}]
|
||||
};
|
||||
|
||||
new Chart(document.getElementById('riskChart'), {
|
||||
type: 'doughnut',
|
||||
data: riskData,
|
||||
options: { responsive: true, maintainAspectRatio: false }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return self.env.from_string(template_str)
|
||||
|
||||
def generate_standalone(self, result: RepositoryResult) -> str:
|
||||
"""Generate standalone HTML with inline resources.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to convert.
|
||||
|
||||
Returns:
|
||||
Complete HTML string.
|
||||
"""
|
||||
template = self.create_inline_template()
|
||||
charts_data = self.generate_charts_data(result)
|
||||
|
||||
return template.render(
|
||||
result=result,
|
||||
generated_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"),
|
||||
charts_data=charts_data
|
||||
)
|
||||
|
||||
def save_standalone(self, result: RepositoryResult, file_path: str) -> None:
|
||||
"""Save standalone HTML to a file.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to save.
|
||||
file_path: Path to output file.
|
||||
"""
|
||||
html_content = self.generate_standalone(result)
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
132
repohealth-cli/src/repohealth/reporters/json_reporter.py
Normal file
132
repohealth-cli/src/repohealth/reporters/json_reporter.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""JSON reporter for machine-readable output."""
|
||||
|
||||
import json
|
||||
|
||||
from repohealth.analyzers.risk_analyzer import DiversificationSuggestion, Hotspot
|
||||
from repohealth.models.file_stats import FileAnalysis
|
||||
from repohealth.models.result import RepositoryResult
|
||||
|
||||
|
||||
class JSONReporter:
|
||||
"""Reporter for JSON output."""
|
||||
|
||||
def __init__(self, indent: int = 2):
|
||||
"""Initialize the reporter.
|
||||
|
||||
Args:
|
||||
indent: JSON indentation level.
|
||||
"""
|
||||
self.indent = indent
|
||||
|
||||
def generate(self, result: RepositoryResult) -> str:
|
||||
"""Generate JSON output from a result.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to convert.
|
||||
|
||||
Returns:
|
||||
JSON string.
|
||||
"""
|
||||
output = {
|
||||
"version": "1.0",
|
||||
"repository": result.repository_path,
|
||||
"analyzed_at": result.analyzed_at.isoformat(),
|
||||
"files_analyzed": result.files_analyzed,
|
||||
"summary": {
|
||||
"files_analyzed": result.files_analyzed,
|
||||
"total_commits": result.total_commits,
|
||||
"unique_authors": result.unique_authors,
|
||||
"overall_bus_factor": round(result.overall_bus_factor, 2),
|
||||
"gini_coefficient": round(result.gini_coefficient, 3),
|
||||
"overall_risk": result.risk_summary.get("overall_risk", "unknown")
|
||||
},
|
||||
"risk_summary": result.risk_summary,
|
||||
"files": result.files,
|
||||
"hotspots": result.hotspots,
|
||||
"suggestions": result.suggestions,
|
||||
"metadata": result.metadata
|
||||
}
|
||||
|
||||
indent = self.indent if self.indent else None
|
||||
return json.dumps(output, indent=indent, default=str)
|
||||
|
||||
def save(self, result: RepositoryResult, file_path: str) -> None:
|
||||
"""Save JSON output to a file.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to save.
|
||||
file_path: Path to output file.
|
||||
"""
|
||||
json_str = self.generate(result)
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(json_str)
|
||||
|
||||
def generate_file_dict(self, analysis: FileAnalysis) -> dict:
|
||||
"""Convert a FileAnalysis to a dictionary.
|
||||
|
||||
Args:
|
||||
analysis: FileAnalysis to convert.
|
||||
|
||||
Returns:
|
||||
Dictionary representation.
|
||||
"""
|
||||
return {
|
||||
"path": analysis.path,
|
||||
"total_commits": analysis.total_commits,
|
||||
"num_authors": analysis.num_authors,
|
||||
"author_commits": analysis.author_commits,
|
||||
"gini_coefficient": round(analysis.gini_coefficient, 3),
|
||||
"bus_factor": round(analysis.bus_factor, 2),
|
||||
"risk_level": analysis.risk_level,
|
||||
"top_author_share": round(analysis.top_author_share, 3),
|
||||
"module": analysis.module,
|
||||
"extension": analysis.extension,
|
||||
"first_commit": (
|
||||
analysis.first_commit.isoformat()
|
||||
if analysis.first_commit else None
|
||||
),
|
||||
"last_commit": (
|
||||
analysis.last_commit.isoformat()
|
||||
if analysis.last_commit else None
|
||||
)
|
||||
}
|
||||
|
||||
def generate_hotspot_dict(self, hotspot: Hotspot) -> dict:
|
||||
"""Convert a Hotspot to a dictionary.
|
||||
|
||||
Args:
|
||||
hotspot: Hotspot to convert.
|
||||
|
||||
Returns:
|
||||
Dictionary representation.
|
||||
"""
|
||||
return {
|
||||
"file_path": hotspot.file_path,
|
||||
"risk_level": hotspot.risk_level,
|
||||
"bus_factor": round(hotspot.bus_factor, 2),
|
||||
"top_author": hotspot.top_author,
|
||||
"top_author_share": round(hotspot.top_author_share, 3),
|
||||
"total_commits": hotspot.total_commits,
|
||||
"num_authors": hotspot.num_authors,
|
||||
"module": hotspot.module,
|
||||
"suggestion": hotspot.suggestion
|
||||
}
|
||||
|
||||
def generate_suggestion_dict(self, suggestion: DiversificationSuggestion) -> dict:
|
||||
"""Convert a DiversificationSuggestion to a dictionary.
|
||||
|
||||
Args:
|
||||
suggestion: Suggestion to convert.
|
||||
|
||||
Returns:
|
||||
Dictionary representation.
|
||||
"""
|
||||
return {
|
||||
"file_path": suggestion.file_path,
|
||||
"current_author": suggestion.current_author,
|
||||
"suggested_authors": suggestion.suggested_authors,
|
||||
"priority": suggestion.priority,
|
||||
"reason": suggestion.reason,
|
||||
"action": suggestion.action
|
||||
}
|
||||
253
repohealth-cli/src/repohealth/reporters/terminal.py
Normal file
253
repohealth-cli/src/repohealth/reporters/terminal.py
Normal file
@@ -0,0 +1,253 @@
|
||||
"""Terminal reporter using Rich library."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from rich.box import ROUNDED
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from repohealth.models.result import RepositoryResult
|
||||
|
||||
|
||||
class TerminalReporter:
|
||||
"""Reporter for terminal output using Rich."""
|
||||
|
||||
RISK_COLORS = {
|
||||
"critical": "red",
|
||||
"high": "orange3",
|
||||
"medium": "yellow",
|
||||
"low": "green",
|
||||
"unknown": "grey"
|
||||
}
|
||||
|
||||
def __init__(self, console: Optional[Console] = None):
|
||||
"""Initialize the reporter.
|
||||
|
||||
Args:
|
||||
console: Rich Console instance.
|
||||
"""
|
||||
self.console = console or Console()
|
||||
|
||||
def display_result(self, result: RepositoryResult) -> None:
|
||||
"""Display a complete analysis result.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to display.
|
||||
"""
|
||||
self.console.print(Panel(
|
||||
self._get_overview_text(result),
|
||||
title="Repository Health Analysis",
|
||||
subtitle=f"Analyzed: {result.analyzed_at.strftime('%Y-%m-%d %H:%M')}",
|
||||
expand=False
|
||||
))
|
||||
|
||||
self._display_risk_summary(result)
|
||||
self._display_file_stats(result)
|
||||
self._display_hotspots(result)
|
||||
self._display_suggestions(result)
|
||||
|
||||
def _get_overview_text(self, result: RepositoryResult) -> Text:
|
||||
"""Get overview text for the result.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to display.
|
||||
|
||||
Returns:
|
||||
Rich Text object.
|
||||
"""
|
||||
text = Text()
|
||||
text.append("Repository: ", style="bold")
|
||||
text.append(f"{result.repository_path}\n")
|
||||
text.append("Files Analyzed: ", style="bold")
|
||||
text.append(f"{result.files_analyzed}\n")
|
||||
text.append("Total Commits: ", style="bold")
|
||||
text.append(f"{result.total_commits}\n")
|
||||
text.append("Unique Authors: ", style="bold")
|
||||
text.append(f"{result.unique_authors}\n")
|
||||
text.append("Overall Bus Factor: ", style="bold")
|
||||
text.append(f"{result.overall_bus_factor:.2f}\n")
|
||||
text.append("Gini Coefficient: ", style="bold")
|
||||
text.append(f"{result.gini_coefficient:.3f}\n")
|
||||
return text
|
||||
|
||||
def _display_risk_summary(self, result: RepositoryResult) -> None:
|
||||
"""Display risk summary.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to display.
|
||||
"""
|
||||
summary = result.risk_summary
|
||||
if not summary:
|
||||
return
|
||||
|
||||
table = Table(title="Risk Summary", box=ROUNDED)
|
||||
table.add_column("Risk Level", justify="center")
|
||||
table.add_column("Count", justify="center")
|
||||
table.add_column("Percentage", justify="center")
|
||||
|
||||
levels = ["critical", "high", "medium", "low"]
|
||||
for level in levels:
|
||||
count = summary.get(level, 0)
|
||||
pct = summary.get(f"percentage_{level}", 0)
|
||||
color = self.RISK_COLORS.get(level, "grey")
|
||||
table.add_row(
|
||||
f"[{color}]{level.upper()}[/]",
|
||||
str(count),
|
||||
f"{pct:.1f}%"
|
||||
)
|
||||
|
||||
self.console.print(Panel(table, title="Risk Overview", expand=False))
|
||||
|
||||
def _display_file_stats(self, result: RepositoryResult) -> None:
|
||||
"""Display file statistics table.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to display.
|
||||
"""
|
||||
if not result.files:
|
||||
return
|
||||
|
||||
table = Table(title="Top Files by Risk", box=ROUNDED)
|
||||
table.add_column("File", style="dim", width=40)
|
||||
table.add_column("Commits", justify="right")
|
||||
table.add_column("Authors", justify="right")
|
||||
table.add_column("Bus Factor", justify="right")
|
||||
table.add_column("Risk", justify="center")
|
||||
table.add_column("Top Author %", justify="right")
|
||||
|
||||
sorted_files = sorted(
|
||||
result.files,
|
||||
key=lambda x: (
|
||||
{"critical": 0, "high": 1, "medium": 2, "low": 3}.get(x.get("risk_level"), 4),
|
||||
-x.get("bus_factor", 1)
|
||||
)
|
||||
)[:15]
|
||||
|
||||
for file_data in sorted_files:
|
||||
risk_level = file_data.get("risk_level", "unknown")
|
||||
color = self.RISK_COLORS.get(risk_level, "grey")
|
||||
|
||||
table.add_row(
|
||||
file_data.get("path", "")[:40],
|
||||
str(file_data.get("total_commits", 0)),
|
||||
str(file_data.get("num_authors", 0)),
|
||||
f"{file_data.get('bus_factor', 1):.2f}",
|
||||
f"[{color}]{risk_level.upper()}[/]",
|
||||
f"{file_data.get('top_author_share', 0):.0%}"
|
||||
)
|
||||
|
||||
self.console.print(Panel(table, title="File Analysis", expand=False))
|
||||
|
||||
def _display_hotspots(self, result: RepositoryResult) -> None:
|
||||
"""Display knowledge hotspots.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to display.
|
||||
"""
|
||||
if not result.hotspots:
|
||||
return
|
||||
|
||||
table = Table(title="Knowledge Hotspots", box=ROUNDED)
|
||||
table.add_column("File", style="dim", width=35)
|
||||
table.add_column("Top Author", width=20)
|
||||
table.add_column("Ownership", justify="right")
|
||||
table.add_column("Bus Factor", justify="right")
|
||||
table.add_column("Risk", justify="center")
|
||||
|
||||
for hotspot in result.hotspots[:10]:
|
||||
color = self.RISK_COLORS.get(hotspot.risk_level, "grey")
|
||||
table.add_row(
|
||||
hotspot.file_path[:35],
|
||||
hotspot.top_author[:20],
|
||||
f"{hotspot.top_author_share:.0%}",
|
||||
f"{hotspot.bus_factor:.2f}",
|
||||
f"[{color}]{hotspot.risk_level.upper()}[/]"
|
||||
)
|
||||
|
||||
self.console.print(Panel(table, title="Hotspots", expand=False))
|
||||
|
||||
def _display_suggestions(self, result: RepositoryResult) -> None:
|
||||
"""Display diversification suggestions.
|
||||
|
||||
Args:
|
||||
result: RepositoryResult to display.
|
||||
"""
|
||||
if not result.suggestions:
|
||||
return
|
||||
|
||||
table = Table(title="Diversification Suggestions", box=ROUNDED)
|
||||
table.add_column("Priority", width=10)
|
||||
table.add_column("File", style="dim", width=30)
|
||||
table.add_column("Action", width=40)
|
||||
|
||||
priority_colors = {
|
||||
"critical": "red",
|
||||
"high": "orange3",
|
||||
"medium": "yellow"
|
||||
}
|
||||
|
||||
for suggestion in result.suggestions[:10]:
|
||||
color = priority_colors.get(suggestion.priority, "grey")
|
||||
table.add_row(
|
||||
f"[{color}]{suggestion.priority.upper()}[/]",
|
||||
suggestion.file_path[:30],
|
||||
suggestion.action[:40]
|
||||
)
|
||||
|
||||
self.console.print(Panel(table, title="Suggestions", expand=False))
|
||||
|
||||
def display_progress(self, message: str) -> Progress:
|
||||
"""Display a progress indicator.
|
||||
|
||||
Args:
|
||||
message: Progress message.
|
||||
|
||||
Returns:
|
||||
Progress instance for updating.
|
||||
"""
|
||||
return Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TaskProgressColumn(),
|
||||
console=self.console
|
||||
)
|
||||
|
||||
def display_error(self, message: str) -> None:
|
||||
"""Display an error message.
|
||||
|
||||
Args:
|
||||
message: Error message to display.
|
||||
"""
|
||||
self.console.print(Panel(
|
||||
Text(message, style="red"),
|
||||
title="Error",
|
||||
expand=False
|
||||
))
|
||||
|
||||
def display_warning(self, message: str) -> None:
|
||||
"""Display a warning message.
|
||||
|
||||
Args:
|
||||
message: Warning message to display.
|
||||
"""
|
||||
self.console.print(Panel(
|
||||
Text(message, style="yellow"),
|
||||
title="Warning",
|
||||
expand=False
|
||||
))
|
||||
|
||||
def display_info(self, message: str) -> None:
|
||||
"""Display an info message.
|
||||
|
||||
Args:
|
||||
message: Info message to display.
|
||||
"""
|
||||
self.console.print(Panel(
|
||||
Text(message, style="blue"),
|
||||
title="Info",
|
||||
expand=False
|
||||
))
|
||||
Reference in New Issue
Block a user