refactor: migrate from class commands to scripts commands

This commit is contained in:
jt
2026-05-17 11:19:30 -03:00
parent 9ffefafad0
commit 0ac04aee84
10 changed files with 775 additions and 812 deletions
+118 -156
View File
@@ -1,16 +1,5 @@
from argparse import Namespace, ArgumentParser
from abc import ABC, abstractmethod
from os.path import isfile
import rich
from rich.prompt import Prompt, Confirm
from git_flow import (
BRANCH_TYPES,
COLOR_BLUE,
COLOR_GREEN,
COLOR_RED,
COLOR_RESET,
COLOR_YELLOW,
REPOSITORY_TOKEN_FILENAME,
GitFlowError,
)
@@ -18,187 +7,160 @@ from git_flow.git import FLOWCONFIG_FILE, Git
from git_flow.remote.base import RemoteAPI
from git_flow.remote.bitbucket import BitbucketRemoteAPI
from git_flow.remote.github import GithubRemoteAPI
from os.path import isfile
from rich.console import Console
from rich.prompt import Prompt, Confirm
from rich.panel import Panel
TYPE_SUCCESS = 0
TYPE_WARNING = 1
TYPE_ERROR = 2
TYPE_INFO = 3
console = Console(highlight=False)
flowconfig = Git.get_config(FLOWCONFIG_FILE) if isfile(FLOWCONFIG_FILE) else {}
class Command(ABC):
flowconfig: dict[str, str]
@abstractmethod
def name(self) -> str:
pass
def success(msg: str):
console.print(f":white_check_mark: [green]{msg}[/green]")
@abstractmethod
def description(self) -> str:
pass
@abstractmethod
def run(self, args: Namespace = Namespace()):
pass
def warning(msg: str):
console.print(f":warning: [yellow]{msg}[/yellow]")
def init(self):
self.flowconfig = (
Git.get_config(FLOWCONFIG_FILE) if isfile(FLOWCONFIG_FILE) else {}
def error(msg: str):
console.print(f":x: [red]{msg}[/red]")
def info(msg: str):
console.print(f":information: [blue]{msg}[/blue]")
def panel(title: str, text: str):
console.print(Panel(text, title=title, expand=False, title_align="left"))
def prompt(
prompt: str,
default: str | None = None,
persistent: bool = False,
strip: bool = True,
) -> str:
onetime = not persistent
while onetime or persistent:
result = Prompt.ask(prompt)
result = result.strip() if strip else result
if result:
return result
elif default is not None:
return default
elif persistent:
error("Debe ingresar un valor no vacío.")
else:
return result
def confirm(question: str, default: bool = True):
return Confirm.ask(question, default=default)
def choice(prompt: str, options: list[str]):
return Prompt.ask(prompt, choices=options)
def ensure_initialized():
initialized = flowconfig["flow.initialized"] if flowconfig else None
if not initialized:
raise GitFlowError(
"El repositorio no fue inicializado, debe ejecutar el comando `init`."
)
elif initialized != "true":
raise GitFlowError(
f"El valor de `flow.initialized` ({initialized}) es inválido."
)
def success(self, msg: str):
rich.print(f":white_check_mark: [green]{msg}[/green]")
def warning(self, msg: str):
rich.print(f":warning: [yellow]{msg}[/yellow]")
def ensure_right_branch():
branch = Git.get_current_branch()
def error(self, msg: str):
rich.print(f":x: [red]{msg}[/red]")
if branch == "HEAD":
raise GitFlowError("No se encuentra parado sobre una rama.")
elif confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"):
return branch
else:
raise GitFlowError("Ejecución cancelada.")
def info(self, msg: str):
rich.print(f":information: [blue]{msg}[/blue]")
def prompt(
self,
prompt: str,
default: str | None = None,
persistent: bool = False,
strip: bool = True,
) -> str:
onetime = not persistent
def get_branch_env_and_type(branch: str) -> tuple[str, str]:
components = branch.split("/")
target_branches = flowconfig["flow.branches"].split(",")
while onetime or persistent:
result = Prompt.ask(prompt)
result = result.strip() if strip else result
if result:
return result
elif default is not None:
return default
elif persistent:
self.error("Debe ingresar un valor no vacío.")
else:
return result
def confirm(self, question: str, default: bool = True):
return Confirm.ask(question, default=default)
def choice(self, prompt: str, options: list[str]):
return Prompt.ask(prompt, choices = options)
def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
return parser
def ensure_initialized(self):
initialized = self.flowconfig["flow.initialized"] if self.flowconfig else None
if not initialized:
if len(components) == 2:
if components[0] not in BRANCH_TYPES:
raise GitFlowError(
"El repositorio no fue inicializado, debe ejecutar el comando `init`."
)
elif initialized != "true":
raise GitFlowError(
f"El valor de `flow.initialized` ({initialized}) es inválido."
f"Branch inválida, el tipo '{components[0]}' no es válido."
)
def ensure_right_branch(self):
branch = Git.get_current_branch()
return (target_branches[0], components[0])
elif len(components) == 4:
target_branches = target_branches[1:]
if branch == "HEAD":
raise GitFlowError("No se encuentra parado sobre una rama.")
elif self.confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"):
return branch
else:
raise GitFlowError("Ejecución cancelada.")
def ensure_repository_token(self):
if not isfile(REPOSITORY_TOKEN_FILENAME):
if (
components[0] != "release"
or components[1] not in target_branches
or components[2] not in BRANCH_TYPES
):
raise GitFlowError(
"No existe un token para el repositorio en " + REPOSITORY_TOKEN_FILENAME
"Branch release inválido, debe tener el siguiente formato: "
"release/<env>/<type>/<name>, pero es: " + branch
)
with open(REPOSITORY_TOKEN_FILENAME) as f:
return f.readline().strip()
return (components[1], components[2])
else:
raise GitFlowError("Branch inválido: " + branch)
def ensure_clean_worktree(self, has_remote: bool):
status = Git.status()
if not status:
return
def ensure_repository_token():
if not isfile(REPOSITORY_TOKEN_FILENAME):
raise GitFlowError(
"No existe un token para el repositorio en " + REPOSITORY_TOKEN_FILENAME
)
not_empty = any(map(lambda s: s.index != " " or s.worktree != " ", status))
with open(REPOSITORY_TOKEN_FILENAME) as f:
return f.readline().strip()
if not_empty:
if not has_remote:
self.warning("Existen cambios en tu entorno de trabajo sin commitear.")
else:
raise GitFlowError("No se puede continuar con cambios pendientes.")
if not self.confirm("¿Desea continuar?", False):
raise GitFlowError("Ejecución abortada")
def ensure_clean_worktree(has_remote: bool):
status = Git.status()
def get_branch_env_and_type(self, branch: str) -> tuple[str, str]:
components = branch.split("/")
target_branches = self.flowconfig["flow.branches"].split(",")
if not status:
return
if len(components) == 2:
if components[0] not in BRANCH_TYPES:
raise GitFlowError(
f"Branch inválida, el tipo '{components[0]}' no es válido."
)
not_empty = any(map(lambda s: s.index != " " or s.worktree != " ", status))
return (target_branches[0], components[0])
elif len(components) == 4:
target_branches = target_branches[1:]
if (
components[0] != "release"
or components[1] not in target_branches
or components[2] not in BRANCH_TYPES
):
raise GitFlowError(
"Branch release inválido, debe tener el siguiente formato: "
"release/<env>/<type>/<name>, pero es: " + branch
)
return (components[1], components[2])
if not_empty:
if not has_remote:
warning("Existen cambios en tu entorno de trabajo sin commitear.")
else:
raise GitFlowError("Branch inválido: " + branch)
raise GitFlowError("No se puede continuar con cambios pendientes.")
def get_remote_api(self, token: str) -> RemoteAPI:
if "flow.remote" not in self.flowconfig:
raise GitFlowError("El repositorio no tiene configurado un remoto.")
if not confirm("¿Desea continuar?", False):
raise GitFlowError("Ejecución abortada")
[host, repository] = RemoteAPI.parse(self.flowconfig["flow.remote"])
if host == "bitbucket.org":
return BitbucketRemoteAPI(repository, token)
elif host == "github.com":
return GithubRemoteAPI(repository, token)
else:
raise GitFlowError("El host del repositorio remoto es inválido")
def get_remote_api(token: str) -> RemoteAPI:
if "flow.remote" not in flowconfig:
raise GitFlowError("El repositorio no tiene configurado un remoto.")
def _print(self, type: int, msg: str):
print(self._get_color(type) + self._get_tag(type) + " " + msg + COLOR_RESET)
[host, repository] = RemoteAPI.parse(flowconfig["flow.remote"])
def _get_color(self, type: int) -> str:
if type == TYPE_SUCCESS:
return COLOR_GREEN
elif type == TYPE_WARNING:
return COLOR_YELLOW
elif type == TYPE_ERROR:
return COLOR_RED
elif type == TYPE_INFO:
return COLOR_BLUE
return ""
def _get_tag(self, type: int) -> str:
if type == TYPE_SUCCESS:
return "[success]"
elif type == TYPE_WARNING:
return "[warning]"
elif type == TYPE_ERROR:
return "[error]"
elif type == TYPE_INFO:
return "[info]"
return ""
if host == "bitbucket.org":
return BitbucketRemoteAPI(repository, token)
elif host == "github.com":
return GithubRemoteAPI(repository, token)
else:
raise GitFlowError("El host del repositorio remoto es inválido")
+125 -101
View File
@@ -1,117 +1,141 @@
from argparse import ArgumentParser, Namespace
from git_flow import COLOR_WHITE_BOLD, COLOR_GREEN, COLOR_RED, COLOR_RESET, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
from git_flow.command.base import Command
from git_flow import (
TRASH_BRANCH_PREFIX,
WIP_BRANCH_PREFIX,
GitFlowError,
)
from git_flow.command.base import *
from git_flow.git import Git
from typing import Annotated
import typer
BRANCH_FORMAT = "%(refname:short)"
app = typer.Typer()
class BranchCommand(Command):
def name(self) -> str:
return "branch"
def description(self) -> str:
return """Lista ramas del repositorio, agrupandolas por entorno objetivo"""
def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
parser.add_argument(
"environment",
nargs="?",
help="Entorno de las ramas a listar. Por defecto, es el entorno actual.",
)
parser.add_argument(
"--trash",
action="store_true",
@app.command()
def branch(
environment: Annotated[
str | None,
typer.Argument(
help="Entorno de las ramas a listar. Por defecto, es el entorno actual."
),
] = None,
trash: Annotated[
bool,
typer.Option(
help=f"Listar ramas en la 'papelera de reciclaje' (ramas que empiezan con '{TRASH_BRANCH_PREFIX}')"
)
parser.add_argument(
"--wip",
action="store_true",
),
] = False,
wip: Annotated[
bool,
typer.Option(
help=f"Listar ramas 'en progreso' (ramas que empiezan con '{WIP_BRANCH_PREFIX}')"
),
] = False,
all: Annotated[
bool, typer.Option(help="Listar ramas de todos los entornos")
] = False,
):
"""Lista ramas del repositorio, agrupandolas por entorno objetivo"""
ensure_initialized()
current_branch = Git.get_current_branch()
environments = flowconfig["flow.branches"].split(",")
if current_branch in environments:
current_environment = current_branch
else:
current_environment, _ = get_branch_env_and_type(current_branch)
if trash:
show_branches(TRASH_BRANCH_PREFIX, current_branch)
elif wip:
show_branches(WIP_BRANCH_PREFIX, current_branch)
elif all:
show_envs(environments, current_branch)
show_all_branches(environments, current_branch)
elif environment is None:
show_envs(environments, current_branch)
show_env_branches(current_environment, environments, current_branch)
elif environment in environments:
show_envs(environments, current_branch)
show_env_branches(environment, environments, current_branch)
else:
raise GitFlowError(f"'{environment}' no es un entorno válido.")
def show_branches(branch_prefix: str, current_branch: str):
branches = get_branches(branch_prefix)
if not branches:
print("No hay ramas a mostrar.")
return
for branch in branches:
is_current = branch == current_branch
prefix = "*" if is_current else " "
console.print(prefix + " " + branch, style="green" if is_current else None)
def get_branches(branch_prefix: str):
return Git("for-each-ref", "refs/heads/" + branch_prefix, format=BRANCH_FORMAT).lines()
def show_all_branches(environments: list[str], current_branch: str):
for environment in environments:
show_env_branches(
environment, environments, current_branch, len(environments) > 1
)
parser.add_argument(
"--all",
action="store_true",
help=f"Listar ramas de todos los entornos"
)
trash = len(get_branches(TRASH_BRANCH_PREFIX))
return parser
def run(self, args: Namespace = Namespace()):
self.ensure_initialized()
self.current_branch = Git.get_current_branch()
self.environments = self.flowconfig["flow.branches"].split(",")
if self.current_branch in self.environments:
current_environment = self.current_branch
else:
(current_environment, _) = self.get_branch_env_and_type(self.current_branch)
if args.trash:
self.show_branches(TRASH_BRANCH_PREFIX)
elif args.wip:
self.show_branches(WIP_BRANCH_PREFIX)
elif args.all:
self.show_envs()
self.show_all_branches()
elif args.environment is None:
self.show_envs()
self.show_env_branches(current_environment)
elif args.environment in self.environments:
self.show_envs()
self.show_env_branches(args.environment)
else:
raise GitFlowError(f"'{args.environment}' no es un entorno válido.")
def show_branches(self, prefix: str):
branches = self.get_branches(prefix)
if not branches:
print("No hay ramas a mostrar.")
return
for branch in branches:
prefix = (COLOR_RED + "*") if branch == self.current_branch else " "
print(prefix + " " + branch + COLOR_RESET)
def get_branches(self, prefix: str):
return Git("for-each-ref", "refs/heads/" + prefix, format=BRANCH_FORMAT).lines()
def show_all_branches(self):
for environment in self.environments:
self.show_env_branches(environment, len(self.environments) > 1)
trash = len(self.get_branches(TRASH_BRANCH_PREFIX))
if trash:
print()
print(COLOR_WHITE_BOLD + "Papelera: " + COLOR_RESET + str(trash) + " rama" + ("" if trash == 1 else "s"))
def show_envs(self):
print(COLOR_WHITE_BOLD + "Entornos" + COLOR_RESET)
for environment in self.environments:
prefix = (COLOR_GREEN + "*") if environment == self.current_branch else " "
print(prefix + " " + environment + COLOR_RESET)
if trash:
print()
console.print(
"[bold]Papelera:[/bold] "
+ str(trash)
+ " rama"
+ ("" if trash == 1 else "s")
)
def show_env_branches(self, environment: str, show_env: bool = False):
is_first_env = environment == self.environments[0]
pattern = "refs/heads/" if is_first_env else "refs/heads/release/" + environment + "/"
pattern += "*/*"
exclude = ["refs/heads/" + TRASH_BRANCH_PREFIX, "refs/heads/release/"] if is_first_env else []
branches = Git("for-each-ref", pattern, exclude=exclude, format=BRANCH_FORMAT).lines()
print(COLOR_WHITE_BOLD + (environment if show_env else "Ramas") + COLOR_RESET)
def show_envs(environments: list[str], current_branch: str):
console.print("Entornos", style="bold")
for environment in environments:
is_current = environment == current_branch
prefix = "*" if is_current else " "
console.print(prefix + " " + environment, style="green" if is_current else None)
print()
if not branches:
print("No hay ramas a mostrar.")
return
for branch in branches:
prefix = (COLOR_GREEN + "*") if branch == self.current_branch else " "
print(prefix + " " + branch + COLOR_RESET)
def show_env_branches(
environment: str,
environments: list[str],
current_branch: str,
show_env: bool = False,
):
is_first_env = environment == environments[0]
pattern = (
"refs/heads/" if is_first_env else "refs/heads/release/" + environment + "/"
)
pattern += "*/*"
exclude = (
["refs/heads/" + TRASH_BRANCH_PREFIX, "refs/heads/release/"]
if is_first_env
else []
)
branches = Git(
"for-each-ref", pattern, exclude=exclude, format=BRANCH_FORMAT
).lines()
console.print(environment if show_env else "Ramas", style="bold")
if not branches:
print("No hay ramas a mostrar.")
return
for branch in branches:
is_current = branch == current_branch
prefix = "*" if is_current else " "
console.print(prefix + " " + branch, style="green" if is_current else None)
+51 -49
View File
@@ -1,62 +1,64 @@
from argparse import Namespace
import typer
from datetime import datetime
from git_flow import COMMIT_TYPES, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
from git_flow.command.base import Command
from git_flow.git import Git
from git_flow.command.base import *
app = typer.Typer()
class CommitCommand(Command):
def name(self) -> str:
return "commit"
@app.command()
def commit():
"""Crea un commit siguiendo el formato de Conventional Commits"""
ensure_initialized()
branch = ensure_right_branch()
def description(self) -> str:
return """Crea un commit siguiendo el formato de Conventional Commits"""
def run(self, args: Namespace = Namespace()):
self.ensure_initialized()
branch = self.ensure_right_branch()
if not self._has_files_staged():
self.warning("No hay cambios en el indice para commitear.")
if self.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 = self.choice("Tipo de commit", COMMIT_TYPES)
commit_message = self.prompt(
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
)
message = commit_type + ": " + commit_message
if branch.startswith(WIP_BRANCH_PREFIX):
original_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
Git("commit", m=message).exec(print="Creando commit en rama WIP")
if commit_type != "wip":
suffix = datetime.now().strftime("%Y%m%dT%H%M")
Git("switch", original_branch).exec(print="Volviendo a rama original")
Git("merge", branch, squash=True).exec(print="Squasheando commits WIP en uno solo")
Git("commit", m=message).exec(print="Creando commit final de rama WIP")
Git("branch", branch, TRASH_BRANCH_PREFIX + branch + "/" + suffix, move=True).exec(print="Backup de rama WIP")
if not _has_files_staged():
warning("No hay cambios en el indice para commitear.")
if confirm("¿Desea agregar la carpeta actual?"):
Git("add", ".").exec(print="Agregando cambios")
else:
if commit_type == "wip":
Git("switch", WIP_BRANCH_PREFIX + branch, create=True).exec(print="Creando nueva rama WIP")
raise GitFlowError("Debe agregar algún cambio al indice para continuar.")
Git("commit", m=message).exec(print="Creando commit")
commit_type = choice("Tipo de commit", COMMIT_TYPES)
commit_message = prompt(
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
)
message = commit_type + ": " + commit_message
def _has_files_staged(self):
status = Git.status()
if branch.startswith(WIP_BRANCH_PREFIX):
original_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
Git("commit", m=message).exec(print="Creando commit en rama WIP")
if not status:
raise GitFlowError("No hay cambios para commitear.")
if commit_type != "wip":
suffix = datetime.now().strftime("%Y%m%dT%H%M")
Git("switch", original_branch).exec(print="Volviendo a rama original")
Git("merge", branch, squash=True).exec(
print="Squasheando commits WIP en uno solo"
)
Git("commit", m=message).exec(print="Creando commit final de rama WIP")
Git(
"branch", branch, TRASH_BRANCH_PREFIX + branch + "/" + suffix, move=True
).exec(print="Backup de rama WIP")
else:
if commit_type == "wip":
Git("switch", WIP_BRANCH_PREFIX + branch, create=True).exec(
print="Creando nueva rama WIP"
)
files_in_index = []
Git("commit", m=message).exec(print="Creando commit")
for s in status:
if s.worktree != "?" and s.worktree != " ":
files_in_index.append(s.file)
return len(files_in_index) > 0
def _has_files_staged():
status = Git.status()
if not status:
raise GitFlowError("No hay cambios para commitear.")
files_in_index = []
for s in status:
if s.worktree != "?" and s.worktree != " ":
files_in_index.append(s.file)
return len(files_in_index) > 0
+93 -90
View File
@@ -1,115 +1,118 @@
from argparse import Namespace
import typer
from git_flow import GitFlowError
from git_flow.command.base import Command
from git_flow.git import FLOWCONFIG_FILE, Git
from git_flow.command.base import *
app = typer.Typer()
class InitCommand(Command):
def name(self) -> str:
return "init"
@app.command()
def init():
"""Inicializa el repositorio para utilizar git-flow"""
def description(self) -> str:
return """Inicializa el repositorio para utilizar git-flow"""
_ensure_is_repository()
_ensure_not_already_initialized()
def run(self, args: Namespace = Namespace()):
self._ensure_is_repository()
self._ensure_not_already_initialized()
branches = _setup_flow_branches()
remote = _setup_flow_remote()
branches = self._setup_flow_branches()
remote = self._setup_flow_remote()
flowconfig = {
"flow.initialized": "true",
"flow.branches": ",".join(branches),
}
flowconfig = {
"flow.initialized": "true",
"flow.branches": ",".join(branches),
}
if remote:
flowconfig["flow.remote"] = remote
if remote:
flowconfig["flow.remote"] = remote
Git.set_config(flowconfig, FLOWCONFIG_FILE)
Git("add", FLOWCONFIG_FILE).exec()
Git("commit", message="feature: initialize git-flow").exec()
Git.set_config(flowconfig, FLOWCONFIG_FILE)
Git("add", FLOWCONFIG_FILE).exec()
Git("commit", message="feature: initialize git-flow").exec()
_ensure_all_flow_branches_exist(branches, remote)
self._ensure_all_flow_branches_exist(branches, remote)
def _ensure_is_repository(self):
if not Git.is_repository():
self.warning("El directorio actual no es un repositorio.")
def _ensure_is_repository():
if not Git.is_repository():
warning("El directorio actual no es un repositorio.")
if not self.confirm("¿Desea inicializarlo?"):
raise GitFlowError(
"No se puede continuar sin inicializar el repositorio"
)
if not confirm("¿Desea inicializarlo?"):
raise GitFlowError("No se puede continuar sin inicializar el repositorio")
Git("init").exec()
Git("init").exec()
def _ensure_not_already_initialized(self):
if not self.flowconfig:
return
elif self.flowconfig.get("flow.initialized") == "true":
raise GitFlowError("El repositorio ya fue inicializado para usar git-flow")
def _ensure_not_already_initialized():
flowconfig = get_config()
if not flowconfig:
return
elif flowconfig.get("flow.initialized") == "true":
raise GitFlowError("El repositorio ya fue inicializado para usar git-flow")
else:
raise GitFlowError("Valor de 'flow.initialized' es inválido")
def _setup_flow_branches():
panel(
"Configuración de entornos",
"""Ingrese las ramas que representan los entornos de deploy del proyecto
en orden creciente de cercanía al entorno productivo, y separados por
coma. Por ejemplo: "dev, test, prod".""",
)
branches = list(map(lambda b: b.strip(), prompt("Ramas", "main").split(",")))
if not all(branches):
raise GitFlowError("No puede ingresar una rama vacia")
return branches
def _setup_flow_remote():
panel(
"Configuración de remoto",
"Puede configurar un repositorio remoto para generar PRs automáticamente",
)
remote = None
if confirm("¿Configurar repositorio remoto?"):
remotes = Git("remote").lines(print="Listando remotos disponibles")
if not remotes:
info("No tiene ningún repositorio remoto, se creará uno.")
url = prompt(
"Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)",
persistent=True,
)
remote = "origin"
Git("remote", "add", remote, url).exec(print="Agregando remoto")
elif len(remotes) == 1:
remote = remotes[0]
else:
raise GitFlowError("Valor de 'flow.initialized' es inválido")
info("Tiene más de un remoto, seleccione el que va a utilizar.")
remote = choice("Remoto", remotes)
def _setup_flow_branches(self):
self.info(
"""Ingrese las ramas que representan los entornos de deploy del proyecto en
orden creciente de cercanía al entorno productivo, y separados por coma.
Por ejemplo: "dev,test,prod"."""
)
return remote
branches = list(
map(lambda b: b.strip(), self.prompt("Ramas", "main").split(","))
)
if not all(branches):
raise GitFlowError("No puede ingresar una rama vacia")
def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None):
existing_branches = Git.get_branches()
return branches
if not all(map(lambda b: b in existing_branches, branches)):
warning("Algunas de las ramas de entornos expecificadas no existen.")
info("Debe indicar sobre que rama se crearán las ramas de entorno.")
def _setup_flow_remote(self):
self.info(
"""Puede elegir o crear un repositorio remoto para generar automáticamente PRs"""
)
target_branch = choice("Rama", existing_branches)
remote = None
if self.confirm("¿Configurar repositorio remoto?"):
remotes = Git("remote").lines(print="Listando remotos disponibles")
if not remotes:
self.info("No tiene ningún repositorio remoto, se creará uno.")
url = self.prompt(
"Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)",
persistent=True,
for branch in branches:
if branch not in existing_branches:
Git("branch", branch, target_branch).exec(
print="Creando rama inexistente"
)
remote = "origin"
Git("remote", "add", remote, url).exec(print="Agregando remoto")
elif len(remotes) == 1:
remote = remotes[0]
else:
self.info("Tiene más de un remoto, seleccione el que va a utilizar.")
remote = self.choice("Remoto", remotes)
return remote
def _ensure_all_flow_branches_exist(self, branches: list[str], remote: str | None):
existing_branches = Git.get_branches()
if not all(map(lambda b: b in existing_branches, branches)):
self.warning("Algunas de las ramas de entornos expecificadas no existen.")
self.info("Debe indicar sobre que rama se crearán las ramas de entorno.")
target_branch = self.choice("Rama", existing_branches)
for branch in branches:
if branch not in existing_branches:
Git("branch", branch, target_branch).exec(
print="Creando rama inexistente"
if remote:
Git("push", remote, branch, set_upstream=True).exec(
print="Creando rama en remoto"
)
if remote:
Git("push", remote, branch, set_upstream=True).exec(
print="Creando rama en remoto"
)
+81 -79
View File
@@ -1,111 +1,113 @@
from argparse import Namespace
import typer
from git_flow import GitFlowError
from git_flow.changelog import Changelog
from git_flow.command.base import Command
from git_flow.command.base import *
from git_flow.git import Git
app = typer.Typer()
class MergeCommand(Command):
def name(self) -> str:
return "merge"
def description(self) -> str:
return """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
@app.command()
def merge():
"""Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
ensure_initialized()
def run(self, args: Namespace = Namespace()):
self.ensure_initialized()
branch = ensure_right_branch()
target, _ = get_branch_env_and_type(branch)
branch = self.ensure_right_branch()
(target, _) = self.get_branch_env_and_type(branch)
if "flow.remote" in flowconfig:
remote = flowconfig["flow.remote"]
token = ensure_repository_token()
run_remote(remote, token, branch, target)
else:
run_local(branch, target)
if "flow.remote" in self.flowconfig:
remote = self.flowconfig["flow.remote"]
token = self.ensure_repository_token()
self.run_remote(remote, token, branch, target)
else:
self.run_local(branch, target)
def run_remote(self, remote: str, token: str, branch: str, target: str):
self.ensure_clean_worktree(True)
def run_remote(remote: str, token: str, branch: str, target: str):
ensure_clean_worktree(True)
Git("switch", target).exec(print="Cambiando a rama destino")
Git("pull").exec(print="Obteniendo ultimos cambios del remoto")
Git("switch", "-").exec(print="Volviendo a rama a mergear")
check_merge_conflicts(target)
show_commits_to_merge(target)
if confirm(f"¿Crear PR de '{branch}' a '{target}'?"):
Git("push", remote, branch, set_upstream=True).exec(
print="Subiendo rama al remoto para crear PR"
)
create_pull_request(token, branch, target)
def run_local(branch: str, target: str):
ensure_clean_worktree(False)
check_merge_conflicts(target)
show_commits_to_merge(target)
if confirm(f"¿Mergear rama '{branch}' a '{target}'?"):
Git("switch", target).exec(print="Cambiando a rama destino")
Git("pull").exec(print="Obteniendo ultimos cambios del remoto")
Git("switch", "-").exec(print="Volviendo a rama a mergear")
Git("merge", branch, ff=False).exec(print="Mergeando")
self.check_merge_conflicts(target)
self.show_commits_to_merge(target)
if self.confirm(f"¿Crear PR de '{branch}' a '{target}'?"):
Git("push", remote, branch, set_upstream=True).exec(
print="Subiendo rama al remoto para crear PR"
)
def show_commits_to_merge(target):
commits = Git("log", target + "..", format="%s").lines()
self.create_pull_request(token, branch, target)
if not commits:
raise GitFlowError("No hay cambios a mergear.")
def run_local(self, branch: str, target: str):
self.ensure_clean_worktree(False)
self.check_merge_conflicts(target)
self.show_commits_to_merge(target)
print("Cambios a mergear:")
if self.confirm(f"¿Mergear rama '{branch}' a '{target}'?"):
Git("switch", target).exec(print="Cambiando a rama destino")
Git("merge", branch, ff=False).exec(print="Mergeando")
for commit in commits:
print("- " + commit)
def show_commits_to_merge(self, target):
commits = Git("log", target + "..", format="%s").lines()
if not commits:
raise GitFlowError("No hay cambios a mergear.")
def check_merge_conflicts(target: str):
merge_conflicts = Git("merge", target, ff=False, commit=False).code(
print="Realizando merge de prueba para verificar conflictos"
)
print("Cambios a mergear:")
Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False)
for commit in commits:
print("- " + commit)
def check_merge_conflicts(self, target: str):
merge_conflicts = Git("merge", target, ff=False, commit=False).code(
print="Realizando merge de prueba para verificar conflictos"
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"""
)
raise GitFlowError("Ejecución abortada.")
else:
success("No se detectaron merge conflicts.")
Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False)
if merge_conflicts:
self.error(f"La rama actual tiene conflictos con {target}")
self.info(
f"""Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente, y ejecutar nuevamente este comando"""
)
raise GitFlowError("Ejecución abortada.")
else:
self.success("No se detectaron merge conflicts.")
def create_pull_request(token: str, branch: str, target: str):
changelog = Changelog()
commits = Git("log", target + "..", format="%s").lines()
def create_pull_request(self, token: str, branch: str, target: str):
changelog = Changelog()
commits = Git("log", target + "..", format="%s").lines()
if len(commits) == 1:
title = commits[0]
else:
title = branch.replace("/", ": ").replace("-", " ")
if len(commits) == 1:
title = commits[0]
else:
title = branch.replace("/", ": ").replace("-", " ")
if title.startswith("release: "):
title = title[title.index(" ", title.index(" ") + 1) + 1 :]
if title.startswith("release: "):
title = title[title.index(' ', title.index(' ') + 1) + 1:]
info("Título del PR por defecto: " + title)
self.info("Título del PR por defecto: " + title)
opt_title = prompt("[Opcional] Ingrese otro titulo para el PR")
opt_title = self.prompt("[Opcional] Ingrese otro titulo para el PR")
if opt_title:
title = opt_title
if opt_title:
title = opt_title
message = get_remote_api(token).create_pull_request(
branch,
target,
title,
changelog.generate_content(target, branch),
)
message = self.get_remote_api(token).create_pull_request(
branch,
target,
title,
changelog.generate_content(target, branch),
)
success(message)
self.success(message)
if self.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")
if 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")
+37 -40
View File
@@ -1,56 +1,53 @@
from argparse import Namespace
import typer
from git_flow import BRANCH_TYPES
from git_flow.command.base import Command
from git_flow.command.base import *
from git_flow.git import Git
app = typer.Typer()
class NewCommand(Command):
def name(self) -> str:
return "new"
def description(self) -> str:
return """Crea una nueva rama siguiendo conventional branches"""
@app.command()
def new():
"""Crea una nueva rama siguiendo Conventional Branches"""
def run(self, args: Namespace = Namespace()):
self.ensure_initialized()
branch = self.ensure_right_branch()
envs = self.flowconfig["flow.branches"].split(",")
ensure_initialized()
branch = ensure_right_branch()
envs = flowconfig["flow.branches"].split(",")
if branch not in envs:
self.warning("La rama actual no corresponde a un entorno configurado.")
if branch not in envs:
warning("La rama actual no corresponde a un entorno configurado.")
self.info("Se creará una nueva rama de trabajo sobre la rama actual.")
self.info("Debe seleccionar el tipo de cambio a realizar.")
info("Se creará una nueva rama de trabajo sobre la rama actual.")
info("Debe seleccionar el tipo de cambio a realizar.")
branch_type = self.choice("Tipos de cambio", BRANCH_TYPES)
new_branch = self._get_unique_branch_name(branch_type)
branch_type = choice("Tipos de cambio", BRANCH_TYPES)
new_branch = _get_unique_branch_name(branch_type)
if "flow.remote" in self.flowconfig and Git.get_tracking_branch(branch):
Git("pull").exec(print="Actualizando rama actual")
if "flow.remote" in flowconfig and Git.get_tracking_branch(branch):
Git("pull").exec(print="Actualizando rama actual")
if self.confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"):
Git("switch", new_branch, create=True).exec(print="Creando nueva rama")
if confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"):
Git("switch", new_branch, create=True).exec(print="Creando nueva rama")
def _get_unique_branch_name(self, branch_type: str) -> str:
self.info(
f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'."
def _get_unique_branch_name(branch_type: str) -> str:
info(f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.")
branch = None
branches = Git.get_branches()
while branch is None:
keywords = prompt(
"Palabras clave del cambio (por ejemplo: 'create worker form')",
persistent=True,
)
branch = None
branches = Git.get_branches()
while branch is None:
keywords = self.prompt(
"Palabras clave del cambio (por ejemplo: 'create worker form')",
persistent=True,
keywords = filter(lambda k: len(k) > 0, keywords.split(" "))
branch = branch_type + "/" + "-".join(keywords)
if branch in branches:
error(
f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra"
)
branch = None
keywords = filter(lambda k: len(k) > 0, keywords.split(" "))
branch = branch_type + "/" + "-".join(keywords)
if branch in branches:
self.error(
f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra"
)
branch = None
return branch
return branch
+76 -89
View File
@@ -1,105 +1,92 @@
from argparse import ArgumentParser, Namespace
from git_flow import COMMIT_TYPES, GitFlowError
from git_flow.command.base import Command
from git_flow.command.base import *
from git_flow.git import Git
from typing import Optional
import typer
app = typer.Typer()
class ReleaseCommand(Command):
def name(self) -> str:
return "release"
@app.command()
def release(group: Optional[str] = None):
"""Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
ensure_initialized()
def description(self) -> str:
return """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
branch = ensure_right_branch()
envs = flowconfig["flow.branches"].split(",")
env, _ = get_branch_env_and_type(branch)
def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
parser.add_argument(
"--group",
nargs="?",
help="Agrupar release",
if env == envs[-1]:
raise GitFlowError(
"No se puede hacer release de una rama en el ultimo entorno."
)
return parser
has_remote = "flow.remote" in flowconfig
def run(self, args: Namespace = Namespace()):
self.ensure_initialized()
ensure_clean_worktree(has_remote)
branch = self.ensure_right_branch()
envs = self.flowconfig["flow.branches"].split(",")
(env, _) = self.get_branch_env_and_type(branch)
next_env = envs[envs.index(env) + 1]
next_branch = (
branch.replace(f"/{env}/", f"/{next_env}/")
if branch.startswith("release/")
else f"release/{next_env}/{branch}"
)
if env == envs[-1]:
raise GitFlowError(
"No se puede hacer release de una rama en el ultimo entorno."
base = Git.get_first_fork_point(branch, env)
commits = Git("log", base + "..", format="%s").lines()
if not commits:
raise GitFlowError("No hay cambios a mergear.")
print("Cambios a pasar al proximo entorno:")
for commit in commits:
print("- " + commit)
grouping = group is not None
if not group:
if confirm("¿Desea agrupar este release con otra rama?", False):
grouping = True
group = choice(
"Grupo release: ", Git.get_branches("release/" + next_env + "/")
)
has_remote = "flow.remote" in self.flowconfig
self.ensure_clean_worktree(has_remote)
next_env = envs[envs.index(env) + 1]
next_branch = (
branch.replace(f"/{env}/", f"/{next_env}/")
if branch.startswith("release/")
else f"release/{next_env}/{branch}"
)
base = Git.get_first_fork_point(branch, env)
commits = Git("log", base + ".." , format="%s").lines()
if not commits:
raise GitFlowError("No hay cambios a mergear.")
print("Cambios a pasar al proximo entorno:")
for commit in commits:
print("- " + commit)
group = args.group
grouping = group is not None
if not grouping:
if self.confirm("¿Desea agrupar este release con otra rama?", False):
grouping = True
group = self.choice("Grupo release: ", Git.get_branches("release/" + next_env + "/"))
else:
group = next_env
if has_remote and Git.get_tracking_branch(group):
Git("switch", group).exec(print="Cambiando a la rama objetivo")
Git("pull").exec(print="Sincronizando cambios la rama objetivo")
Git("switch", "-").exec(print="Volviendo a la rama original")
if len(commits) > 1 and self.confirm(
f"¿Desea reemplazar los {len(commits)} commits de la rama por uno solo?",
False
):
self.info("Debe ingresar el mensaje del commit a crear.")
commit_type = self.choice("Tipo de commit", COMMIT_TYPES[:-1])
commit_message = self.prompt(
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
)
message = commit_type + ": " + commit_message
Git("switch", next_branch, base, create=True).exec(
print="Creando rama release en base"
)
Git("merge", branch, squash=True).exec(
print="Squasheando commits en uno solo"
)
Git("commit", m=message).exec(print="Creando commit único")
else:
Git("switch", next_branch, create=True).exec(print="Creando rama release")
group = next_env
status = Git("rebase", base, next_branch, onto=group).code(
print="Moviendo cambios hacia el siguiente ambiente"
if has_remote and Git.get_tracking_branch(group):
Git("switch", group).exec(print="Cambiando a la rama objetivo")
Git("pull").exec(print="Sincronizando cambios la rama objetivo")
Git("switch", "-").exec(print="Volviendo a la rama original")
if len(commits) > 1 and confirm(
f"¿Desea reemplazar los {len(commits)} commits de la rama por uno solo?", False
):
info("Debe ingresar el mensaje del commit a crear.")
commit_type = choice("Tipo de commit", COMMIT_TYPES[:-1])
commit_message = prompt(
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
)
message = commit_type + ": " + commit_message
if status:
Git("rebase", abort=True).exec(
print="Deshaciendo cambios por conflictos"
)
raise GitFlowError("Error al realizar pasaje de cambios a rama objetivo")
elif grouping:
Git("switch", group).exec(print="Cambiando a rama objetivo")
Git("merge", next_branch, ff_only=True).exec(print="Mergeando cambios de la nueva rama")
Git("switch", next_branch, base, create=True).exec(
print="Creando rama release en base"
)
Git("merge", branch, squash=True).exec(print="Squasheando commits en uno solo")
Git("commit", m=message).exec(print="Creando commit único")
else:
Git("switch", next_branch, create=True).exec(print="Creando rama release")
status = Git("rebase", base, next_branch, onto=group).code(
print="Moviendo cambios hacia el siguiente ambiente"
)
if status:
Git("rebase", abort=True).exec(print="Deshaciendo cambios por conflictos")
raise GitFlowError("Error al realizar pasaje de cambios a rama objetivo")
elif grouping:
Git("switch", group).exec(print="Cambiando a rama objetivo")
Git("merge", next_branch, ff_only=True).exec(
print="Mergeando cambios de la nueva rama"
)
+73 -72
View File
@@ -1,4 +1,3 @@
from argparse import ArgumentParser, Namespace
from git_flow import (
COMMIT_TYPE_INCREMENT,
SEMVER_MAJOR,
@@ -8,100 +7,102 @@ from git_flow import (
GitFlowError,
)
from git_flow.changelog import Changelog
from git_flow.command.base import Command
from git_flow.command.base import *
from git_flow.git import Git
from typing import Optional
import typer
app = typer.Typer()
class TagCommand(Command):
def name(self) -> str:
return "tag"
@app.command()
def tag(token: Optional[str] = None):
"""Crea un nuevo tag para el último merge."""
ensure_initialized()
def description(self) -> str:
return """Crea un nuevo tag para el último merge."""
envs = flowconfig["flow.branches"].split(",")
target = Git.get_current_branch()
def run(self, args: Namespace = Namespace()):
self.ensure_initialized()
if target not in envs:
raise GitFlowError("Solo se pueden taggear commits en ramas principales.")
envs = self.flowconfig["flow.branches"].split(",")
target = Git.get_current_branch()
branch = Git(
"show", get_last_merge_commit() + "^2", patch=False, format="%h"
).firstline()
base = Git.get_first_fork_point(branch, target)
commits = Git("log", base + ".." + branch, format="%s").lines()
next_tag = get_next_tag_from_commits(commits, target)
if target not in envs:
raise GitFlowError("Solo se pueden taggear commits en ramas principales.")
if not next_tag:
info(
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
)
return
branch = Git("show", self.get_last_merge_commit() + "^2", patch=False, format="%h").firstline()
base = Git.get_first_fork_point(branch, target)
commits = Git("log", base + ".." + branch, format="%s").lines()
next_tag = self.get_next_tag_from_commits(commits, target)
token, ci = get_token_and_ci(token)
if not next_tag:
self.info(
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
)
return
if not token:
raise GitFlowError("No hay token disponible")
(token, ci) = self.get_token_and_ci(args)
changelog = Changelog()
changelog = Changelog()
changelog.update(next_tag, base, branch)
Git("add", Changelog.FILENAME).exec(print="Agregando nuevo CHANGELOG.md")
Git("commit", message=Changelog.COMMIT_MESSAGE).exec(print="Creando commit")
tag_commit = Git("show", "HEAD", patch=False, format="%h").firstline()
changelog.update(next_tag, base, branch)
Git("add", Changelog.FILENAME).exec(print="Agregando nuevo CHANGELOG.md")
Git("commit", message=Changelog.COMMIT_MESSAGE).exec(print="Creando commit")
tag_commit = Git("show", "HEAD", patch=False, format="%h").firstline()
if ci:
Git("push").exec(print="Subiendo commit al remoto para taggearlo")
self.success(self.get_remote_api(token).create_tag(next_tag, tag_commit))
if ci:
Git("push").exec(print="Subiendo commit al remoto para taggearlo")
success(get_remote_api(token).create_tag(next_tag, tag_commit))
Git("tag", next_tag, tag_commit).exec(print="Creando tag localmente")
Git("tag", next_tag, tag_commit).exec(print="Creando tag localmente")
def get_token_and_ci(self, args: Namespace):
try:
return (self.ensure_repository_token(), False)
except GitFlowError:
return (args.token, True)
def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
parser.add_argument("--token")
def get_token_and_ci(token: Optional[str]):
try:
return (ensure_repository_token(), False)
except GitFlowError:
return (token, True)
return parser
def get_last_merge_commit(self):
commit = Git(
"log", first_parent=True, merges=True, max_count=1, format="%h"
).firstline()
def get_last_merge_commit():
commit = Git(
"log", first_parent=True, merges=True, max_count=1, format="%h"
).firstline()
if not commit:
raise GitFlowError("No hay merges a taggear.")
if not commit:
raise GitFlowError("No hay merges a taggear.")
return commit
return commit
def check_not_tagged(self, commit: str):
tag = Git("describe", commit, tags=True, exact_match=True).firstline(check=False)
if tag:
raise GitFlowError("El commit a taggear ya tiene tag: " + tag)
def check_not_tagged(commit: str):
tag = Git("describe", commit, tags=True, exact_match=True).firstline(check=False)
def get_next_tag_from_commits(self, commits: list[str], branch: str) -> str | None:
increment = SEMVER_SKIP
for commit in commits:
commit_type = commit[: commit.index(":")]
increment = max(
increment, COMMIT_TYPE_INCREMENT.get(commit_type, SEMVER_SKIP)
)
if tag:
raise GitFlowError("El commit a taggear ya tiene tag: " + tag)
tag = Git.get_current_tag() or f"v0.0.0-{branch}"
until_dash = tag.index("-") if "-" in tag else None
suffix = tag[until_dash:] if until_dash is not None else ""
[major, minor, patch] = map(int, tag[1:until_dash].split("."))
def get_next_tag_from_commits(commits: list[str], branch: str) -> str | None:
increment = SEMVER_SKIP
for commit in commits:
commit_type = commit[: commit.index(":")]
increment = max(increment, COMMIT_TYPE_INCREMENT.get(commit_type, SEMVER_SKIP))
if increment == SEMVER_MAJOR:
major += 1
minor = 0
patch = 0
elif increment == SEMVER_MINOR:
minor += 1
patch = 0
elif increment == SEMVER_PATCH:
patch += 1
tag = Git.get_current_tag() or f"v0.0.0-{branch}"
return None if increment == SEMVER_SKIP else f"v{major}.{minor}.{patch}{suffix}"
until_dash = tag.index("-") if "-" in tag else None
suffix = tag[until_dash:] if until_dash is not None else ""
[major, minor, patch] = map(int, tag[1:until_dash].split("."))
if increment == SEMVER_MAJOR:
major += 1
minor = 0
patch = 0
elif increment == SEMVER_MINOR:
minor += 1
patch = 0
elif increment == SEMVER_PATCH:
patch += 1
return None if increment == SEMVER_SKIP else f"v{major}.{minor}.{patch}{suffix}"
+18 -70
View File
@@ -1,84 +1,32 @@
#!/usr/bin/env python3
import locale
from argparse import Namespace
from typing import Optional
import typer
from git_flow import GitFlowError
from git_flow.command.base import Command
from git_flow.command.branch import BranchCommand
from git_flow.command.commit import CommitCommand
from git_flow.command.init import InitCommand
from git_flow.command.merge import MergeCommand
from git_flow.command.new import NewCommand
from git_flow.command.release import ReleaseCommand
from git_flow.command.tag import TagCommand
from git_flow.io import *
from git_flow.command.base import *
from git_flow.command import init, new, commit, merge, tag, release, branch
REPOSITORY_TOKEN_PATH = ".repository-token"
LOCALE = "es_AR.UTF-8"
app = typer.Typer()
@app.command()
def init():
run(InitCommand())
@app.command()
def new():
run(NewCommand())
@app.command()
def commit():
run(CommitCommand())
@app.command()
def merge():
run(MergeCommand())
@app.command()
def tag(token: Optional[str] = None):
run(TagCommand(), Namespace(token = token))
@app.command()
def release(group: Optional[str] = None):
run(ReleaseCommand(), Namespace(group = group))
@app.command()
def branch(env: Optional[str] = None, trash: bool = False, wip: bool = False, all: bool = False):
args = Namespace(
environment = env,
trash = trash,
wip = wip,
all = all,
)
run(BranchCommand(), args)
def run(command: Command, args: Namespace = Namespace()):
command.init()
try:
command.run(args)
except GitFlowError as e:
command.error(str(e))
except Exception as e:
command.error("Ocurrió un error inesperado: " + str(e))
except KeyboardInterrupt as e:
print()
command.error("Ejecución abortada")
app.add_typer(init.app)
app.add_typer(new.app)
app.add_typer(commit.app)
app.add_typer(merge.app)
app.add_typer(tag.app)
app.add_typer(release.app)
app.add_typer(branch.app)
def main():
app()
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")