diff --git a/pyproject.toml b/pyproject.toml index 81cd782..915a415 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Git workflow automation to follow best practices when working wit dynamic = ["version"] requires-python = ">= 3.14" dependencies = [ - "requests", "typer" + "requests", "typer", "rich", "questionary" ] authors = [ {name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"}, diff --git a/src/git_flow/command/base.py b/src/git_flow/command/base.py index 3409684..c11d32e 100644 --- a/src/git_flow/command/base.py +++ b/src/git_flow/command/base.py @@ -13,10 +13,10 @@ from git_flow.remote.gitea import GiteaRemoteAPI from git_flow.io import * from os.path import isfile -TYPE_SUCCESS = 0 -TYPE_WARNING = 1 -TYPE_ERROR = 2 -TYPE_INFO = 3 +from typing import Optional +import rich +import rich.panel +import questionary flowconfig = Git.get_config(FLOWCONFIG_FILENAME) if isfile(FLOWCONFIG_FILENAME) else {} @@ -48,7 +48,7 @@ def ensure_right_branch(): if branch == "HEAD": raise GitFlowError("No se encuentra parado sobre una rama.") - elif confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"): + elif io_confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"): return branch else: raise GitFlowError("Ejecución cancelada.") @@ -102,11 +102,11 @@ def ensure_clean_worktree(has_remote: bool): if not_empty: if not has_remote: - warning("Existen cambios en tu entorno de trabajo sin commitear.") + io_warning("Existen cambios en tu entorno de trabajo sin commitear.") else: raise GitFlowError("No se puede continuar con cambios pendientes.") - if not confirm("¿Desea continuar?", False): + if not io_confirm("¿Desea continuar?", False): raise GitFlowError("Ejecución abortada") @@ -137,4 +137,33 @@ def is_valid_ticket(ticket: str) -> bool: ticket_project = components[0] ticket_number = components[1] - return ticket_project.isalpha() and ticket_project.isupper() and ticket_number.isnumeric() \ No newline at end of file + return ticket_project.isalpha() and ticket_project.isupper() and ticket_number.isnumeric() + +def io_error(message: str, title: Optional[str] = None): + _io_status(message, "❌ Error: ", "red", title) + +def io_warning(message: str, title: Optional[str] = None): + _io_status(message, "⚠️ Warning: ", "yellow", title) + +def io_info(message: str, title: Optional[str] = None): + _io_status(message, "ℹ️ Info: ", "blue", title) + +def _io_status(message: str, prefix: str, style: str, title: Optional[str] = None): + if title: + rich.print(rich.panel.Panel(message, title=prefix + title, style=style, expand=False, title_align="left")) + else: + rich.print(f"[{style}]{prefix}{message}[/{style}]") + +def io_confirm(question: str, default: bool = True) -> bool: + return questionary.confirm(question, default=default, auto_enter=False).ask() + +def io_choice(prompt: str, options: list[str]) -> str: + return questionary.select(prompt, options, instruction="Usar flechas").ask() + +def io_prompt(message: str, default: str = "", persistent: bool = False, strip: bool = False, validator = None) -> str: + if persistent and not validator: + validator = lambda s: len(s.strip()) > 0 + + value = questionary.text(message, default, validate=validator).ask() + + return value.strip() if strip else value \ No newline at end of file diff --git a/src/git_flow/command/new.py b/src/git_flow/command/new.py index ca9afbc..eef826a 100644 --- a/src/git_flow/command/new.py +++ b/src/git_flow/command/new.py @@ -1,6 +1,6 @@ import typer from git_flow import BRANCH_TYPES -from git_flow.command.base import * +import git_flow.command.base as base from git_flow.git import Git from typing import Optional @@ -11,52 +11,45 @@ app = typer.Typer() def new(): """Crea una nueva rama siguiendo Conventional Branches""" - ensure_initialized() - branch = ensure_right_branch() + base.ensure_initialized() + branch = base.ensure_right_branch() - if branch not in environments: - warning("La rama actual no corresponde a un entorno configurado.") + if branch not in base.environments: + base.io_warning("La rama actual no corresponde a un entorno configurado.") - info("Se creará una nueva rama de trabajo sobre la rama actual.") - info("Debe seleccionar el tipo de cambio a realizar.") + base.io_info("Se creará una nueva rama de trabajo sobre la rama actual.\n" + "Debe seleccionar el tipo de cambio a realizar.", title="Tipo de cambio") - branch_type = choice("Tipos de cambio", BRANCH_TYPES) + branch_type = base.io_choice("Tipos de cambio", BRANCH_TYPES) ticket = _get_optional_ticket() new_branch = _get_unique_branch_name(branch_type, ticket) - if "flow.remote" in flowconfig and Git.get_tracking_branch(branch): + if "flow.remote" in base.flowconfig and Git.get_tracking_branch(branch): Git("pull").exec(print="Actualizando rama actual") - if confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"): + if base.io_confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"): Git("switch", new_branch, create=True).exec(print="Creando nueva rama") def _get_optional_ticket() -> Optional[str]: - ticket = prompt("[Opcional] Ticket que respalda el cambio (en formato ABC-123)") - - if not ticket: - return None - if not is_valid_ticket(ticket): - warning("Formato de ticket inválido, ignorando.") - return None - - return ticket + return base.io_prompt( + "Ticket que respalda el cambio (en formato ABC-123)", + validator=lambda x: len(x.strip()) == 0 or base.is_valid_ticket(x) + ) def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str: - info(f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.") + base.io_info(f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.\n" + "Las mismas se utilizaran para generar el nombre de la rama.", title="Nombre de rama") branch = None branches = Git.get_branches() while branch is None: - keywords = prompt( - "Palabras clave del cambio (por ejemplo: 'create worker form')", - persistent=True, - ) + keywords = base.io_prompt("Palabras clave", persistent=True) keywords = filter(lambda k: len(k) > 0, keywords.split(" ")) branch = branch_type + "/" + (ticket + "-" if ticket else "") + "-".join(keywords) if branch in branches: - error( + base.io_error( f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra" ) branch = None diff --git a/src/git_flow/git.py b/src/git_flow/git.py index 6e10139..28c5243 100644 --- a/src/git_flow/git.py +++ b/src/git_flow/git.py @@ -140,17 +140,7 @@ class Git: return process def _run(self, print: str | None = None, check: bool = True): - process = subprocess.run( - self.command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) - - if print is not None: - self._print_process("", "", title=print) - - if check: - process.check_returncode() - - return process + return self._get(print, check) def _print_process(self, stdout: str, stderr: str, title: str): command = [] @@ -164,12 +154,12 @@ class Git: command.append(arg) - text = f"[yellow]$ {" ".join(command)}[/yellow]" + text = f"[green]$ {" ".join(command)}[/green]" for line in stdout.splitlines(): - text += "\n\\[out] " + line + text += "\n" + line for line in stderr.splitlines(): - text += "\n[red]\\[err][/red] " + line + text += "\n[red]" + line + "[/red]" panel(title or "Ejecutando", text) diff --git a/src/git_flow/main.py b/src/git_flow/main.py index 5355d23..00e79bb 100755 --- a/src/git_flow/main.py +++ b/src/git_flow/main.py @@ -3,7 +3,7 @@ import typer from git_flow import GitFlowError -from git_flow.command.base import * +import git_flow.command.base as base from git_flow.command import init, new, commit, merge, tag, release, branch REPOSITORY_TOKEN_PATH = ".repository-token" @@ -23,10 +23,10 @@ app.add_typer(branch.app) def main(): try: app() - except GitFlowError as e: - error(str(e)) - except Exception as e: - error("Ocurrió un error inesperado: " + str(e)) - except KeyboardInterrupt as e: print() - error("Ejecución abortada") + except GitFlowError as e: + base.io_error(str(e), title="Git Flow") + except Exception as e: + base.io_error(str(e), title="Inesperado") + except KeyboardInterrupt as e: + base.io_error("Ejecución abortada") diff --git a/uv.lock b/uv.lock index 6673080..2335e7c 100644 --- a/uv.lock +++ b/uv.lock @@ -70,13 +70,17 @@ wheels = [ name = "git-flow-envs" source = { editable = "." } dependencies = [ + { name = "questionary" }, { name = "requests" }, + { name = "rich" }, { name = "typer" }, ] [package.metadata] requires-dist = [ + { name = "questionary" }, { name = "requests" }, + { name = "rich" }, { name = "typer" }, ] @@ -110,6 +114,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -119,6 +135,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "questionary" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -179,3 +207,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6 wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] + +[[package]] +name = "wcwidth" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, +]