Compare commits

..
4 Commits
4 changed files with 120 additions and 52 deletions
+30 -35
View File
@@ -1,5 +1,8 @@
# Git Flow # Git Flow
[![PyPI Version](https://img.shields.io/pypi/v/git-flow-envs)](https://pypi.org/project/git-flow-envs/)
![Python Version](https://img.shields.io/python/required-version-toml?tomlFilePath=https://git.jonathanteran.dev/jt/git-flow/raw/branch/main/pyproject.toml)
Herramienta para establecer un workflow siguiendo [Conventional Commits Herramienta para establecer un workflow siguiendo [Conventional Commits
(CC)](https://www.conventionalcommits.org), [Conventional Branch (CC)](https://www.conventionalcommits.org), [Conventional Branch
(CB)](https://conventional-branch.github.io/), y [SemVer](https://semver.org/). (CB)](https://conventional-branch.github.io/), y [SemVer](https://semver.org/).
@@ -9,27 +12,12 @@ derivar versiones automaticamente de los merges sobre ramas principales.
## Instalación ## Instalación
Para instalar Git Flow, se sugiere utilizar [pipx](https://github.com/pypa/pipx) (seguir la Se puede instalar con `pip` ejecutando el siguiente comando: `pip install git-flow-envs`. Sin
documentación para instalarlo). Una vez instalado `pipx`, simplemente clonamos el repositorio en una embargo, se sugiere utilizar [pipx](https://github.com/pypa/pipx) (seguir la documentación para
carpeta local, y lo instalamos: instalarlo), ya que aisla las dependencias de la herramienta en un `venv`. Una vez instalado,
simplemente ejecutamos `pipx install git-flow-envs`
```sh Luego podemos verificar que se haya instalado correctamente ejecutando: `git-flow --help`
cd ~/Downloads
git clone ssh://git@git.jonathanteran.dev:59169/jt/git-flow.git
cd git-flow
pipx install .
```
Luego podemos verificar que se haya instalado correctamente ejecutando: `git-flow -h`
Para actualizarlo, hacemos un pull de los cambios recientes del repositorio, y hacemos el upgrade
mediante `pipx`:
```sh
cd ~/Downloads/git-flow
git pull
pipx upgrade git-flow
```
## Configuración ## Configuración
@@ -46,10 +34,10 @@ El comando solicitará 2 opciones:
- **Remoto** (opcional): Nombre del remoto que se usará para crear PRs, y taggear merges - **Remoto** (opcional): Nombre del remoto que se usará para crear PRs, y taggear merges
automáticamente. Por el momento, se soportan los siguientes remotos: automáticamente. Por el momento, se soportan los siguientes remotos:
- [x] Bitbucket - [x] Bitbucket
- [x] Github - [x] Github
- [x] Gitea - [x] Gitea
- [x] Gitlab - [x] Gitlab
La configuración del remoto requiere tener un Access Token para poder crear los PRs y tags La configuración del remoto requiere tener un Access Token para poder crear los PRs y tags
automáticamente. Ver la sección [Remotos soportados](#remotos-soportados) para las instrucciones automáticamente. Ver la sección [Remotos soportados](#remotos-soportados) para las instrucciones
@@ -60,13 +48,13 @@ El comando solicitará 2 opciones:
## Utilización ## Utilización
Git Flow cuenta con los siguientes comandos para guiar el workflow (ver `git-flow <command> -h` para Git Flow cuenta con los siguientes comandos para guiar el workflow (ver `git-flow <command> --help` para
más información): más información):
- `init`: Inicializa el repositorio para utilizar git-flow (ramas principales y remoto). - `init`: Inicializa el repositorio para utilizar git-flow (ramas principales y remoto).
- `new`: Crea una nueva rama siguiendo CB. - `new`: Crea una nueva rama siguiendo CB.
- `commit`: Crea un commit siguiendo CC. - `commit`: Crea un commit siguiendo CC.
- `merge`: Mergea la rama actual a una de las ramas principales. - `merge`: Mergea la rama actual a uno de los entornos.
- `tag`: Crea un tag sobre el ultimo merge siguiendo SemVer (ver [Tag](#tag)). - `tag`: Crea un tag sobre el ultimo merge siguiendo SemVer (ver [Tag](#tag)).
- `release`: Prepara la rama actual para pasarse al siguiente entorno configurado. - `release`: Prepara la rama actual para pasarse al siguiente entorno configurado.
- `branch ls`: Lista las ramas del repositorio, agrupadas por entorno objetivo. - `branch ls`: Lista las ramas del repositorio, agrupadas por entorno objetivo.
@@ -74,6 +62,9 @@ más información):
`--remote`), dejando un reporte CSV (`branch-clean-report.csv`) con nombre, commit, autor y `--remote`), dejando un reporte CSV (`branch-clean-report.csv`) con nombre, commit, autor y
fecha de cada rama eliminada. fecha de cada rama eliminada.
Por defecto solo se muestra cada paso que se ejecuta. Con `git-flow --verbose <command>` (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: 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 1. Sobre la primer rama principal configurada (por ejemplo, `dev`), se crea una nueva rama con
@@ -153,12 +144,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 10
- uses: astral-sh/setup-uv@v3 - uses: astral-sh/setup-uv@v3
- run: uv run git-flow tag --token=${{ secrets.GIT_FLOW_TOKEN }} - run: uv run git-flow tag --token=${{ secrets.GIT_FLOW_TOKEN }}
- run: uv build - run: uv build
- run: uv publish --token=${{ secrets.PYPI_TOKEN }} - run: uv publish --token=${{ secrets.PYPI_TOKEN }}
``` ```
@@ -177,11 +167,14 @@ Ejemplo de pipeline (GitLab CI) para taggear automáticamente en cada push a una
# .gitlab-ci.yml # .gitlab-ci.yml
tag: tag:
image: python:3.14 image: python:3.14
variables:
GIT_DEPTH: 10
rules: rules:
- if: $CI_COMMIT_BRANCH == "main" - if: $CI_COMMIT_BRANCH == "main"
script: script:
- pip install uv - pip install uv
- uv run git-flow tag --token=$GIT_FLOW_TOKEN # GitLab hace checkout en detached HEAD, por lo que la rama se indica explícitamente
- uv run git-flow tag --target=$CI_COMMIT_BRANCH --token=$GIT_FLOW_TOKEN
- uv build - uv build
- uv publish --token=$PYPI_TOKEN - uv publish --token=$PYPI_TOKEN
``` ```
@@ -203,6 +196,9 @@ ver el archivo `bitbucket-pipelines.yml` como ejemplo:
# bitbucket-pipelines.yml # bitbucket-pipelines.yml
image: python:3.14 image: python:3.14
clone:
depth: 10
pipelines: pipelines:
branches: branches:
'main': 'main':
@@ -245,19 +241,18 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 10
- uses: astral-sh/setup-uv@v3 - uses: astral-sh/setup-uv@v3
- run: uv run git-flow tag --token=${{ secrets.GIT_FLOW_TOKEN }} - run: uv run git-flow tag --token=${{ secrets.GIT_FLOW_TOKEN }}
- run: uv build - run: uv build
- run: uv publish --token=${{ secrets.PYPI_TOKEN }} - run: uv publish --token=${{ secrets.PYPI_TOKEN }}
``` ```
## Taggeo automático por pipeline ## Taggeo automático por pipeline
En todos los casos, `git-flow tag` detecta que se está ejecutando desde un pipeline (no encuentra En todos los casos, al pasar `--token=<TOKEN>` `git-flow tag` asume que se está ejecutando desde un
`.repository-token` en el repositorio) y usa el token pasado por `--token=<TOKEN>` tanto para crear pipeline: obtiene los commits del último PR / MR y el último tag por API del remoto, y usa el token
el tag en el remoto como para pushear el commit del `CHANGELOG.md` generado, en vez de crear el tag tanto para crear el tag en el remoto como para pushear el commit del `CHANGELOG.md` generado (con
únicamente en forma local como sucede al ejecutarlo manualmente sin remoto configurado. `--update-changelog`), en vez de resolver todo en forma local.
+30 -10
View File
@@ -1,6 +1,10 @@
import subprocess import subprocess
from dataclasses import dataclass 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 import FLOWCONFIG_FILENAME
from git_flow.io import * from git_flow.io import *
@@ -180,10 +184,13 @@ class Git:
self._run(**kwargs) self._run(**kwargs)
def _get(self, print: str | None = None, check: bool = True): 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: # Sin --verbose solo se muestra el paso; si falla, la salida la muestra main()
self._print_process(process.stdout, process.stderr, print) 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: if check:
process.check_returncode() process.check_returncode()
@@ -193,7 +200,7 @@ class Git:
def _run(self, print: str | None = None, check: bool = True): def _run(self, print: str | None = None, check: bool = True):
return self._get(print, check) 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 = [] command = []
for arg in self.command: for arg in self.command:
@@ -205,12 +212,25 @@ class Git:
command.append(arg) 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(): for line in process.stdout.splitlines():
text += "\n" + line text.append("\n" + line)
for line in stderr.splitlines(): for line in process.stderr.splitlines():
text += "\n[red]" + line + "[/red]" 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,
)
)
+10 -1
View File
@@ -1,10 +1,19 @@
from rich.console import Console from rich.console import Console
from rich.prompt import Prompt, Confirm
from rich.panel import Panel from rich.panel import Panel
from rich.prompt import Confirm, Prompt
console = Console(highlight=False) 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): def success(msg: str):
console.print(f":white_check_mark: [green]{msg}[/green]") console.print(f":white_check_mark: [green]{msg}[/green]")
+50 -6
View File
@@ -1,12 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import traceback from subprocess import CalledProcessError, SubprocessError
from subprocess import SubprocessError from typing import Annotated
import click
import typer import typer
from rich.markup import escape
from rich.traceback import Traceback
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.command import base, branch, commit, init, merge, new, release, tag 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" REPOSITORY_TOKEN_PATH = ".repository-token"
LOCALE = "es_AR.UTF-8" LOCALE = "es_AR.UTF-8"
@@ -22,15 +26,55 @@ app.add_typer(release.app)
app.add_typer(branch.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(): def main():
try: try:
app() app()
print() print()
except GitFlowError as e: except GitFlowError as e:
lines = [str(e), ""] _print_error(escape(str(e)), "Git Flow", e)
lines += traceback.format_exception(e) except CalledProcessError as e:
base.io_error("".join(lines), title="Git Flow") _print_error(_format_process_error(e), "Comando", e)
except SubprocessError as e: except SubprocessError as e:
base.io_error(str(e), title="Comando") _print_error(escape(str(e)), "Comando", e)
except KeyboardInterrupt: except KeyboardInterrupt:
base.io_error("Ejecución abortada") 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 <comando>` para ver el detalle[/dim]",
title=title,
)