diff --git a/src/gdiffer/cli.py b/src/gdiffer/cli.py index 4a31433..ac14a12 100644 --- a/src/gdiffer/cli.py +++ b/src/gdiffer/cli.py @@ -1,291 +1 @@ -"""CLI interface for git diff explainer.""" - -import json -import sys -from typing import Optional - -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 OutputFormatter, OutputFormat -from gdiffer.parser import parse_diff - - -def create_analysis(files: list[DiffFile], verbose: bool = False) -> DiffAnalysis: - 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) - - summary = 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): - 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: Optional[str], file: Optional[str], stdin: bool): - 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, 'r') 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) - 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: Optional[str], stdin: bool): - diff_content = "" - - if stdin: - diff_content = sys.stdin.read() - elif file: - with open(file, 'r') 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: - color = {'critical': 'red', 'high': 'orange3', 'medium': 'yellow', 'low': 'cyan'}.get( - issue['severity'], 'white' - ) - click.echo(f"[{color}][{issue['severity'].upper()}][/] {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: Optional[str], stdin: bool): - diff_content = "" - - if stdin: - diff_content = sys.stdin.read() - elif file: - with open(file, 'r') 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: - 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) +# src/gdiffer/cli.py