From 58dddd2d4be40d049c3539c03639d85bf962e249 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Mon, 2 Feb 2026 14:39:07 +0000 Subject: [PATCH] fix: resolve CI linting errors - remove unused imports and update type annotations --- src/gdiffer/cli.py | 313 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 312 insertions(+), 1 deletion(-) diff --git a/src/gdiffer/cli.py b/src/gdiffer/cli.py index ac14a12..c026cfa 100644 --- a/src/gdiffer/cli.py +++ b/src/gdiffer/cli.py @@ -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)