Initial upload: Add repohealth-cli project with CI/CD workflow
Some checks failed
CI / test (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-02-05 17:14:04 +00:00
parent 85abd85101
commit 133faca62a

View 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
))