Add CLI and output formatter modules

This commit is contained in:
2026-02-02 13:58:00 +00:00
parent 0f9efa77d6
commit 0e4182c504

256
src/gdiffer/output.py Normal file
View File

@@ -0,0 +1,256 @@
"""Output formatter for color-coded terminal display."""
from enum import Enum
from rich.console import Console
from rich.theme import Theme
from gdiffer.models import DiffAnalysis
class OutputFormat(Enum):
TERMINAL = "terminal"
JSON = "json"
PLAIN = "plain"
class SeverityColors:
CRITICAL = "red"
HIGH = "orange3"
MEDIUM = "yellow"
LOW = "cyan"
INFO = "blue"
class OutputFormatter:
def __init__(self, output_format: OutputFormat = OutputFormat.TERMINAL):
self.output_format = output_format
self.console = Console(theme=Theme({
"critical": "bold red",
"high": "orange3",
"medium": "yellow",
"low": "cyan",
"info": "blue",
"added": "green",
"removed": "red",
"modified": "yellow",
"filename": "bold blue",
}))
def format_analysis(self, analysis: DiffAnalysis) -> str:
if self.output_format == OutputFormat.JSON:
return self._format_json(analysis)
elif self.output_format == OutputFormat.PLAIN:
return self._format_plain(analysis)
else:
return self._format_terminal(analysis)
def _format_terminal(self, analysis: DiffAnalysis) -> str:
output_parts = []
output_parts.append(self._format_summary(analysis))
output_parts.append(self._format_files(analysis))
if analysis.all_issues:
output_parts.append(self._format_issues(analysis.all_issues))
if analysis.all_suggestions:
output_parts.append(self._format_suggestions(analysis.all_suggestions))
return '\n'.join(output_parts)
def _format_summary(self, analysis: DiffAnalysis) -> str:
lines = []
lines.append("[bold blue]=== Git Diff Analysis Summary ===[/bold blue]")
lines.append(f"[info]Total files changed:[/info] [bold]{analysis.total_files}[/bold]")
lines.append(f"[added]Files added:[/added] {analysis.files_added}")
lines.append(f"[removed]Files deleted:[/removed] {analysis.files_deleted}")
lines.append(f"[modified]Files modified:[/modified] {analysis.files_modified}")
lines.append(f"[info]Total changes:[/info] {analysis.total_changes}")
if analysis.language_breakdown:
lines.append("\n[bold blue]Languages:[/bold blue]")
for lang, count in sorted(analysis.language_breakdown.items()):
lines.append(f" - {lang}: {count}")
return '\n'.join(lines)
def _format_files(self, analysis: DiffAnalysis) -> str:
lines = []
lines.append("\n[bold blue]=== File Changes ===[/bold blue]")
for i, file_obj in enumerate(analysis.files, 1):
lines.append(f"\n[filename]{i}. {file_obj.filename}[/filename]")
change_emoji = {
"add": "[added][✚][/added]",
"delete": "[removed][✖][/removed]",
"rename": "[info][↪][/info]",
"modify": "[modified][✎][/modified]",
}
change_label = change_emoji.get(file_obj.change_type, "")
lines.append(f" Status: {change_label} {file_obj.change_type}")
if file_obj.is_rename:
lines.append(f" Renamed from: {file_obj.rename_from}")
if file_obj.hunks:
total_changes = sum(h.new_lines for h in file_obj.hunks)
lines.append(f" Changes: {total_changes} lines")
for j, hunk in enumerate(file_obj.hunks, 1):
lines.append(f" Hunk {j} (lines {hunk.old_start}-{hunk.old_start + hunk.old_lines}):")
lines.append(self._format_hunk(hunk))
return '\n'.join(lines)
def _format_hunk(self, hunk) -> str:
lines = []
for line in hunk.new_lines_content:
if line.startswith('+++'):
continue
if line.startswith('+'):
lines.append(f" [added]{line}[/added]")
elif line.startswith('-'):
lines.append(f" [removed]{line}[/removed]")
elif line.startswith('@@'):
lines.append(f" [info]{line}[/info]")
else:
lines.append(f" {line}")
return '\n'.join(lines)
def _format_issues(self, issues: list[dict]) -> str:
lines = []
lines.append("\n[bold blue]=== Detected Issues ===[/bold blue]")
severity_priority = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3}
sorted_issues = sorted(issues, key=lambda x: severity_priority.get(x.get('severity', ''), 4))
for issue in sorted_issues:
severity = issue.get('severity', 'info').lower()
color = getattr(SeverityColors, severity.upper(), 'info')
lines.append(f"\n[{color}][✖] {issue.get('title', 'Issue')}[/]")
lines.append(f" Severity: [{color}]{severity.upper()}[/]")
lines.append(f" Description: {issue.get('description', '')}")
if issue.get('line'):
lines.append(f" Line: {issue['line']}")
if issue.get('suggestion'):
lines.append(f" Suggestion: {issue['suggestion']}")
return '\n'.join(lines)
def _format_suggestions(self, suggestions: list[str]) -> str:
lines = []
lines.append("\n[bold blue]=== Suggestions ===[/bold blue]")
for i, suggestion in enumerate(suggestions, 1):
lines.append(f"\n[info]{i}. {suggestion}[/info]")
return '\n'.join(lines)
def _format_json(self, analysis: DiffAnalysis) -> str:
import json
result = {
'summary': {
'total_files': analysis.total_files,
'files_added': analysis.files_added,
'files_deleted': analysis.files_deleted,
'files_modified': analysis.files_modified,
'files_renamed': analysis.files_renamed,
'total_changes': analysis.total_changes,
'language_breakdown': analysis.language_breakdown,
},
'files': [],
'issues': analysis.all_issues,
'suggestions': analysis.all_suggestions,
}
for file_obj in analysis.files:
file_data = {
'filename': file_obj.filename,
'change_type': file_obj.change_type,
'old_path': file_obj.old_path,
'new_path': file_obj.new_path,
'is_new': file_obj.is_new,
'is_deleted': file_obj.is_deleted,
'is_rename': file_obj.is_rename,
'language': file_obj.extension,
'hunks': [],
}
for hunk in file_obj.hunks:
hunk_data = {
'old_start': hunk.old_start,
'old_lines': hunk.old_lines,
'new_start': hunk.new_start,
'new_lines': hunk.new_lines,
'added_lines': hunk.get_added_lines(),
'removed_lines': hunk.get_removed_lines(),
}
file_data['hunks'].append(hunk_data)
result['files'].append(file_data)
return json.dumps(result, indent=2)
def _format_plain(self, analysis: DiffAnalysis) -> str:
lines = []
lines.append("=== Git Diff Analysis Summary ===")
lines.append(f"Total files changed: {analysis.total_files}")
lines.append(f"Files added: {analysis.files_added}")
lines.append(f"Files deleted: {analysis.files_deleted}")
lines.append(f"Files modified: {analysis.files_modified}")
lines.append(f"Total changes: {analysis.total_changes}")
if analysis.language_breakdown:
lines.append("\nLanguages:")
for lang, count in sorted(analysis.language_breakdown.items()):
lines.append(f" - {lang}: {count}")
lines.append("\n=== File Changes ===")
for i, file_obj in enumerate(analysis.files, 1):
lines.append(f"\n{i}. {file_obj.filename}")
lines.append(f" Status: {file_obj.change_type}")
if file_obj.is_rename:
lines.append(f" Renamed from: {file_obj.rename_from}")
for j, hunk in enumerate(file_obj.hunks, 1):
lines.append(f" Hunk {j}:")
for line in hunk.new_lines_content:
if line.startswith('+++'):
continue
lines.append(f" {line}")
if analysis.all_issues:
lines.append("\n=== Detected Issues ===")
for issue in analysis.all_issues:
lines.append(f"- {issue.get('title', 'Issue')}")
lines.append(f" Severity: {issue.get('severity', 'unknown')}")
lines.append(f" Description: {issue.get('description', '')}")
if analysis.all_suggestions:
lines.append("\n=== Suggestions ===")
for i, suggestion in enumerate(analysis.all_suggestions, 1):
lines.append(f"{i}. {suggestion}")
return '\n'.join(lines)
def print(self, content: str) -> None:
self.console.print(content)
def print_analysis(self, analysis: DiffAnalysis) -> None:
formatted = self.format_analysis(analysis)
self.print(formatted)
def format_analysis(analysis: DiffAnalysis, output_format: str = "terminal") -> str:
fmt = OutputFormatter(OutputFormat(output_format))
return fmt.format_analysis(analysis)
def print_analysis(analysis: DiffAnalysis, output_format: str = "terminal") -> None:
fmt = OutputFormatter(OutputFormat(output_format))
fmt.print_analysis(analysis)