From 0beb059db187e79b20e098c42b584ada64dd8c37 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Thu, 5 Feb 2026 17:30:12 +0000 Subject: [PATCH] fix: resolve CI issues - remove unused imports and fix code quality --- repohealth-cli/src/repohealth/cli/cli.py | 359 +++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 repohealth-cli/src/repohealth/cli/cli.py diff --git a/repohealth-cli/src/repohealth/cli/cli.py b/repohealth-cli/src/repohealth/cli/cli.py new file mode 100644 index 0000000..1bc09ac --- /dev/null +++ b/repohealth-cli/src/repohealth/cli/cli.py @@ -0,0 +1,359 @@ +import os +from typing import Optional + +import click +from rich.console import Console + +from repohealth.analyzers.bus_factor import BusFactorCalculator +from repohealth.analyzers.git_analyzer import GitAnalyzer +from repohealth.analyzers.risk_analyzer import RiskAnalyzer +from repohealth.models.result import RepositoryResult +from repohealth.reporters.html_reporter import HTMLReporter +from repohealth.reporters.json_reporter import JSONReporter +from repohealth.reporters.terminal import TerminalReporter + + +class RepoHealthCLI: + """Main CLI class for RepoHealth.""" + + def __init__(self): + """Initialize the CLI.""" + self.console = Console() + self.terminal_reporter = TerminalReporter(self.console) + self.json_reporter = JSONReporter() + self.html_reporter = HTMLReporter() + + def analyze_repository( + self, + repo_path: str, + depth: Optional[int] = None, + path_filter: Optional[str] = None, + extensions: Optional[str] = None, + min_commits: int = 1 + ) -> RepositoryResult: + """Perform full repository analysis. + + Args: + repo_path: Path to the repository. + depth: Optional limit on commit history. + path_filter: Optional path to filter files. + extensions: Comma-separated list of extensions. + min_commits: Minimum commits to consider a file. + + Returns: + RepositoryResult with all analysis data. + """ + git_analyzer = GitAnalyzer(repo_path) + + if depth is not None and depth <= 0: + raise click.ClickException("--depth must be a positive integer") + + if not git_analyzer.validate_repository(): + raise click.ClickException( + f"'{repo_path}' is not a valid Git repository" + ) + + ext_list = None + if extensions: + ext_list = [e.strip().lstrip('.') for e in extensions.split(',')] + + file_analyses = [] + all_authors = git_analyzer.get_unique_authors() + + for _file_path, _commit in git_analyzer.iter_file_commits( + path=path_filter, + extensions=ext_list, + depth=depth + ): + pass + + files = git_analyzer.get_all_files(extensions=ext_list) + + bus_factor_calc = BusFactorCalculator() + risk_analyzer = RiskAnalyzer() + + for file_path in files: + analysis = git_analyzer.analyze_file_authors(file_path, depth=depth) + + if analysis.total_commits >= min_commits: + file_analyses.append(analysis) + + if analysis.path in all_authors: + author_email = list(analysis.author_commits.keys())[0] + if author_email in all_authors: + all_authors[author_email].add_file( + analysis.path, + analysis.module + ) + + file_analyses = bus_factor_calc.assign_risk_levels(file_analyses) + + overall_bus_factor = bus_factor_calc.calculate_repository_bus_factor(file_analyses) + gini = bus_factor_calc.calculate_repository_gini(file_analyses) + + hotspots = risk_analyzer.identify_hotspots(file_analyses) + suggestions = risk_analyzer.generate_suggestions(file_analyses) + risk_summary = risk_analyzer.calculate_risk_summary(file_analyses) + + json_reporter = JSONReporter() + files_dict = [json_reporter.generate_file_dict(f) for f in file_analyses] + + hotspots_dict = [ + { + "file_path": h.file_path, + "risk_level": h.risk_level, + "bus_factor": round(h.bus_factor, 2), + "top_author": h.top_author, + "top_author_share": round(h.top_author_share, 3), + "total_commits": h.total_commits, + "num_authors": h.num_authors, + "module": h.module, + "suggestion": h.suggestion + } + for h in hotspots + ] + + suggestions_dict = [ + { + "file_path": s.file_path, + "current_author": s.current_author, + "suggested_authors": s.suggested_authors, + "priority": s.priority, + "reason": s.reason, + "action": s.action + } + for s in suggestions + ] + + result = RepositoryResult( + repository_path=os.path.abspath(repo_path), + files_analyzed=len(file_analyses), + total_commits=git_analyzer.get_commit_count(), + unique_authors=len(all_authors), + overall_bus_factor=overall_bus_factor, + gini_coefficient=gini, + files=files_dict, + hotspots=hotspots_dict, + suggestions=suggestions_dict, + risk_summary=risk_summary, + metadata={ + "depth": depth, + "path_filter": path_filter, + "extensions": ext_list, + "min_commits": min_commits + } + ) + + return result + + +@click.group() +@click.version_option(version="1.0.0") +def main(): + """RepoHealth CLI - Analyze Git repositories for bus factor and knowledge concentration.""" + pass + + +@main.command() +@click.argument( + "repo_path", + type=click.Path(file_okay=False, dir_okay=True), + default="." +) +@click.option( + "--depth", + type=int, + default=None, + help="Limit commit history depth" +) +@click.option( + "--path", + "path_filter", + type=str, + default=None, + help="Analyze specific paths within the repository" +) +@click.option( + "--extensions", + type=str, + default=None, + help="Filter by file extensions (comma-separated, e.g., 'py,js,ts')" +) +@click.option( + "--min-commits", + type=int, + default=1, + help="Minimum commits to consider a file (default: 1)" +) +@click.option( + "--json", + "output_json", + is_flag=True, + default=False, + help="Output in JSON format" +) +@click.option( + "--output", + type=click.Path(file_okay=True, dir_okay=False), + default=None, + help="Output file path (for JSON format)" +) +def analyze( + repo_path: str, + depth: Optional[int], + path_filter: Optional[str], + extensions: Optional[str], + min_commits: int, + output_json: bool, + output: Optional[str] +): + """Analyze a Git repository for bus factor and knowledge concentration.""" + cli = RepoHealthCLI() + + try: + result = cli.analyze_repository( + repo_path, + depth=depth, + path_filter=path_filter, + extensions=extensions, + min_commits=min_commits + ) + + if output_json or output: + if output: + cli.json_reporter.save(result, output) + click.echo(f"JSON report saved to: {output}") + else: + click.echo(cli.json_reporter.generate(result)) + else: + cli.terminal_reporter.display_result(result) + + except click.ClickException: + raise + except Exception as e: + raise click.ClickException(f"Analysis failed: {str(e)}") from e + + +@main.command() +@click.argument( + "repo_path", + type=click.Path(file_okay=False, dir_okay=True), + default="." +) +@click.option( + "--format", + "output_format", + type=click.Choice(["json", "html", "terminal"]), + default="terminal", + help="Output format (default: terminal)" +) +@click.option( + "--output", + type=click.Path(file_okay=True, dir_okay=False), + default=None, + help="Output file path (for JSON/HTML formats)" +) +@click.option( + "--depth", + type=int, + default=None, + help="Limit commit history depth" +) +@click.option( + "--path", + "path_filter", + type=str, + default=None, + help="Analyze specific paths within the repository" +) +@click.option( + "--extensions", + type=str, + default=None, + help="Filter by file extensions (comma-separated)" +) +@click.option( + "--min-commits", + type=int, + default=1, + help="Minimum commits to consider a file" +) +def report( + repo_path: str, + output_format: str, + output: Optional[str], + depth: Optional[int], + path_filter: Optional[str], + extensions: Optional[str], + min_commits: int +): + """Generate a detailed report of repository analysis.""" + cli = RepoHealthCLI() + + try: + result = cli.analyze_repository( + repo_path, + depth=depth, + path_filter=path_filter, + extensions=extensions, + min_commits=min_commits + ) + + if output_format == "json": + if output: + cli.json_reporter.save(result, output) + click.echo(f"JSON report saved to: {output}") + else: + click.echo(cli.json_reporter.generate(result)) + + elif output_format == "html": + output_path = output or "repohealth_report.html" + cli.html_reporter.save_standalone(result, output_path) + click.echo(f"HTML report saved to: {output_path}") + + else: + cli.terminal_reporter.display_result(result) + + except click.ClickException: + raise + except Exception as e: + raise click.ClickException(f"Report generation failed: {str(e)}") from e + + +@main.command() +@click.argument( + "repo_path", + type=click.Path(file_okay=False, dir_okay=True), + default="." +) +def health( + repo_path: str +): + """Show repository health summary.""" + cli = RepoHealthCLI() + + try: + result = cli.analyze_repository(repo_path) + + risk = result.risk_summary.get("overall_risk", "unknown") + bus_factor = result.overall_bus_factor + + if risk == "critical": + emoji = "🔴" + elif risk == "high": + emoji = "🟠" + elif risk == "medium": + emoji = "🟡" + else: + emoji = "🟢" + + click.echo(f"{emoji} Repository Health: {risk.upper()}") + click.echo(f" Bus Factor: {bus_factor:.2f}") + click.echo(f" Files Analyzed: {result.files_analyzed}") + click.echo(f" Critical Files: {result.risk_summary.get('critical', 0)}") + click.echo(f" High Risk Files: {result.risk_summary.get('high', 0)}") + + except click.ClickException: + raise + except Exception as e: + raise click.ClickException(f"Health check failed: {str(e)}") from e