fix: resolve CI linting errors - remove unused imports and update type annotations
This commit is contained in:
@@ -1 +1,312 @@
|
|||||||
# src/gdiffer/cli.py
|
"""CLI interface for git diff explainer."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from gdiffer import __version__
|
||||||
|
from gdiffer.code_analyzer import CodeAnalyzer
|
||||||
|
from gdiffer.issue_detector import IssueDetector
|
||||||
|
from gdiffer.language_detector import LanguageDetector
|
||||||
|
from gdiffer.models import DiffAnalysis, DiffFile
|
||||||
|
from gdiffer.output import OutputFormat, OutputFormatter
|
||||||
|
from gdiffer.parser import parse_diff
|
||||||
|
|
||||||
|
|
||||||
|
def create_analysis(files: list[DiffFile], verbose: bool = False) -> DiffAnalysis:
|
||||||
|
"""Create a complete analysis from parsed diff files."""
|
||||||
|
analysis = DiffAnalysis()
|
||||||
|
language_detector = LanguageDetector()
|
||||||
|
code_analyzer = CodeAnalyzer()
|
||||||
|
issue_detector = IssueDetector()
|
||||||
|
|
||||||
|
for file_obj in files:
|
||||||
|
analysis.files.append(file_obj)
|
||||||
|
|
||||||
|
if file_obj.change_type == "add":
|
||||||
|
analysis.files_added += 1
|
||||||
|
elif file_obj.change_type == "delete":
|
||||||
|
analysis.files_deleted += 1
|
||||||
|
elif file_obj.change_type == "rename":
|
||||||
|
analysis.files_renamed += 1
|
||||||
|
else:
|
||||||
|
analysis.files_modified += 1
|
||||||
|
|
||||||
|
lang = language_detector.detect(file_obj.filename)
|
||||||
|
if lang != "text":
|
||||||
|
analysis.language_breakdown[lang] = analysis.language_breakdown.get(lang, 0) + 1
|
||||||
|
|
||||||
|
for hunk in file_obj.hunks:
|
||||||
|
old_code = "\n".join(hunk.old_lines_content)
|
||||||
|
new_code = "\n".join(hunk.new_lines_content)
|
||||||
|
|
||||||
|
code_analyzer.summarize_change(old_code, new_code, lang)
|
||||||
|
|
||||||
|
issues = issue_detector.detect_diff_issues(old_code, new_code, lang)
|
||||||
|
for issue in issues:
|
||||||
|
issue_dict = {
|
||||||
|
"type": issue.type,
|
||||||
|
"severity": issue.severity,
|
||||||
|
"title": issue.title,
|
||||||
|
"description": issue.description,
|
||||||
|
"line": issue.line,
|
||||||
|
"suggestion": issue.suggestion,
|
||||||
|
"file": file_obj.filename,
|
||||||
|
}
|
||||||
|
analysis.all_issues.append(issue_dict)
|
||||||
|
|
||||||
|
suggestions = issue_detector.suggest_improvements(new_code, lang)
|
||||||
|
analysis.all_suggestions.extend(suggestions)
|
||||||
|
|
||||||
|
analysis.total_changes += hunk.new_lines
|
||||||
|
|
||||||
|
analysis.total_files = len(files)
|
||||||
|
|
||||||
|
return analysis
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.version_option(version=__version__)
|
||||||
|
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
||||||
|
@click.option(
|
||||||
|
"--output",
|
||||||
|
"-o",
|
||||||
|
type=click.Choice(["terminal", "json", "plain"]),
|
||||||
|
default="terminal",
|
||||||
|
help="Output format",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def main(ctx: click.Context, verbose: bool, output: str):
|
||||||
|
"""A CLI tool that parses git diffs and provides intelligent explanations."""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj["verbose"] = verbose
|
||||||
|
ctx.obj["output"] = output
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.argument("diff_input", type=click.STRING, required=False)
|
||||||
|
@click.option("--file", "-f", type=click.Path(exists=True), help="Read diff from file")
|
||||||
|
@click.option("--stdin", "-s", is_flag=True, help="Read diff from stdin")
|
||||||
|
@click.pass_context
|
||||||
|
def explain(ctx: click.Context, diff_input: str | None, file: str | None, stdin: bool):
|
||||||
|
"""Explain git diff changes with intelligent analysis."""
|
||||||
|
verbose = ctx.obj.get("verbose", False)
|
||||||
|
output_format = ctx.obj.get("output", "terminal")
|
||||||
|
|
||||||
|
diff_content = ""
|
||||||
|
|
||||||
|
if stdin:
|
||||||
|
diff_content = sys.stdin.read()
|
||||||
|
elif file:
|
||||||
|
with open(file) as f:
|
||||||
|
diff_content = f.read()
|
||||||
|
elif diff_input:
|
||||||
|
diff_content = diff_input
|
||||||
|
else:
|
||||||
|
click.echo("No diff provided. Use --stdin, --file, or pass diff as argument.", err=True)
|
||||||
|
click.echo("\nUsage examples:")
|
||||||
|
click.echo(" gdiffer explain 'diff --git a/file.py...'", err=True)
|
||||||
|
click.echo(" git diff | gdiffer explain --stdin", err=True)
|
||||||
|
click.echo(" gdiffer explain --file changes.diff", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
files = parse_diff(diff_content)
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
click.echo("No valid diff files found. Please provide a valid git diff.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
analysis = create_analysis(files, verbose)
|
||||||
|
|
||||||
|
if output_format == "json":
|
||||||
|
result = format_analysis_json(analysis)
|
||||||
|
click.echo(result)
|
||||||
|
else:
|
||||||
|
formatter = OutputFormatter(OutputFormat(output_format))
|
||||||
|
formatter.print_analysis(analysis)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"Error analyzing diff: {e}", err=True)
|
||||||
|
if verbose:
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option("--file", "-f", type=click.Path(exists=True), help="Read diff from file")
|
||||||
|
@click.option("--stdin", "-s", is_flag=True, help="Read diff from stdin")
|
||||||
|
@click.pass_context
|
||||||
|
def issues(ctx: click.Context, file: str | None, stdin: bool):
|
||||||
|
"""Show only detected issues and security concerns."""
|
||||||
|
diff_content = ""
|
||||||
|
|
||||||
|
if stdin:
|
||||||
|
diff_content = sys.stdin.read()
|
||||||
|
elif file:
|
||||||
|
with open(file) as f:
|
||||||
|
diff_content = f.read()
|
||||||
|
else:
|
||||||
|
diff_content = sys.stdin.read()
|
||||||
|
|
||||||
|
if not diff_content.strip():
|
||||||
|
click.echo("No diff provided.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
files = parse_diff(diff_content)
|
||||||
|
|
||||||
|
issue_detector = IssueDetector()
|
||||||
|
all_issues = []
|
||||||
|
|
||||||
|
for file_obj in files:
|
||||||
|
for hunk in file_obj.hunks:
|
||||||
|
old_code = "\n".join(hunk.old_lines_content)
|
||||||
|
new_code = "\n".join(hunk.new_lines_content)
|
||||||
|
lang = LanguageDetector().detect(file_obj.filename)
|
||||||
|
|
||||||
|
issues = issue_detector.detect_diff_issues(old_code, new_code, lang)
|
||||||
|
for issue in issues:
|
||||||
|
all_issues.append(
|
||||||
|
{
|
||||||
|
"file": file_obj.filename,
|
||||||
|
"line": issue.line,
|
||||||
|
"severity": issue.severity,
|
||||||
|
"title": issue.title,
|
||||||
|
"description": issue.description,
|
||||||
|
"suggestion": issue.suggestion,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if all_issues:
|
||||||
|
severity_priority = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
||||||
|
all_issues.sort(key=lambda x: severity_priority.get(x.get("severity", ""), 4))
|
||||||
|
|
||||||
|
if ctx.obj.get("output") == "json":
|
||||||
|
click.echo(json.dumps(all_issues, indent=2))
|
||||||
|
else:
|
||||||
|
for issue in all_issues:
|
||||||
|
severity = issue["severity"].upper()
|
||||||
|
color = {
|
||||||
|
"critical": "red",
|
||||||
|
"high": "orange3",
|
||||||
|
"medium": "yellow",
|
||||||
|
"low": "cyan",
|
||||||
|
}.get(issue["severity"], "white")
|
||||||
|
click.echo(f"[{color}][{severity}][/] {issue['title']}")
|
||||||
|
click.echo(f" File: {issue['file']}:{issue['line']}")
|
||||||
|
click.echo(f" {issue['description']}")
|
||||||
|
click.echo(f" Suggestion: {issue['suggestion']}")
|
||||||
|
click.echo()
|
||||||
|
else:
|
||||||
|
click.echo("No issues detected in the diff.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"Error analyzing issues: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option("--file", "-f", type=click.Path(exists=True), help="Read diff from file")
|
||||||
|
@click.option("--stdin", "-s", is_flag=True, help="Read diff from stdin")
|
||||||
|
@click.pass_context
|
||||||
|
def summarize(ctx: click.Context, file: str | None, stdin: bool):
|
||||||
|
"""Show only a brief summary of changes."""
|
||||||
|
diff_content = ""
|
||||||
|
|
||||||
|
if stdin:
|
||||||
|
diff_content = sys.stdin.read()
|
||||||
|
elif file:
|
||||||
|
with open(file) as f:
|
||||||
|
diff_content = f.read()
|
||||||
|
else:
|
||||||
|
diff_content = sys.stdin.read()
|
||||||
|
|
||||||
|
if not diff_content.strip():
|
||||||
|
click.echo("No diff provided.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
files = parse_diff(diff_content)
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
click.echo("No valid diff files found.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
analysis = create_analysis(files)
|
||||||
|
|
||||||
|
click.echo(f"Files changed: {analysis.total_files}")
|
||||||
|
click.echo(f" Added: {analysis.files_added}")
|
||||||
|
click.echo(f" Deleted: {analysis.files_deleted}")
|
||||||
|
click.echo(f" Modified: {analysis.files_modified}")
|
||||||
|
click.echo(f" Renamed: {analysis.files_renamed}")
|
||||||
|
click.echo(f"Total changes: {analysis.total_changes}")
|
||||||
|
|
||||||
|
if analysis.language_breakdown:
|
||||||
|
click.echo("\nLanguages:")
|
||||||
|
for lang, count in sorted(analysis.language_breakdown.items()):
|
||||||
|
click.echo(f" - {lang}: {count} files")
|
||||||
|
|
||||||
|
if analysis.all_issues:
|
||||||
|
critical = sum(1 for i in analysis.all_issues if i.get("severity") == "critical")
|
||||||
|
high = sum(1 for i in analysis.all_issues if i.get("severity") == "high")
|
||||||
|
medium = sum(1 for i in analysis.all_issues if i.get("severity") == "medium")
|
||||||
|
low = sum(1 for i in analysis.all_issues if i.get("severity") == "low")
|
||||||
|
|
||||||
|
click.echo(f"\nIssues found: {len(analysis.all_issues)}")
|
||||||
|
if critical:
|
||||||
|
click.echo(f" Critical: {critical}")
|
||||||
|
if high:
|
||||||
|
click.echo(f" High: {high}")
|
||||||
|
if medium:
|
||||||
|
click.echo(f" Medium: {medium}")
|
||||||
|
if low:
|
||||||
|
click.echo(f" Low: {low}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"Error summarizing diff: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_analysis_json(analysis: DiffAnalysis) -> str:
|
||||||
|
"""Format analysis as JSON string."""
|
||||||
|
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,
|
||||||
|
"language": file_obj.extension,
|
||||||
|
"hunks": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for hunk in file_obj.hunks:
|
||||||
|
hunk_data = {
|
||||||
|
"old_start": hunk.old_start,
|
||||||
|
"new_start": hunk.new_start,
|
||||||
|
"changes": {
|
||||||
|
"added": hunk.get_added_lines(),
|
||||||
|
"removed": hunk.get_removed_lines(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
file_data["hunks"].append(hunk_data)
|
||||||
|
|
||||||
|
result["files"].append(file_data)
|
||||||
|
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|||||||
Reference in New Issue
Block a user