From 839f71962fb7162d7d1ea88ec36889783060797c Mon Sep 17 00:00:00 2001 From: Jonathan Teran Carballo Date: Sat, 26 Sep 2026 17:18:34 -0300 Subject: [PATCH] feature: add verbose flag, improve command output, improve error display --- README.md | 3 +++ src/git_flow/git.py | 40 +++++++++++++++++++++++-------- src/git_flow/io.py | 11 ++++++++- src/git_flow/main.py | 56 +++++++++++++++++++++++++++++++++++++++----- 4 files changed, 93 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index fa632d8..d465afa 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ más información): `--remote`), dejando un reporte CSV (`branch-clean-report.csv`) con nombre, commit, autor y fecha de cada rama eliminada. +Por defecto solo se muestra cada paso que se ejecuta. Con `git-flow --verbose ` (o `-v`) se +muestra además la salida de cada comando git y el traceback completo de los errores. + El workflow para el cual se penso la herramienta es el siguiente: 1. Sobre la primer rama principal configurada (por ejemplo, `dev`), se crea una nueva rama con diff --git a/src/git_flow/git.py b/src/git_flow/git.py index 723bde8..95e4f52 100644 --- a/src/git_flow/git.py +++ b/src/git_flow/git.py @@ -1,6 +1,10 @@ import subprocess from dataclasses import dataclass +from rich.markup import escape +from rich.panel import Panel +from rich.text import Text + from git_flow import FLOWCONFIG_FILENAME from git_flow.io import * @@ -180,10 +184,13 @@ class Git: self._run(**kwargs) def _get(self, print: str | None = None, check: bool = True): - process = subprocess.run(self.command, capture_output=True, text=True) + process = subprocess.run(self.command, capture_output=True, text=True, check=False) - if print is not None: - self._print_process(process.stdout, process.stderr, print) + # Sin --verbose solo se muestra el paso; si falla, la salida la muestra main() + if print is not None and settings.verbose: + self._print_process(process, print) + elif print is not None: + console.print(Text.assemble(("› ", "cyan"), print)) if check: process.check_returncode() @@ -193,7 +200,7 @@ class Git: def _run(self, print: str | None = None, check: bool = True): return self._get(print, check) - def _print_process(self, stdout: str, stderr: str, title: str): + def _print_process(self, process: subprocess.CompletedProcess, title: str | None): command = [] for arg in self.command: @@ -205,12 +212,25 @@ class Git: command.append(arg) - text = f"[green]$ {' '.join(command)}[/green]" + ok = process.returncode == 0 + # git escribe mensajes informativos en stderr, así que solo se resalta si falló + stderr_style = "dim" if ok else "red" + text = Text.assemble(("$ ", "dim"), (" ".join(command), "bold yellow")) - for line in stdout.splitlines(): - text += "\n" + line + for line in process.stdout.splitlines(): + text.append("\n" + line) - for line in stderr.splitlines(): - text += "\n[red]" + line + "[/red]" + for line in process.stderr.splitlines(): + text.append("\n" + line, stderr_style) - panel(title or "Ejecutando", text) + console.print( + Panel( + text, + title=f"[bold]{escape(title or 'Ejecutando')}[/bold]", + title_align="left", + subtitle=None if ok else f"código de salida {process.returncode}", + subtitle_align="right", + border_style="bright_black" if ok else "red", + expand=False, + ) + ) diff --git a/src/git_flow/io.py b/src/git_flow/io.py index a168e3c..80f3548 100644 --- a/src/git_flow/io.py +++ b/src/git_flow/io.py @@ -1,10 +1,19 @@ from rich.console import Console -from rich.prompt import Prompt, Confirm from rich.panel import Panel +from rich.prompt import Confirm, Prompt console = Console(highlight=False) +class Settings: + """Opciones globales de ejecución, configuradas desde la línea de comandos.""" + + verbose: bool = False + + +settings = Settings() + + def success(msg: str): console.print(f":white_check_mark: [green]{msg}[/green]") diff --git a/src/git_flow/main.py b/src/git_flow/main.py index 6414b9e..fb37af8 100755 --- a/src/git_flow/main.py +++ b/src/git_flow/main.py @@ -1,12 +1,16 @@ #!/usr/bin/env python3 -import traceback -from subprocess import SubprocessError +from subprocess import CalledProcessError, SubprocessError +from typing import Annotated +import click import typer +from rich.markup import escape +from rich.traceback import Traceback from git_flow import GitFlowError from git_flow.command import base, branch, commit, init, merge, new, release, tag +from git_flow.io import console, settings REPOSITORY_TOKEN_PATH = ".repository-token" LOCALE = "es_AR.UTF-8" @@ -22,15 +26,55 @@ app.add_typer(release.app) app.add_typer(branch.app) +@app.callback() +def options( + verbose: Annotated[ + bool, + typer.Option( + "--verbose", + "-v", + help="Muestra la salida de los comandos git y el traceback de los errores", + ), + ] = False, +): + settings.verbose = verbose + + def main(): try: app() print() except GitFlowError as e: - lines = [str(e), ""] - lines += traceback.format_exception(e) - base.io_error("".join(lines), title="Git Flow") + _print_error(escape(str(e)), "Git Flow", e) + except CalledProcessError as e: + _print_error(_format_process_error(e), "Comando", e) except SubprocessError as e: - base.io_error(str(e), title="Comando") + _print_error(escape(str(e)), "Comando", e) except KeyboardInterrupt: base.io_error("Ejecución abortada") + + +def _format_process_error(e: CalledProcessError) -> str: + command = " ".join(e.cmd) if isinstance(e.cmd, list) else str(e.cmd) + message = f"[bold]$ {escape(command)}[/bold]\nTerminó con código de salida {e.returncode}" + + for output in (e.stdout, e.stderr): + if isinstance(output, str) and output.strip(): + message += "\n\n" + escape(output.strip()) + + return message + + +def _print_error(message: str, title: str, e: BaseException): + if settings.verbose: + base.io_error(message, title=title) + console.print( + Traceback.from_exception( + type(e), e, e.__traceback__, suppress=[click, typer] + ) + ) + else: + base.io_error( + message + "\n\n[dim]Ejecutar con `git-flow --verbose ` para ver el detalle[/dim]", + title=title, + )