Initial upload: Add repohealth-cli project with CI/CD workflow
This commit is contained in:
258
src/repohealth/reporters/terminal.py
Normal file
258
src/repohealth/reporters/terminal.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""Terminal reporter using Rich library."""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich.style import Style
|
||||
from rich.align import Align
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
||||
from rich import box
|
||||
|
||||
from repohealth.models.file_stats import FileAnalysis
|
||||
from repohealth.models.result import RepositoryResult
|
||||
from repohealth.analyzers.risk_analyzer import Hotspot, DiversificationSuggestion
|
||||
|
||||
|
||||
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(f"Repository: ", style="bold")
|
||||
text.append(f"{result.repository_path}\n")
|
||||
text.append(f"Files Analyzed: ", style="bold")
|
||||
text.append(f"{result.files_analyzed}\n")
|
||||
text.append(f"Total Commits: ", style="bold")
|
||||
text.append(f"{result.total_commits}\n")
|
||||
text.append(f"Unique Authors: ", style="bold")
|
||||
text.append(f"{result.unique_authors}\n")
|
||||
text.append(f"Overall Bus Factor: ", style="bold")
|
||||
text.append(f"{result.overall_bus_factor:.2f}\n")
|
||||
text.append(f"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=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=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=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=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