diff --git a/src/ghlabel/cli.py b/src/ghlabel/cli.py index 1d51f7c..536001d 100644 --- a/src/ghlabel/cli.py +++ b/src/ghlabel/cli.py @@ -24,14 +24,14 @@ from ghlabel.utils.dump_label import DumpLabel from ghlabel.utils.github_api import GithubApi from ghlabel.utils.github_api_types import GithubLabel -from ghlabel.utils.helpers import validate_env +from ghlabel.utils.helpers import clear_screen, validate_env from ghlabel.utils.setup_github_label import SetupGithubLabel -def parse_remove_labels(labels: str | None) -> list[str] | None: - if not labels: +def parse_remove_labels(label_names: str | None) -> set[str] | None: + if not label_names: return None - return list(map(str.strip, labels.split(","))) + return set(map(str.strip, label_names.split(","))) def parse_add_labels(labels: str | None) -> list[GithubLabel] | None: @@ -125,7 +125,7 @@ def setup_labels( # noqa: PLR0913 typer.Option( "--add-labels", "-a", - help="Add more labels.", + help="Add more Github labels.", ), ] = None, remove_labels: Annotated[ @@ -133,7 +133,7 @@ def setup_labels( # noqa: PLR0913 typer.Option( "--remove-labels", "-r", - help="Remove more labels.", + help="Remove more Github labels.", ), ] = None, remove_all: Annotated[ @@ -144,6 +144,14 @@ def setup_labels( # noqa: PLR0913 help="Remove all Github labels.", ), ] = RemoveAllChoices.disable.value, # type: ignore[assignment] + force: Annotated[ + bool, + typer.Option( + "--force-remove/--safe-remove", + "-f/-F", + help="Forcefully remove GitHub labels, even if they are currently in use on issues or pull requests.", + ), + ] = False, ) -> None: if not token: token = validate_env("GITHUB_TOKEN") @@ -153,6 +161,7 @@ def setup_labels( # noqa: PLR0913 repo_name = validate_env("GITHUB_REPO_NAME") gh_api: GithubApi = GithubApi(token, repo_owner, repo_name) + clear_screen() with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), @@ -168,32 +177,19 @@ def setup_labels( # noqa: PLR0913 ) rich.print() - with Progress( - SpinnerColumn(style="[magenta]"), - TextColumn("[progress.description]{task.description}"), - transient=True, - ) as progress: - progress.add_task(description="[magenta]Removing...", total=None) - - if remove_all.value == "enable": - gh_label.remove_all_labels(preview=preview) - elif remove_all.value == "silent": - gh_label.remove_all_labels(silent=True, preview=preview) - elif remove_all.value == "disable": - gh_label.remove_labels( - strict=strict, - labels=parse_remove_labels(remove_labels), - preview=preview, - ) - - with Progress( - SpinnerColumn(style="[cyan]"), - TextColumn("[progress.description]{task.description}"), - transient=True, - ) as progress: - progress.add_task(description="[cyan]Adding...", total=None) + if remove_all.value == "enable": + gh_label.remove_all_labels(preview=preview, force=force) + elif remove_all.value == "silent": + gh_label.remove_all_labels(silent=True, preview=preview, force=force) + elif remove_all.value == "disable": + gh_label.remove_labels( + strict=strict, + label_names=parse_remove_labels(remove_labels), + preview=preview, + force=force, + ) - gh_label.add_labels(labels=parse_add_labels(add_labels), preview=preview) + gh_label.add_labels(labels=parse_add_labels(add_labels), preview=preview) if gh_label.labels_unsafe_to_remove: if not preview: @@ -205,6 +201,11 @@ def setup_labels( # noqa: PLR0913 ) rich.print() + if not preview: + rich.print( + f"[green]Successfully[/green] setup github labels from config to repo `{repo_owner}/{repo_name}`." + ) + @app.command("dump", help="Generate starter labels config files.") # type: ignore[misc] def app_dump( @@ -243,6 +244,7 @@ def app_dump( ), ] = AppChoices.app.value, # type: ignore[assignment] ) -> None: + clear_screen() with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), @@ -253,6 +255,8 @@ def app_dump( DumpLabel.dump(labels_dir=labels_dir, new=new, ext=ext.value, app=app.value) time.sleep(0.5) + rich.print(f"[green]Successfully[/green] dumped labels config to `{labels_dir}`.") + @app.callback() # type: ignore[misc] def app_callback( diff --git a/src/ghlabel/utils/github_api.py b/src/ghlabel/utils/github_api.py index d3da2b7..b6c978a 100644 --- a/src/ghlabel/utils/github_api.py +++ b/src/ghlabel/utils/github_api.py @@ -123,7 +123,7 @@ def create_label(self, label: GithubLabel) -> tuple[GithubLabel, StatusCode]: return res.json(), res.status_code def update_label(self, label: GithubLabel) -> tuple[GithubLabel, StatusCode]: - url: str = f"{self.base_url}/{label['name']}" + url: str = f"{self.base_url}/labels/{label['name']}" label["new_name"] = label.pop("name") # type: ignore[misc] res: Response @@ -140,7 +140,8 @@ def update_label(self, label: GithubLabel) -> tuple[GithubLabel, StatusCode]: "The site can't be reached, `github.com` took to long to respond. Try checking the connection." ) sys.exit() - except HTTPError: + except HTTPError as ex: + logging.exception(ex) logging.error( f"Failed to update label `{label['new_name']}`. Check the label format." ) diff --git a/src/ghlabel/utils/helpers.py b/src/ghlabel/utils/helpers.py index 589050e..ba968c8 100644 --- a/src/ghlabel/utils/helpers.py +++ b/src/ghlabel/utils/helpers.py @@ -1,5 +1,7 @@ import logging import os +import platform +import subprocess import sys from logging.config import fileConfig from pathlib import Path @@ -19,3 +21,10 @@ def validate_env(env: str) -> str: logging.error(f"{env} environment variable not set.") sys.exit() return _env + + +def clear_screen() -> None: + if platform.system() == "Windows": + subprocess.run("cls", shell=True, check=False) # noqa: S607, S602 + else: + subprocess.run("clear", shell=True, check=False) # noqa: S607, S602 diff --git a/src/ghlabel/utils/setup_github_label.py b/src/ghlabel/utils/setup_github_label.py index 02209f3..3132375 100644 --- a/src/ghlabel/utils/setup_github_label.py +++ b/src/ghlabel/utils/setup_github_label.py @@ -22,10 +22,16 @@ import rich import yaml from dotenv import load_dotenv +from rich.progress import Progress +from rich.prompt import Confirm from ghlabel.utils.github_api import GithubApi from ghlabel.utils.github_api_types import GithubIssue, GithubLabel -from ghlabel.utils.helpers import STATUS_OK, validate_env +from ghlabel.utils.helpers import ( + STATUS_OK, + clear_screen, + validate_env, +) load_dotenv() Path("logs").mkdir(exist_ok=True) @@ -50,6 +56,7 @@ def __init__( self._labels: list[GithubLabel] = self._load_labels_from_config() or [] self._label_name_urls_map: dict[str, set[str]] = {} self._labels_unsafe_to_remove: set[str] = set() + self._labels_force_remove: set[str] = set() @property def labels_dir(self) -> str: @@ -61,6 +68,7 @@ def github_labels(self) -> list[GithubLabel]: @property def github_label_names(self) -> list[str]: + # requires index, so using list instead of set return self._github_label_names @property @@ -75,10 +83,17 @@ def label_name_urls_map(self) -> dict[str, set[str]]: def labels_unsafe_to_remove(self) -> set[str]: return self._labels_unsafe_to_remove + @property + def labels_force_remove(self) -> set[str]: + return self._labels_force_remove + @property def gh_api(self) -> GithubApi: return self._gh_api + def set_labels_force_remove(self, label_names: set[str]) -> None: + return self._labels_force_remove.update(label_names) + def _fetch_formatted_github_labels(self) -> list[GithubLabel]: github_labels, status_code = self.gh_api.list_labels() if status_code != STATUS_OK: @@ -232,27 +247,33 @@ def _load_labels_to_remove_from_config(self) -> set[str]: return set(labels_to_remove) - def remove_all_labels(self, silent: bool = False, preview: bool = False) -> None: + def remove_all_labels( + self, silent: bool = False, preview: bool = False, force: bool = False + ) -> None: confirmation: bool = False if silent is False and not preview: - confirmation = input( - "WARNING: This action will remove all labels in the repository.\n" - "Are you sure you want to continue? (yes/no): " - ).strip().lower() in ("y", "yes") + rich.print( + "[[yellow]WARNING[/yellow]] This action will [red]remove[/red] all labels in the repository." + ) + confirmation = Confirm.ask("Are you sure you want to continue?") else: confirmation = True if confirmation: - self.remove_labels(labels=self.github_label_names, preview=preview) + self.remove_labels( + label_names=set(self.github_label_names), preview=preview, force=force + ) def remove_labels( self, - labels: list[str] | None = None, + label_names: set[str] | None = None, strict: bool = False, preview: bool = False, + force: bool = False, ) -> None: labels_to_remove: set[str] = set() + labels_safe_to_remove: set[str] if strict: labels_to_remove.update( @@ -260,12 +281,15 @@ def remove_labels( - set([label["name"] for label in self.labels]) ) - if labels: - labels_to_remove.update(labels) + if label_names: + labels_to_remove.update(label_names) - labels_safe_to_remove: set[str] = self._list_labels_safe_to_remove( - label_names=labels_to_remove - ) + if not force: + labels_safe_to_remove = self._list_labels_safe_to_remove( + label_names=labels_to_remove + ) + else: + labels_safe_to_remove = labels_to_remove if preview: rich.print(" will [red]remove[/red] the following labels:") @@ -282,9 +306,20 @@ def remove_labels( rich.print() return - for label_name in labels_safe_to_remove: - if label_name in self.github_label_names: - self.gh_api.delete_label(label_name) + clear_screen() + with Progress(transient=True) as progress: + task_id = progress.add_task( + "[red]Removing...[/red]", total=len(labels_safe_to_remove) + ) + + for label_name in labels_safe_to_remove: + if label_name in self.github_label_names: + self.gh_api.delete_label(label_name) + progress.update( + task_id, + advance=1, + description=f"[red]Removed[/red] Label `{label_name}`", + ) def update_labels(self, labels: list[GithubLabel], preview: bool = False) -> None: if preview and labels: @@ -305,8 +340,20 @@ def update_labels(self, labels: list[GithubLabel], preview: bool = False) -> Non rich.print() return - for label in labels: - self.gh_api.update_label(label) + if preview and labels: + clear_screen() + with Progress(transient=True) as progress: + task_id = progress.add_task( + "[yellow]Updating...[/yellow]", total=len(labels) + ) + + for label in labels: + self.gh_api.update_label(label) + progress.update( + task_id, + advance=1, + description=f'[yellow]Updated[/yellow] Label `{label["new_name"]}`', + ) def add_labels( self, labels: list[GithubLabel] | None = None, preview: bool = False @@ -358,8 +405,19 @@ def add_labels( self.update_labels(labels_to_update, preview=preview) return - for label in labels_to_add: - self.gh_api.create_label(label) + clear_screen() + with Progress(transient=True) as progress: + task_id = progress.add_task( + "[cyan]Adding...[/cyan]", total=len(labels_to_add) + ) + + for label in labels_to_add: + self.gh_api.create_label(label) + progress.update( + task_id, + advance=1, + description=f'[cyan]Added[/cyan] Label `{label["name"]}`', + ) self.update_labels(labels_to_update, preview=preview) logging.info("Label creation process completed.")