From a5a753f429d4ea71c58b937c8c44b171f31625dd Mon Sep 17 00:00:00 2001 From: Jonathan Teran Carballo Date: Tue, 14 Jul 2026 00:22:22 -0300 Subject: [PATCH 1/3] refactor: migrar comando new para que utilice metodos usando questionary --- pyproject.toml | 2 +- src/git_flow/command/base.py | 45 +++++++++++++++++++++++++++++------- src/git_flow/command/new.py | 43 +++++++++++++++------------------- src/git_flow/git.py | 18 ++++----------- src/git_flow/main.py | 14 +++++------ uv.lock | 37 +++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 55 deletions(-) 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" }, +] From 320d40a211b39523e15329336690823fc873ba61 Mon Sep 17 00:00:00 2001 From: Jonathan Teran Carballo Date: Tue, 14 Jul 2026 00:23:36 -0300 Subject: [PATCH 2/3] refactor: migrar comando commit para que utilice questionary --- src/git_flow/command/commit.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/git_flow/command/commit.py b/src/git_flow/command/commit.py index 27b3cae..79ae2c6 100644 --- a/src/git_flow/command/commit.py +++ b/src/git_flow/command/commit.py @@ -2,7 +2,7 @@ import typer from datetime import datetime from git_flow import COMMIT_TYPES, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError from git_flow.git import Git -from git_flow.command.base import * +import git_flow.command.base as base app = typer.Typer() @@ -10,18 +10,18 @@ app = typer.Typer() @app.command() def commit(): """Crea un commit siguiendo el formato de Conventional Commits""" - ensure_initialized() - branch = ensure_right_branch() + base.ensure_initialized() + branch = base.ensure_right_branch() if not _has_files_staged(): - warning("No hay cambios en el indice para commitear.") - if confirm("¿Desea agregar la carpeta actual?"): + base.io_warning("No hay cambios en el indice para commitear.") + if base.io_confirm("¿Desea agregar la carpeta actual?"): Git("add", ".").exec(print="Agregando cambios") else: raise GitFlowError("Debe agregar algún cambio al indice para continuar.") - commit_type = choice("Tipo de commit", COMMIT_TYPES) - commit_message = prompt( + commit_type = base.io_choice("Tipo de commit", COMMIT_TYPES) + commit_message = base.io_prompt( "Mensaje (máximo recomendado: 100 caracteres)", persistent=True ) message = commit_type + ": " + commit_message From 0782cfb0410fc8f4a97570985e9ba4f27c150a22 Mon Sep 17 00:00:00 2001 From: Jonathan Teran Carballo Date: Tue, 14 Jul 2026 00:28:20 -0300 Subject: [PATCH 3/3] refactor: migrar comando merge para que utilice questionary --- src/git_flow/command/base.py | 3 ++ src/git_flow/command/merge.py | 64 +++++++++++++++++------------------ 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/git_flow/command/base.py b/src/git_flow/command/base.py index c11d32e..b3ef583 100644 --- a/src/git_flow/command/base.py +++ b/src/git_flow/command/base.py @@ -148,6 +148,9 @@ def io_warning(message: str, title: Optional[str] = None): def io_info(message: str, title: Optional[str] = None): _io_status(message, "ℹ️ Info: ", "blue", title) +def io_success(message: str, title: Optional[str] = None): + _io_status(message, "✅ Success: ", "green", 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")) diff --git a/src/git_flow/command/merge.py b/src/git_flow/command/merge.py index 9f76f20..74d3f3c 100644 --- a/src/git_flow/command/merge.py +++ b/src/git_flow/command/merge.py @@ -1,7 +1,7 @@ import typer from git_flow import GitFlowError from git_flow.changelog import Changelog -from git_flow.command.base import * +import git_flow.command.base as base from git_flow.git import Git app = typer.Typer() @@ -10,36 +10,36 @@ app = typer.Typer() @app.command() def merge(): """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado""" - ensure_initialized() + base.ensure_initialized() - branch = ensure_right_branch() + branch = base.ensure_right_branch() - if branch in environments: - if branch == environments[-1]: + if branch in base.environments: + if branch == base.environments[-1]: raise GitFlowError("No se puede hacer un merge del último entorno.") - panel( - "Merge de entornos", + base.io_info( """Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""", + "Merge de entornos", ) - if not confirm("¿Desea continuar?"): + if not base.io_confirm("¿Desea continuar?"): raise GitFlowError("Ejecución abortada") - target = environments[environments.index(branch) + 1] + target = base.environments[base.environments.index(branch) + 1] else: - target, _ = get_branch_env_and_type(branch) + target, _ = base.get_branch_env_and_type(branch) - if "flow.remote" in flowconfig: - remote = flowconfig["flow.remote"] - token = ensure_repository_token() + if "flow.remote" in base.flowconfig: + remote = base.flowconfig["flow.remote"] + token = base.ensure_repository_token() run_remote(remote, token, branch, target) else: run_local(branch, target) def run_remote(remote: str, token: str, branch: str, target: str): - ensure_clean_worktree(True) + base.ensure_clean_worktree(True) Git("switch", target).exec(print="Cambiando a rama destino") Git("pull").exec(print="Obteniendo ultimos cambios del remoto") @@ -48,8 +48,8 @@ def run_remote(remote: str, token: str, branch: str, target: str): check_merge_conflicts(target) show_commits_to_merge(target) - if confirm(f"¿Crear PR de '{branch}' a '{target}'?"): - if branch not in environments: + if base.io_confirm(f"¿Crear PR de '{branch}' a '{target}'?"): + if branch not in base.environments: Git("push", remote, branch, set_upstream=True).exec( print="Subiendo rama al remoto para crear PR" ) @@ -58,11 +58,11 @@ def run_remote(remote: str, token: str, branch: str, target: str): def run_local(branch: str, target: str): - ensure_clean_worktree(False) + base.ensure_clean_worktree(False) check_merge_conflicts(target) show_commits_to_merge(target) - if confirm(f"¿Mergear rama '{branch}' a '{target}'?"): + if base.io_confirm(f"¿Mergear rama '{branch}' a '{target}'?"): Git("switch", target).exec(print="Cambiando a rama destino") Git("merge", branch, ff=False).exec(print="Mergeando") @@ -87,13 +87,11 @@ def check_merge_conflicts(target: str): Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False) if merge_conflicts: - error(f"La rama actual tiene conflictos con {target}") - info( - f"""Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente, y ejecutar nuevamente este comando""" - ) + base.io_error(f"La rama actual tiene conflictos con {target}.\n" + f"Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente, y ejecutar nuevamente este comando", "Conflictos de merge") raise GitFlowError("Ejecución abortada.") else: - success("No se detectaron merge conflicts.") + base.io_success("No se detectaron merge conflicts.") def create_pull_request(token: str, branch: str, target: str): @@ -101,28 +99,28 @@ def create_pull_request(token: str, branch: str, target: str): commits = Git("log", target + "..", format="%s").lines() title = commits[0] if len(commits) == 1 else _get_pr_title_from_branch(branch) - panel("Título del PR", title) + base.panel("Título del PR", title) - if confirm("¿Desea cambiar el título del PR?", False): - title = prompt("Título del PR") + if base.io_confirm("¿Desea cambiar el título del PR?", False): + title = base.io_prompt("Título del PR", persistent=True) - message = get_remote_api(token).create_pull_request( + message = base.get_remote_api(token).create_pull_request( branch, target, title, changelog.generate_content(target, branch), - branch not in environments + branch not in base.environments ) - success(message) + base.io_success(message, "Rama creada") - if confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"): + if base.io_confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"): Git("switch", target).exec(print="Cambiando a rama objetivo") Git("pull").exec(print="Obteniendo cambios") def _get_pr_title_from_branch(branch: str) -> str: - if branch in environments: - target = environments[environments.index(branch) + 1] + if branch in base.environments: + target = base.environments[base.environments.index(branch) + 1] return f"Sincronización de entorno {branch} a {target}" components = branch.split("/") # / or release/// @@ -136,7 +134,7 @@ def _get_pr_title_from_branch(branch: str) -> str: if first_dash >= 0 and second_dash >= 0: maybe_ticket = branch_desc[:second_dash] - if is_valid_ticket(maybe_ticket): + if base.is_valid_ticket(maybe_ticket): branch_ticket = maybe_ticket branch_desc = branch_desc[second_dash+1:]