Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ Read GitHub repository info #55

Merged
merged 5 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 57 additions & 32 deletions codelimit/__main__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path
from typing import List, Annotated, Optional

import pyperclip
import typer
from click import Context
from rich import print
Expand All @@ -10,6 +11,8 @@
from codelimit.commands.report import report_command, ReportFormat
from codelimit.commands.scan import scan_command
from codelimit.common.Configuration import Configuration
from codelimit.common.utils import configure_github_repository
from codelimit.utils import success, fail
from codelimit.version import version


Expand All @@ -26,50 +29,72 @@ def list_commands(self, ctx: Context):

@cli.command(help="Check file(s)")
def check(
paths: Annotated[List[Path], typer.Argument(exists=True)],
exclude: Annotated[
Optional[list[str]], typer.Option(help="Glob patterns for exclusion")
] = None,
quiet: Annotated[
bool, typer.Option("--quiet", help="No output when successful")
] = False,
paths: Annotated[List[Path], typer.Argument(exists=True)],
exclude: Annotated[
Optional[list[str]], typer.Option(help="Glob patterns for exclusion")
] = None,
quiet: Annotated[
bool, typer.Option("--quiet", help="No output when successful")
] = False,
):
if exclude:
Configuration.excludes.extend(exclude)
Configuration.exclude.extend(exclude)
Configuration.load(Path('.'))
check_command(paths, quiet)


@cli.command(help="Scan a codebase")
def scan(
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path("."),
exclude: Annotated[
Optional[list[str]], typer.Option(help="Glob patterns for exclusion")
] = None,
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path("."),
exclude: Annotated[
Optional[list[str]], typer.Option(help="Glob patterns for exclusion")
] = None,
):
if exclude:
Configuration.excludes.extend(exclude)
Configuration.exclude.extend(exclude)
Configuration.load(path)
configure_github_repository(path)
scan_command(path)


@cli.command(help="Show report for codebase")
def report(
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path("."),
full: Annotated[bool, typer.Option("--full", help="Show full report")] = False,
totals: Annotated[bool, typer.Option("--totals", help="Only show totals")] = False,
fmt: Annotated[
ReportFormat, typer.Option("--format", help="Output format")
] = ReportFormat.text,
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path("."),
full: Annotated[bool, typer.Option("--full", help="Show full report")] = False,
totals: Annotated[bool, typer.Option("--totals", help="Only show totals")] = False,
fmt: Annotated[
ReportFormat, typer.Option("--format", help="Output format")
] = ReportFormat.text,
):
Configuration.load(path)
report_command(path, full, totals, fmt)


@cli.command(help="Generate badge Markdown")
def badge(
path: Annotated[
Path, typer.Argument(exists=True, file_okay=False, help="Codebase root")
] = Path(".")
):
Configuration.load(path)
configure_github_repository(path)
if not Configuration.repository:
fail("Could not determine repository information.")
raise typer.Exit(1)
owner = Configuration.repository.owner
name = Configuration.repository.name
branch = Configuration.repository.branch
badge_markdown = (f'[![CodeLimit](https://github.com/{owner}/{name}/blob/_codelimit_reports/{branch}/badge.svg)]('
f'https://github.com/{owner}/{name}/blob/_codelimit_reports/{branch}/codelimit.md)')
print(f'{badge_markdown}\n')
pyperclip.copy(badge_markdown)
success("Badge Markdown copied to clipboard!")


def _version_callback(show: bool):
if show:
print(f"Code Limit version: {version}")
Expand All @@ -78,15 +103,15 @@ def _version_callback(show: bool):

@cli.callback()
def main(
verbose: Annotated[
Optional[bool], typer.Option("--verbose", "-v", help="Verbose output")
] = False,
version: Annotated[
Optional[bool],
typer.Option(
"--version", "-V", help="Show version", callback=_version_callback
),
] = None,
verbose: Annotated[
Optional[bool], typer.Option("--verbose", "-v", help="Verbose output")
] = False,
version: Annotated[
Optional[bool],
typer.Option(
"--version", "-V", help="Show version", callback=_version_callback
),
] = None,
):
"""Code Limit: Your refactoring alarm."""

Expand Down
2 changes: 1 addition & 1 deletion codelimit/commands/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

