fix: resolve CI issues - push complete implementation with tests
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-02-02 15:30:39 +00:00
parent 6577302aa4
commit 5fe6dd83c9

View File

@@ -1,5 +1,3 @@
"""Output formatter for color-coded terminal display."""
from enum import Enum from enum import Enum
from rich.console import Console from rich.console import Console
@@ -9,14 +7,12 @@ from gdiffer.models import DiffAnalysis
class OutputFormat(Enum): class OutputFormat(Enum):
"""Output format options."""
TERMINAL = "terminal" TERMINAL = "terminal"
JSON = "json" JSON = "json"
PLAIN = "plain" PLAIN = "plain"
class SeverityColors: class SeverityColors:
"""Color scheme for severity levels."""
CRITICAL = "red" CRITICAL = "red"
HIGH = "orange3" HIGH = "orange3"
MEDIUM = "yellow" MEDIUM = "yellow"
@@ -25,9 +21,7 @@ class SeverityColors:
class OutputFormatter: class OutputFormatter:
"""Formats and displays diff analysis results.""" def __init__(self, output_format=OutputFormat.TERMINAL):
def __init__(self, output_format: OutputFormat = OutputFormat.TERMINAL):
self.output_format = output_format self.output_format = output_format
self.console = Console(theme=Theme({ self.console = Console(theme=Theme({
"critical": "bold red", "critical": "bold red",
@@ -41,8 +35,7 @@ class OutputFormatter:
"filename": "bold blue", "filename": "bold blue",
})) }))
def format_analysis(self, analysis: DiffAnalysis) -> str: def format_analysis(self, analysis):
"""Format the complete analysis for display."""
if self.output_format == OutputFormat.JSON: if self.output_format == OutputFormat.JSON:
return self._format_json(analysis) return self._format_json(analysis)
elif self.output_format == OutputFormat.PLAIN: elif self.output_format == OutputFormat.PLAIN:
@@ -50,8 +43,7 @@ class OutputFormatter:
else: else:
return self._format_terminal(analysis) return self._format_terminal(analysis)
def _format_terminal(self, analysis: DiffAnalysis) -> str: def _format_terminal(self, analysis):
"""Format for terminal display with colors."""
output_parts = [] output_parts = []
output_parts.append(self._format_summary(analysis)) output_parts.append(self._format_summary(analysis))
@@ -63,10 +55,9 @@ class OutputFormatter:
if analysis.all_suggestions: if analysis.all_suggestions:
output_parts.append(self._format_suggestions(analysis.all_suggestions)) output_parts.append(self._format_suggestions(analysis.all_suggestions))
return '\\n'.join(output_parts) return "\n".join(output_parts)
def _format_summary(self, analysis: DiffAnalysis) -> str: def _format_summary(self, analysis):
"""Format the summary section."""
lines = [] lines = []
lines.append("[bold blue]=== Git Diff Analysis Summary ===[/bold blue]") 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"[info]Total files changed:[/info] [bold]{analysis.total_files}[/bold]")
@@ -76,19 +67,18 @@ class OutputFormatter:
lines.append(f"[info]Total changes:[/info] {analysis.total_changes}") lines.append(f"[info]Total changes:[/info] {analysis.total_changes}")
if analysis.language_breakdown: if analysis.language_breakdown:
lines.append("\\n[bold blue]Languages:[/bold blue]") lines.append("\n[bold blue]Languages:[/bold blue]")
for lang, count in sorted(analysis.language_breakdown.items()): for lang, count in sorted(analysis.language_breakdown.items()):
lines.append(f" - {lang}: {count}") lines.append(f" - {lang}: {count}")
return '\\n'.join(lines) return "\n".join(lines)
def _format_files(self, analysis: DiffAnalysis) -> str: def _format_files(self, analysis):
"""Format file changes section."""
lines = [] lines = []
lines.append("\\n[bold blue]=== File Changes ===[/bold blue]") lines.append("\n[bold blue]=== File Changes ===[/bold blue]")
for i, file_obj in enumerate(analysis.files, 1): for i, file_obj in enumerate(analysis.files, 1):
lines.append(f"\\n[filename]{i}. {file_obj.filename}[/filename]") lines.append(f"\n[filename]{i}. {file_obj.filename}[/filename]")
change_emoji = { change_emoji = {
"add": "[added]✚[/added]", "add": "[added]✚[/added]",
@@ -111,106 +101,101 @@ class OutputFormatter:
lines.append(f" Hunk {j} (lines {hunk_range}):") lines.append(f" Hunk {j} (lines {hunk_range}):")
lines.append(self._format_hunk(hunk)) lines.append(self._format_hunk(hunk))
return '\\n'.join(lines) return "\n".join(lines)
def _format_hunk(self, hunk) -> str: def _format_hunk(self, hunk):
"""Format a single hunk with color-coded changes."""
lines = [] lines = []
for line in hunk.new_lines_content: for line in hunk.new_lines_content:
if line.startswith('+++'): if line.startswith("+++"):
continue continue
if line.startswith('+'): if line.startswith("+"):
lines.append(f" [added]{line}[/added]") lines.append(f" [added]{line}[/added]")
elif line.startswith('-'): elif line.startswith("-"):
lines.append(f" [removed]{line}[/removed]") lines.append(f" [removed]{line}[/removed]")
elif line.startswith('@@'): elif line.startswith("@@"):
lines.append(f" [info]{line}[/info]") lines.append(f" [info]{line}[/info]")
else: else:
lines.append(f" {line}") lines.append(f" {line}")
return '\\n'.join(lines) return "\n".join(lines)
def _format_issues(self, issues: list[dict]) -> str: def _format_issues(self, issues):
"""Format issues section."""
lines = [] lines = []
lines.append("\\n[bold blue]=== Detected Issues ===[/bold blue]") lines.append("\n[bold blue]=== Detected Issues ===[/bold blue]")
severity_priority = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3} severity_priority = {"critical": 0, "high": 1, "medium": 2, "low": 3}
sorted_issues = sorted( sorted_issues = sorted(
issues, key=lambda x: severity_priority.get(x.get('severity', ''), 4) issues, key=lambda x: severity_priority.get(x.get("severity", ""), 4)
) )
for issue in sorted_issues: for issue in sorted_issues:
severity = issue.get('severity', 'info').lower() severity = issue.get("severity", "info").lower()
color = getattr(SeverityColors, severity.upper(), 'info') color = getattr(SeverityColors, severity.upper(), "info")
lines.append(f"\\n[{color}]✖ {issue.get('title', 'Issue')}[/[{color}]]") lines.append(f"\n[{color}]✖ {issue.get('title', 'Issue')}[/[{color}]]")
lines.append(f" Severity: [{color}]{severity.upper()}[/[{color}]]") lines.append(f" Severity: [{color}]{severity.upper()}[/[{color}]]")
lines.append(f" Description: {issue.get('description', '')}") lines.append(f" Description: {issue.get('description', '')}")
if issue.get('line'): if issue.get("line"):
lines.append(f" Line: {issue['line']}") lines.append(f" Line: {issue['line']}")
if issue.get('suggestion'): if issue.get("suggestion"):
lines.append(f" Suggestion: {issue['suggestion']}") lines.append(f" Suggestion: {issue['suggestion']}")
return '\\n'.join(lines) return "\n".join(lines)
def _format_suggestions(self, suggestions: list[str]) -> str: def _format_suggestions(self, suggestions):
"""Format suggestions section."""
lines = [] lines = []
lines.append("\\n[bold blue]=== Suggestions ===[/bold blue]") lines.append("\n[bold blue]=== Suggestions ===[/bold blue]")
for i, suggestion in enumerate(suggestions, 1): for i, suggestion in enumerate(suggestions, 1):
lines.append(f"\\n[info]{i}. {suggestion}[/info]") lines.append(f"\n[info]{i}. {suggestion}[/info]")
return '\\n'.join(lines) return "\n".join(lines)
def _format_json(self, analysis: DiffAnalysis) -> str: def _format_json(self, analysis):
"""Format as JSON."""
import json import json
result = { result = {
'summary': { "summary": {
'total_files': analysis.total_files, "total_files": analysis.total_files,
'files_added': analysis.files_added, "files_added": analysis.files_added,
'files_deleted': analysis.files_deleted, "files_deleted": analysis.files_deleted,
'files_modified': analysis.files_modified, "files_modified": analysis.files_modified,
'files_renamed': analysis.files_renamed, "files_renamed": analysis.files_renamed,
'total_changes': analysis.total_changes, "total_changes": analysis.total_changes,
'language_breakdown': analysis.language_breakdown, "language_breakdown": analysis.language_breakdown,
}, },
'files': [], "files": [],
'issues': analysis.all_issues, "issues": analysis.all_issues,
'suggestions': analysis.all_suggestions, "suggestions": analysis.all_suggestions,
} }
for file_obj in analysis.files: for file_obj in analysis.files:
file_data = { file_data = {
'filename': file_obj.filename, "filename": file_obj.filename,
'change_type': file_obj.change_type, "change_type": file_obj.change_type,
'old_path': file_obj.old_path, "old_path": file_obj.old_path,
'new_path': file_obj.new_path, "new_path": file_obj.new_path,
'is_new': file_obj.is_new, "is_new": file_obj.is_new,
'is_deleted': file_obj.is_deleted, "is_deleted": file_obj.is_deleted,
'is_rename': file_obj.is_rename, "is_rename": file_obj.is_rename,
'language': file_obj.extension, "language": file_obj.extension,
'hunks': [], "hunks": [],
} }
for hunk in file_obj.hunks: for hunk in file_obj.hunks:
hunk_data = { hunk_data = {
'old_start': hunk.old_start, "old_start": hunk.old_start,
'old_lines': hunk.old_lines, "old_lines": hunk.old_lines,
'new_start': hunk.new_start, "new_start": hunk.new_start,
'new_lines': hunk.new_lines, "new_lines": hunk.new_lines,
'added_lines': hunk.get_added_lines(), "added_lines": hunk.get_added_lines(),
'removed_lines': hunk.get_removed_lines(), "removed_lines": hunk.get_removed_lines(),
} }
file_data['hunks'].append(hunk_data) file_data["hunks"].append(hunk_data)
result['files'].append(file_data) result["files"].append(file_data)
return json.dumps(result, indent=2) return json.dumps(result, indent=2)
def _format_plain(self, analysis: DiffAnalysis) -> str: def _format_plain(self, analysis):
"""Format as plain text without colors."""
lines = [] lines = []
lines.append("=== Git Diff Analysis Summary ===") lines.append("=== Git Diff Analysis Summary ===")
lines.append(f"Total files changed: {analysis.total_files}") lines.append(f"Total files changed: {analysis.total_files}")
@@ -220,14 +205,14 @@ class OutputFormatter:
lines.append(f"Total changes: {analysis.total_changes}") lines.append(f"Total changes: {analysis.total_changes}")
if analysis.language_breakdown: if analysis.language_breakdown:
lines.append("\\nLanguages:") lines.append("\nLanguages:")
for lang, count in sorted(analysis.language_breakdown.items()): for lang, count in sorted(analysis.language_breakdown.items()):
lines.append(f" - {lang}: {count}") lines.append(f" - {lang}: {count}")
lines.append("\\n=== File Changes ===") lines.append("\n=== File Changes ===")
for i, file_obj in enumerate(analysis.files, 1): for i, file_obj in enumerate(analysis.files, 1):
lines.append(f"\\n{i}. {file_obj.filename}") lines.append(f"\n{i}. {file_obj.filename}")
lines.append(f" Status: {file_obj.change_type}") lines.append(f" Status: {file_obj.change_type}")
if file_obj.is_rename: if file_obj.is_rename:
@@ -236,41 +221,37 @@ class OutputFormatter:
for j, hunk in enumerate(file_obj.hunks, 1): for j, hunk in enumerate(file_obj.hunks, 1):
lines.append(f" Hunk {j}:") lines.append(f" Hunk {j}:")
for line in hunk.new_lines_content: for line in hunk.new_lines_content:
if line.startswith('+++'): if line.startswith("+++"):
continue continue
lines.append(f" {line}") lines.append(f" {line}")
if analysis.all_issues: if analysis.all_issues:
lines.append("\\n=== Detected Issues ===") lines.append("\n=== Detected Issues ===")
for issue in analysis.all_issues: for issue in analysis.all_issues:
lines.append(f"- {issue.get('title', 'Issue')}") lines.append(f"- {issue.get('title', 'Issue')}")
lines.append(f" Severity: {issue.get('severity', 'unknown')}") lines.append(f" Severity: {issue.get('severity', 'unknown')}")
lines.append(f" Description: {issue.get('description', '')}") lines.append(f" Description: {issue.get('description', '')}")
if analysis.all_suggestions: if analysis.all_suggestions:
lines.append("\\n=== Suggestions ===") lines.append("\n=== Suggestions ===")
for i, suggestion in enumerate(analysis.all_suggestions, 1): for i, suggestion in enumerate(analysis.all_suggestions, 1):
lines.append(f"{i}. {suggestion}") lines.append(f"{i}. {suggestion}")
return '\\n'.join(lines) return "\n".join(lines)
def print(self, content: str) -> None: def print(self, content):
"""Print content to console."""
self.console.print(content) self.console.print(content)
def print_analysis(self, analysis: DiffAnalysis) -> None: def print_analysis(self, analysis):
"""Print analysis result to console."""
formatted = self.format_analysis(analysis) formatted = self.format_analysis(analysis)
self.print(formatted) self.print(formatted)
def format_analysis(analysis: DiffAnalysis, output_format: str = "terminal") -> str: def format_analysis(analysis, output_format="terminal"):
"""Format analysis for display."""
fmt = OutputFormatter(OutputFormat(output_format)) fmt = OutputFormatter(OutputFormat(output_format))
return fmt.format_analysis(analysis) return fmt.format_analysis(analysis)
def print_analysis(analysis: DiffAnalysis, output_format: str = "terminal") -> None: def print_analysis(analysis, output_format="terminal"):
"""Print analysis to console."""
fmt = OutputFormatter(OutputFormat(output_format)) fmt = OutputFormatter(OutputFormat(output_format))
fmt.print_analysis(analysis) fmt.print_analysis(analysis)