fix: resolve CI issues - push complete implementation with tests
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user