def check_command(paths: list[Path], quiet: bool):
check_result = CheckResult()
excludes_spec = PathSpec.from_lines("gitignore", Configuration.excludes)
excludes_spec = PathSpec.from_lines("gitignore", Configuration.exclude)
for path in paths:
if path.is_file():
_handle_file_path(path, check_result, excludes_spec)
Expand Down
2 changes: 1 addition & 1 deletion codelimit/commands/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def _report_functions_markdown(
result += "| --- | ---: | ---: | ---: | --- |\n"
for unit in report_units:
file_path = unit.file if root is None else root.joinpath(unit.file)
type = "" if unit.measurement.value > 60 else ""
type = "\u274C" if unit.measurement.value > 60 else "\u26A0"
result += (
f"| {str(file_path)} | {unit.measurement.start.line} | {unit.measurement.start.column} | "
f"{unit.measurement.value} | {type} {unit.measurement.unit_name} |\n"
Expand Down
9 changes: 6 additions & 3 deletions codelimit/common/Configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

from yaml import load, FullLoader

from codelimit.common.GithubRepository import GithubRepository


class Configuration:
excludes: list[str] = []
exclude: list[str] = []
verbose = False
repository: GithubRepository | None = None

@classmethod
def load(cls, root: Path):
Expand All @@ -14,7 +17,7 @@ def load(cls, root: Path):
return
with open(config_path) as f:
d = load(f, Loader=FullLoader)
if "excludes" in d:
cls.excludes.extend(d["excludes"])
if "exclude" in d:
cls.exclude.extend(d["exclude"])
if "verbose" in d:
cls.verbose = d["verbose"]
15 changes: 15 additions & 0 deletions codelimit/common/GithubRepository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from dataclasses import dataclass


@dataclass
class GithubRepository:
owner: str
name: str
branch: str | None = None
tag: str | None = None

def __str__(self) -> str:
result = f'{self.owner}/{self.name}'
if self.tag:
result += f'@{self.tag}'
return result
12 changes: 9 additions & 3 deletions codelimit/common/Scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,16 @@ def _scan_file(
codebase.add_file(entry)
return entry

def _read_file(path: Path):
try:
with open(path) as f:
return f.read()
except UnicodeDecodeError:
with open(path, encoding="latin-1") as f:
return f.read()

def _analyze_file(path, rel_path, checksum, lexer):
with open(path) as f:
code = f.read()
code = _read_file(path)
all_tokens = lex(lexer, code, False)
code_tokens = filter_tokens(all_tokens)
file_loc = count_lines(code_tokens)
Expand Down Expand Up @@ -176,7 +182,7 @@ def scan_file(tokens: list[Token], language: Language) -> list[Measurement]:

def _generate_exclude_spec(root: Path) -> PathSpec:
excludes = DEFAULT_EXCLUDES.copy()
excludes.extend(Configuration.excludes)
excludes.extend(Configuration.exclude)
gitignore_excludes = _read_gitignore(root)
if gitignore_excludes:
excludes.extend(gitignore_excludes)
Expand Down
3 changes: 3 additions & 0 deletions codelimit/common/console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from rich.console import Console

console = Console()
35 changes: 34 additions & 1 deletion codelimit/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
import os
import sys
from math import ceil
from pathlib import Path
from typing import Union, Any

from rich.style import Style
from rich.text import Text
from sh import git, ErrorReturnCode

from codelimit.common.Configuration import Configuration
from codelimit.common.Measurement import Measurement
from codelimit.common.GithubRepository import GithubRepository
from codelimit.common.gsm.Expression import Expression
from codelimit.common.gsm.operator.Operator import Operator
from codelimit.common.token_matching.predicate.TokenValue import TokenValue
Expand Down Expand Up @@ -66,7 +70,7 @@ def render_quality_profile(profile: list[int]) -> Text:
def path_has_extension(path: str, suffixes: Union[str, list[str]]):
dot_index = path.rfind(".")
if dot_index >= 0:
suffix = path[dot_index + 1 :]
suffix = path[dot_index + 1:]
if isinstance(suffixes, list):
return suffix in suffixes
else:
Expand Down Expand Up @@ -199,3 +203,32 @@ def replace_string_literal_with_predicate(expression: Expression) -> Expression:
)
)
]


def _get_git_branch(path: Path) -> str | None:
try:
out = git('rev-parse', '--abbrev-ref', 'HEAD', _cwd=path)
return out.strip()
except ErrorReturnCode:
return None


def _get_remote_url(path: Path) -> str | None:
try:
out = git('config', '--get', 'remote.origin.url', _cwd=path)
return out.strip()
except ErrorReturnCode:
return None


def configure_github_repository(path: Path):
branch = _get_git_branch(path)
url = _get_remote_url(path)
if not url or not branch:
return
if url.startswith('[email protected]:') and url.endswith('.git'):
[owner, name] = url[15:-4].split('/')
Configuration.repository = GithubRepository(owner, name, branch=branch)
elif url.startswith('https://github.com/') and url.endswith('.git'):
[owner, name] = url[19:-4].split('/')
Configuration.repository = GithubRepository(owner, name, branch=branch)
21 changes: 17 additions & 4 deletions codelimit/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,24 @@
import typer
from rich.progress import Progress, SpinnerColumn, TextColumn

from codelimit.common.console import console
from codelimit.common.report.Report import Report
from codelimit.common.report.ReportReader import ReportReader
from codelimit.common.report.ReportWriter import ReportWriter


def info(text: str):
console.print(f'[bold]ℹ︎[/bold] {text}', soft_wrap=True)


def success(text: str):
console.print(f'[green]✔[/green] {text}', soft_wrap=True)


def fail(text: str):
console.print(f'[red]⨯[/red] {text}', soft_wrap=True)


def make_report_path(root: Path) -> Path:
return root.joinpath(".codelimit_cache").resolve().joinpath("codelimit.json")

Expand All @@ -24,7 +37,7 @@ def read_cached_report(path: Path) -> Optional[Report]:


def upload_report(
report: Report, repository: str, branch: str, url: str, token: str
report: Report, repository: str, branch: str, url: str, token: str
) -> None:
result = api_post_report(report, branch, repository, url, token)
if result.ok:
Expand All @@ -44,9 +57,9 @@ def api_post_report(report, branch, repository, url, token):
f'{{{{"repository": "{repository}", "branch": "{branch}", "report":{{}}}}}}'
)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
transient=True,
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
transient=True,
) as progress:
progress.add_task(description=f"Uploading report to {url}", total=None)
result = requests.post(
Expand Down
Loading
Loading