Compare commits

...
43 Commits
Author SHA1 Message Date
gitea-actions 94a219ed82 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-31 15:25:28 +00:00
jt 3f3650360a Merge pull request 'Feature/parse git remote url with library' (#12) from feature/parse-git-remote-url-with-library into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 55s
Reviewed-on: #12
2026-07-31 15:24:35 +00:00
Jonathan Teran 11a48a922c style: format with ruff 2026-07-31 12:22:39 -03:00
Jonathan Teran f4adb431db feature: parse git remote url with giturlparse 2026-07-31 12:22:14 -03:00
gitea-actions 8f1a60cc06 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-29 14:56:49 +00:00
jt fcc8114c8e Merge pull request 'bugfix: change checkout version to v7' (#11) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 16s
Reviewed-on: #11
2026-07-29 14:56:38 +00:00
Jonathan Teran Carballo c58c4cea96 bugfix: change checkout version to v7 2026-07-29 11:55:54 -03:00
jt 7fbf499278 Merge pull request 'bugfix: move git fetch to separate line' (#10) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 8s
Reviewed-on: #10
2026-07-29 14:49:33 +00:00
Jonathan Teran Carballo 64fc890cb7 bugfix: move git fetch to separate line 2026-07-29 11:49:25 -03:00
gitea-actions 7199249f75 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-29 14:47:56 +00:00
jt 7efaf367e5 Merge pull request 'bugfix: change actions/checkout to v4 and add step to fetch tags' (#9) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 17s
Reviewed-on: #9
2026-07-29 14:47:38 +00:00
Jonathan Teran Carballo dc79ffa399 bugfix: change actions/checkout to v4 and add step to fetch tags 2026-07-29 11:47:00 -03:00
jt 31513a3e3a Merge pull request 'bugfix: change actions/checkout version' (#8) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 19s
Reviewed-on: #8
2026-07-29 14:45:20 +00:00
Jonathan Teran Carballo c45f065e77 bugfix: change actions/checkout version 2026-07-29 11:45:12 -03:00
jt 7f7dd7b538 Merge pull request 'bugfix: add fetch-tags: true' (#7) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 14s
Reviewed-on: #7
2026-07-29 14:41:06 +00:00
Jonathan Teran Carballo adcb53156a bugfix: add fetch-tags: true 2026-07-29 11:40:54 -03:00
jt 5ca817c796 Merge pull request 'bugfix: remove checkout parameters' (#6) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 14s
Reviewed-on: #6
2026-07-29 14:35:04 +00:00
Jonathan Teran Carballo 2be9b50ac4 bugfix: remove checkout parameters 2026-07-29 11:34:43 -03:00
gitea-actions 347b8b9556 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-29 13:57:37 +00:00
jt 9b547bd6a9 Merge pull request 'feature: change flow settings in local config' (#5) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 29s
Reviewed-on: #5
2026-07-29 13:57:06 +00:00
Jonathan Teran Carballo f05582b50f feature: show commit count and list tags on workflow 2026-07-29 10:53:56 -03:00
gitea-actions a1a9a7acac chore: bump version and update CHANGELOG.md [skip ci] 2026-07-18 03:16:04 +00:00
jt 190b4b8408 Merge pull request 'bugfix: add fetch tags to checkout step' (#4) from bugfix/fetch-tags-on-checkout into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 11s
Reviewed-on: #4
2026-07-18 03:15:50 +00:00
jt f9fb5a62d1 bugfix: add fetch tags to checkout step 2026-07-18 00:15:19 -03:00
gitea-actions 09d0de2860 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-18 01:35:18 +00:00
jt f233adfd1d Merge pull request 'feature: print current tag command output' (#3) from feature/log-current-tag-command into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 10s
Reviewed-on: #3
2026-07-18 01:35:03 +00:00
jt 0cdc01291c feature: print current tag command output 2026-07-17 22:34:39 -03:00
gitea-actions 098a4724ca chore: bump version and update CHANGELOG.md [skip ci] 2026-07-18 01:13:08 +00:00
jt 9167390098 Merge pull request 'feature: update I/O methods to use questionary where applicable' (#2) from feature/refactor-use-questionary-package into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 43s
Reviewed-on: #2
2026-07-18 01:12:22 +00:00
jt a08666cac3 style: organize imports 2026-07-17 22:07:37 -03:00
jt 726df217ec style: format and organize imports 2026-07-17 22:04:56 -03:00
jt 07d4ff960e refactor: use base.io_ methods 2026-07-17 22:01:42 -03:00
jt 919e680e75 refactor: use base.io_ methods 2026-07-16 22:26:48 -03:00
jt add4cd2747 refactor: use base.io_ methods 2026-07-16 22:25:20 -03:00
jt e4833c9ed9 style: run black formatter on merge.py 2026-07-16 22:12:30 -03:00
jt 7315681b23 style: run black formatter on commit.py 2026-07-16 22:11:35 -03:00
jt ba1c9bd68d style: run black formatter on new.py 2026-07-16 22:11:07 -03:00
jt 1a7ae338ff style: run black formatter 2026-07-16 22:09:28 -03:00
jt 0120faac9e refactor: use new base.io_ methods 2026-07-16 22:08:46 -03:00
jt febd18c3b3 refactor: use unsafe_ask in favor of global error catch 2026-07-16 22:02:47 -03:00
jt 069c33bc17 style: run black formatter 2026-07-16 21:48:28 -03:00
jt 1d186e117c refactor: reword current branch check, move if around so it's better placed 2026-07-16 21:47:27 -03:00
jt c0f7642687 feature: change unicode status icons 2026-07-16 21:35:41 -03:00
15 changed files with 323 additions and 144 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
Publish-to-PyPI:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
+73
View File
@@ -1,3 +1,76 @@
## v1.19.0 - 2026-07-31
- **style**: format with ruff [[jteran](mailto:jteran@renatre.org.ar)] (11a48a9)
- **feature**: parse git remote url with giturlparse [[jteran](mailto:jteran@renatre.org.ar)] (f4adb43)
---
## v1.18.0 - 2026-07-29
- **bugfix**: change checkout version to v7 [[jteran](mailto:jteran@renatre.org.ar)] (c58c4ce)
- **bugfix**: move git fetch to separate line [[jteran](mailto:jteran@renatre.org.ar)] (64fc890)
- **bugfix**: change actions/checkout to v4 and add step to fetch tags [[jteran](mailto:jteran@renatre.org.ar)] (dc79ffa)
- **bugfix**: change actions/checkout version [[jteran](mailto:jteran@renatre.org.ar)] (c45f065)
- **bugfix**: add fetch-tags: true [[jteran](mailto:jteran@renatre.org.ar)] (adcb531)
- **bugfix**: remove checkout parameters [[jteran](mailto:jteran@renatre.org.ar)] (2be9b50)
- **feature**: show commit count and list tags on workflow [[jteran](mailto:jteran@renatre.org.ar)] (f05582b)
---
## v0.1.0-main - 2026-07-29
- **bugfix**: change actions/checkout to v4 and add step to fetch tags [[jteran](mailto:jteran@renatre.org.ar)] (dc79ffa)
- **bugfix**: change actions/checkout version [[jteran](mailto:jteran@renatre.org.ar)] (c45f065)
- **bugfix**: add fetch-tags: true [[jteran](mailto:jteran@renatre.org.ar)] (adcb531)
- **bugfix**: remove checkout parameters [[jteran](mailto:jteran@renatre.org.ar)] (2be9b50)
- **feature**: show commit count and list tags on workflow [[jteran](mailto:jteran@renatre.org.ar)] (f05582b)
---
## v0.1.0-main - 2026-07-29
- **feature**: show commit count and list tags on workflow [[jteran](mailto:jteran@renatre.org.ar)] (f05582b)
---
## v0.0.1-main - 2026-07-18
- **bugfix**: add fetch tags to checkout step [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (f9fb5a6)
---
## v0.1.0-main - 2026-07-18
- **feature**: print current tag command output [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (0cdc012)
---
## v0.1.0-main - 2026-07-18
- **style**: organize imports [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (a08666c)
- **style**: format and organize imports [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (726df21)
- **refactor**: use base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (07d4ff9)
- **refactor**: use base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (919e680)
- **refactor**: use base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (add4cd2)
- **style**: run black formatter on merge.py [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e4833c9)
- **style**: run black formatter on commit.py [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (7315681)
- **style**: run black formatter on new.py [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ba1c9bd)
- **style**: run black formatter [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1a7ae33)
- **refactor**: use new base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (0120faa)
- **refactor**: use unsafe_ask in favor of global error catch [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (febd18c)
- **style**: run black formatter [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (069c33b)
- **refactor**: reword current branch check, move if around so it's better placed [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1d186e1)
- **feature**: change unicode status icons [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (c0f7642)
---
## v1.17.6 - 2026-07-09
- **bugfix**: actualizo versiones de checkout, setup-python, clonar depth completo [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (4666503)
+1 -1
View File
@@ -8,7 +8,7 @@ description = "Git workflow automation to follow best practices when working wit
dynamic = ["version"]
requires-python = ">= 3.14"
dependencies = [
"requests", "typer", "rich", "questionary"
"requests", "typer", "rich", "questionary", "giturlparse"
]
authors = [
{name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"},
+4 -3
View File
@@ -43,9 +43,10 @@ COMMIT_TYPE_INCREMENT = {
"test": SEMVER_SKIP,
}
SUPPORTED_REMOTE_APIS = [
"bitbucket.org",
"github.com",
SUPPORTED_REMOTE_URL_SCHEMAS = [
"ssh",
"http",
"https"
]
REPOSITORY_TOKEN_FILENAME = ".repository-token"
+69 -20
View File
@@ -3,6 +3,7 @@ from git_flow import (
REPOSITORY_TOKEN_FILENAME,
FLOWCONFIG_FILENAME,
FLOWCONFIG_VERSION,
COMMIT_TYPES,
GitFlowError,
)
from git_flow.git import Git
@@ -18,9 +19,11 @@ import rich
import rich.panel
import questionary
flowconfig = Git.get_config(FLOWCONFIG_FILENAME) if isfile(FLOWCONFIG_FILENAME) else {}
environments = flowconfig["flow.branches"].split(",") if "flow.branches" in flowconfig else []
environments = (
flowconfig["flow.branches"].split(",") if "flow.branches" in flowconfig else []
)
def ensure_initialized():
initialized = flowconfig["flow.initialized"] if flowconfig else None
@@ -48,7 +51,7 @@ def ensure_right_branch():
if branch == "HEAD":
raise GitFlowError("No se encuentra parado sobre una rama.")
elif io_confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"):
elif io_confirm(f"Rama actual: {branch}. ¿Es correcto?"):
return branch
else:
raise GitFlowError("Ejecución cancelada.")
@@ -103,12 +106,12 @@ def ensure_clean_worktree(has_remote: bool):
if not_empty:
if not has_remote:
io_warning("Existen cambios en tu entorno de trabajo sin commitear.")
if not io_confirm("¿Desea continuar?", False):
raise GitFlowError("Ejecución abortada")
else:
raise GitFlowError("No se puede continuar con cambios pendientes.")
if not io_confirm("¿Desea continuar?", False):
raise GitFlowError("Ejecución abortada")
def get_remote_api(token: str) -> RemoteAPI:
if "flow.remote" not in flowconfig:
@@ -118,7 +121,8 @@ def get_remote_api(token: str) -> RemoteAPI:
[schema, host, repository] = RemoteAPI.parse(flowconfig["flow.remote"])
if remote_type == "gitea":
return GiteaRemoteAPI(("https://" if schema == "ssh://" else schema) + host, repository, token)
remote_schema = "https://" if schema == "ssh://" else schema
return GiteaRemoteAPI(remote_schema + host, repository, token)
elif remote_type == "bitbucket":
return BitbucketRemoteAPI(repository, token)
elif remote_type == "github":
@@ -133,40 +137,85 @@ def is_valid_ticket(ticket: str) -> bool:
if len(components) != 2:
return False
ticket_project = components[0]
ticket_number = components[1]
return ticket_project.isalpha() and ticket_project.isupper() and ticket_number.isnumeric()
return (
ticket_project.isalpha()
and ticket_project.isupper()
and ticket_number.isnumeric()
)
def get_commit_message(commit_types: list[str] = COMMIT_TYPES) -> str:
commit_type = io_choice("Tipo de commit", commit_types)
commit_message = io_prompt(
"Mensaje de commit",
validator=lambda s: 0 < len(s) and len(s) < 100,
instruction="100 caracteres máximo",
)
return commit_type + ": " + commit_message
def io_error(message: str, title: Optional[str] = None):
_io_status(message, " Error: ", "red", title)
_io_status(message, " Error: ", "red", title)
def io_warning(message: str, title: Optional[str] = None):
_io_status(message, " Warning: ", "yellow", title)
_io_status(message, "⚠ Warning: ", "yellow", title)
def io_info(message: str, title: Optional[str] = None):
_io_status(message, " Info: ", "blue", title)
_io_status(message, " Info: ", "blue", title)
def io_success(message: str, title: Optional[str] = None):
_io_status(message, " Success: ", "green", title)
_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"))
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()
return questionary.confirm(question, default=default, auto_enter=False).unsafe_ask()
def io_choice(prompt: str, options: list[str]) -> str:
return questionary.select(prompt, options, instruction="Usar flechas").ask()
return questionary.select(
prompt,
options,
use_search_filter=True,
use_jk_keys=False,
instruction="Tipear o usar flechas",
).unsafe_ask()
def io_prompt(message: str, default: str = "", persistent: bool = False, strip: bool = False, validator = None) -> str:
def io_prompt(
message: str,
default: str = "",
persistent: bool = False,
strip: bool = False,
validator=None,
instruction: str | None = None,
) -> str:
if persistent and not validator:
validator = lambda s: len(s.strip()) > 0
value = questionary.text(message, default, validate=validator).ask()
value = questionary.text(
message, default, validate=validator, instruction=instruction
).unsafe_ask()
return value.strip() if strip else value
return value.strip() if strip else value
+20 -14
View File
@@ -1,16 +1,20 @@
from typing import Annotated
import typer
from rich.console import Console
import git_flow.command.base as base
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()
console = Console()
@app.command()
@@ -38,28 +42,28 @@ def branch(
] = False,
):
"""Lista ramas del repositorio, agrupandolas por entorno objetivo"""
ensure_initialized()
base.ensure_initialized()
current_branch = Git.get_current_branch()
if current_branch in environments:
if current_branch in base.environments:
current_environment = current_branch
else:
current_environment, _ = get_branch_env_and_type(current_branch)
current_environment, _ = base.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)
show_envs(base.environments, current_branch)
show_all_branches(base.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)
show_envs(base.environments, current_branch)
show_env_branches(current_environment, base.environments, current_branch)
elif environment in base.environments:
show_envs(base.environments, current_branch)
show_env_branches(environment, base.environments, current_branch)
else:
raise GitFlowError(f"'{environment}' no es un entorno válido.")
@@ -78,7 +82,9 @@ def show_branches(branch_prefix: str, current_branch: str):
def get_branches(branch_prefix: str):
return Git("for-each-ref", "refs/heads/" + branch_prefix, format=BRANCH_FORMAT).lines()
return Git(
"for-each-ref", "refs/heads/" + branch_prefix, format=BRANCH_FORMAT
).lines()
def show_all_branches(environments: list[str], current_branch: str):
+7 -8
View File
@@ -1,8 +1,10 @@
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
import typer
import git_flow.command.base as base
from git_flow import TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
from git_flow.git import Git
app = typer.Typer()
@@ -20,11 +22,8 @@ def commit():
else:
raise GitFlowError("Debe agregar algún cambio al indice para continuar.")
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
message = base.get_commit_message()
commit_type = message[: message.index(":")]
if branch.startswith(WIP_BRANCH_PREFIX):
original_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
+27 -21
View File
@@ -1,8 +1,8 @@
import typer
import git_flow.command.base as base
from git_flow import FLOWCONFIG_FILENAME, FLOWCONFIG_VERSION, GitFlowError
from git_flow.git import Git
from git_flow.command.base import *
app = typer.Typer()
@@ -15,7 +15,7 @@ def init():
_ensure_not_already_initialized()
branches = _setup_flow_branches()
(remote, remote_type) = _setup_flow_remote()
remote, remote_type = _setup_flow_remote()
flowconfig = {
"flow.version": str(FLOWCONFIG_VERSION),
@@ -36,32 +36,34 @@ def init():
def _ensure_is_repository():
if not Git.is_repository():
warning("El directorio actual no es un repositorio.")
base.io_warning("El directorio actual no es un repositorio.")
if not confirm("¿Desea inicializarlo?"):
if not base.io_confirm("¿Desea inicializarlo?"):
raise GitFlowError("No se puede continuar sin inicializar el repositorio")
Git("init").exec()
def _ensure_not_already_initialized():
if not flowconfig:
if not base.flowconfig:
return
elif flowconfig.get("flow.initialized") == "true":
elif base.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",
base.io_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".""",
title="Configuración de entornos",
)
branches = list(map(lambda b: b.strip(), prompt("Ramas", "main").split(",")))
branches = list(
map(lambda b: b.strip(), base.io_prompt("Ramas", "main").split(","))
)
if not all(branches):
raise GitFlowError("No puede ingresar una rama vacia")
@@ -70,21 +72,23 @@ coma. Por ejemplo: "dev, test, prod".""",
def _setup_flow_remote():
panel(
"Configuración de remoto",
"Puede configurar un repositorio remoto para generar PRs automáticamente",
base.io_info(
"""Configurar un repositorio remoto le permite generar PRs automáticamente para el
entorno correspondiente. Deberá seleccionar uno de los remotos actuales, o crear
uno nuevo.""",
title="Configuración de remoto",
)
remote = None
remote_type = None
if confirm("¿Configurar repositorio remoto?"):
if base.io_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.")
base.io_info("No tiene ningún repositorio remoto, se creará uno.")
url = prompt(
url = base.io_prompt(
"Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)",
persistent=True,
)
@@ -93,10 +97,12 @@ def _setup_flow_remote():
elif len(remotes) == 1:
remote = remotes[0]
else:
info("Tiene más de un remoto, seleccione el que va a utilizar.")
remote = choice("Remoto", remotes)
base.io_info("Tiene más de un remoto, seleccione el que va a utilizar.")
remote = base.io_choice("Remoto", remotes)
remote_type = choice("Tipo de remoto:", ["bitbucket", "github", "gitea"])
remote_type = base.io_choice(
"Tipo de remoto:", ["bitbucket", "github", "gitea"]
)
return (remote, remote_type)
@@ -105,10 +111,10 @@ def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None):
existing_branches = Git.get_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.")
base.io_warning("Algunas de las ramas de entornos expecificadas no existen.")
base.io_info("Debe indicar sobre que rama se crearán las ramas de entorno.")
target_branch = choice("Rama", existing_branches)
target_branch = base.io_choice("Rama", existing_branches)
for branch in branches:
if branch not in existing_branches:
+23 -12
View File
@@ -1,7 +1,8 @@
import typer
import git_flow.command.base as base
from git_flow import GitFlowError
from git_flow.changelog import Changelog
import git_flow.command.base as base
from git_flow.git import Git
app = typer.Typer()
@@ -87,8 +88,12 @@ def check_merge_conflicts(target: str):
Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False)
if merge_conflicts:
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")
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,\n"
"y ejecutar nuevamente este comando",
title="Conflictos de merge",
)
raise GitFlowError("Ejecución abortada.")
else:
base.io_success("No se detectaron merge conflicts.")
@@ -109,7 +114,7 @@ def create_pull_request(token: str, branch: str, target: str):
target,
title,
changelog.generate_content(target, branch),
branch not in base.environments
branch not in base.environments,
)
base.io_success(message, "Rama creada")
@@ -118,12 +123,13 @@ def create_pull_request(token: str, branch: str, target: str):
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 base.environments:
target = base.environments[base.environments.index(branch) + 1]
return f"Sincronización de entorno {branch} a {target}"
components = branch.split("/") # <type>/<desc> or release/<env>/<type>/<desc>
components = branch.split("/") # <type>/<desc> or release/<env>/<type>/<desc>
branch_type = components[-2]
branch_desc = components[-1]
@@ -136,11 +142,16 @@ def _get_pr_title_from_branch(branch: str) -> str:
maybe_ticket = branch_desc[:second_dash]
if base.is_valid_ticket(maybe_ticket):
branch_ticket = maybe_ticket
branch_desc = branch_desc[second_dash+1:]
branch_desc = branch_desc[second_dash + 1 :]
return " ".join(filter(None, [
f"[Pasaje a {components[1]}]" if len(components) == 4 else None,
branch_type + ":",
branch_ticket,
branch_desc.replace("-", " ")
]))
return " ".join(
filter(
None,
[
f"[Pasaje a {components[1]}]" if len(components) == 4 else None,
branch_type + ":",
branch_ticket,
branch_desc.replace("-", " "),
],
)
)
+22 -10
View File
@@ -1,9 +1,11 @@
import typer
from git_flow import BRANCH_TYPES
import git_flow.command.base as base
from git_flow.git import Git
from typing import Optional
import typer
import git_flow.command.base as base
from git_flow import BRANCH_TYPES
from git_flow.git import Git
app = typer.Typer()
@@ -17,8 +19,11 @@ def new():
if branch not in base.environments:
base.io_warning("La rama actual no corresponde a un entorno configurado.")
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")
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 = base.io_choice("Tipos de cambio", BRANCH_TYPES)
ticket = _get_optional_ticket()
@@ -30,15 +35,20 @@ def new():
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]:
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)
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:
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")
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()
@@ -46,7 +56,9 @@ def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str:
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)
branch = (
branch_type + "/" + (ticket + "-" if ticket else "") + "-".join(keywords)
)
if branch in branches:
base.io_error(
+21 -24
View File
@@ -1,38 +1,40 @@
from git_flow import COMMIT_TYPES, GitFlowError
from git_flow.command.base import *
from git_flow.git import Git
from typing import Optional
import typer
import git_flow.command.base as base
from git_flow import COMMIT_TYPES, GitFlowError
from git_flow.git import Git
app = typer.Typer()
@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()
base.ensure_initialized()
branch = ensure_right_branch()
env, _ = get_branch_env_and_type(branch)
branch = base.ensure_right_branch()
env, _ = base.get_branch_env_and_type(branch)
if env == environments[-1]:
if env == base.environments[-1]:
raise GitFlowError(
"No se puede hacer release de una rama en el ultimo entorno."
)
has_remote = "flow.remote" in flowconfig
has_remote = "flow.remote" in base.flowconfig
ensure_clean_worktree(has_remote)
base.ensure_clean_worktree(has_remote)
next_env = environments[environments.index(env) + 1]
next_env = base.environments[base.environments.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()
fork_point = Git.get_first_fork_point(branch, env)
commits = Git("log", fork_point + "..", format="%s").lines()
if not commits:
raise GitFlowError("No hay cambios a mergear.")
@@ -45,9 +47,9 @@ def release(group: Optional[str] = None):
grouping = group is not None
if not group:
if confirm("¿Desea agrupar este release con otra rama?", False):
if base.io_confirm("¿Desea agrupar este release con otra rama?", False):
grouping = True
group = choice(
group = base.io_choice(
"Grupo release: ", Git.get_branches("release/" + next_env + "/")
)
else:
@@ -58,18 +60,13 @@ def release(group: Optional[str] = None):
Git("pull").exec(print="Sincronizando cambios la rama objetivo")
Git("switch", "-").exec(print="Volviendo a la rama original")
if len(commits) > 1 and confirm(
if len(commits) > 1 and base.io_confirm(
f"¿Desea reemplazar los {len(commits)} commits de la rama por uno solo?", False
):
info("Debe ingresar el mensaje del commit a crear.")
base.io_info("Debe ingresar el mensaje del commit a crear.")
message = base.get_commit_message(COMMIT_TYPES[:-1])
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
Git("switch", next_branch, base, create=True).exec(
Git("switch", next_branch, fork_point, create=True).exec(
print="Creando rama release en base"
)
Git("merge", branch, squash=True).exec(print="Squasheando commits en uno solo")
@@ -77,7 +74,7 @@ def release(group: Optional[str] = None):
else:
Git("switch", next_branch, create=True).exec(print="Creando rama release")
status = Git("rebase", base, next_branch, onto=group).code(
status = Git("rebase", fork_point, next_branch, onto=group).code(
print="Moviendo cambios hacia el siguiente ambiente"
)
+13 -11
View File
@@ -1,3 +1,8 @@
from typing import Optional
import typer
import git_flow.command.base as base
from git_flow import (
COMMIT_TYPE_INCREMENT,
SEMVER_MAJOR,
@@ -7,10 +12,7 @@ from git_flow import (
GitFlowError,
)
from git_flow.changelog import Changelog
from git_flow.command.base import *
from git_flow.git import Git
from typing import Optional
import typer
app = typer.Typer()
@@ -18,22 +20,22 @@ app = typer.Typer()
@app.command()
def tag(token: Optional[str] = None):
"""Crea un nuevo tag para el último merge."""
ensure_initialized()
base.ensure_initialized()
target = Git.get_current_branch()
if target not in environments:
if target not in base.environments:
raise GitFlowError("Solo se pueden taggear commits en ramas principales.")
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()
fork_point = Git.get_first_fork_point(branch, target)
commits = Git("log", fork_point + ".." + branch, format="%s").lines()
next_tag = get_next_tag_from_commits(commits, target)
if not next_tag:
info(
base.io_info(
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
)
return
@@ -45,21 +47,21 @@ def tag(token: Optional[str] = None):
changelog = Changelog()
changelog.update(next_tag, base, branch)
changelog.update(next_tag, fork_point, 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")
success(get_remote_api(token).create_tag(next_tag, tag_commit))
base.io_success(base.get_remote_api(token).create_tag(next_tag, tag_commit))
Git("tag", next_tag, tag_commit).exec(print="Creando tag localmente")
def get_token_and_ci(token: Optional[str]):
try:
return (ensure_repository_token(), False)
return (base.ensure_repository_token(), False)
except GitFlowError:
return (token, True)
+2 -2
View File
@@ -53,7 +53,7 @@ class Git:
@staticmethod
def get_current_tag():
return Git("describe", abbrev="0", tags=True).firstline(check=False)
return Git("describe", abbrev="0", tags=True).firstline(check=False, print="Obteniendo tag actual")
@staticmethod
def get_first_fork_point(branch: str, env: str):
@@ -117,7 +117,7 @@ class Git:
return lines if not strip else list(map(lambda l: l.strip(), lines))
def firstline(self, **kwargs: bool):
def firstline(self, **kwargs):
lines = self.lines(**kwargs)
return lines[0] if lines else ""
+29 -17
View File
@@ -1,6 +1,8 @@
from abc import ABC, abstractmethod
from git_flow import SUPPORTED_REMOTE_APIS, GitFlowError
import giturlparse
from git_flow import SUPPORTED_REMOTE_URL_SCHEMAS, GitFlowError
from git_flow.git import Git
@@ -11,25 +13,35 @@ class RemoteAPI(ABC):
@staticmethod
def parse(remote: str):
"""Soportamos 3 tipos de URL: http, https y ssh. Si no se especifica el esquema, asumimos ssh:
Ejemplos:
- http://gitea/user/repo
- https://gitea.com/user/repo.git
- git@bitbucket.org:user/repo
- git.mygiteainstance.com:user/repo.git
- ssh://git@git.mygiteainstance.com:1234/user/repo.git
"""
url = Git("remote", "get-url", remote).firstline()
if url.startswith("git@"):
at_pos = url.index("@")
colon_pos = url.index(":")
schema = "ssh://"
host = url[at_pos+1:colon_pos]
repository = url[colon_pos+1:]
elif url.startswith("http://") or url.startswith("https://"):
start_pos = url.index("://") + 3
uri_start = url.index("/", start_pos)
schema = url[:start_pos]
host = url[start_pos:uri_start]
repository = url[uri_start+1:]
else:
parsed_url = giturlparse.parse(url) # Validamos que la URL sea válida
if not parsed_url.valid:
raise GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
repository = repository.removesuffix(".git")
return [schema, host, repository]
if not parsed_url.protocol in SUPPORTED_REMOTE_URL_SCHEMAS:
raise GitFlowError(
f"La URL del remoto {remote} no tiene un esquema soportado: {url}"
)
# Cuando el host es seteado por pipeline puede ser "http://gitea/OWNER/REPO", en ese caso
# la libreria parsea OWNER="" y REPO="OWNER/REPO"
repository = (
"/".join(filter(None, [parsed_url.owner, parsed_url.repo]))
.removeprefix("/")
.removesuffix(".git")
)
return [parsed_url.protocol + "://", parsed_url.host, repository]
@abstractmethod
def create_pull_request(
@@ -38,7 +50,7 @@ class RemoteAPI(ABC):
destination: str,
title: str | None = None,
description: str | None = None,
close_source_branch: bool = True
close_source_branch: bool = True,
) -> str:
pass
Generated
+11
View File
@@ -70,6 +70,7 @@ wheels = [
name = "git-flow-envs"
source = { editable = "." }
dependencies = [
{ name = "giturlparse" },
{ name = "questionary" },
{ name = "requests" },
{ name = "rich" },
@@ -78,12 +79,22 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "giturlparse" },
{ name = "questionary" },
{ name = "requests" },
{ name = "rich" },
{ name = "typer" },
]
[[package]]
name = "giturlparse"
version = "0.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/8a/b70b84cc78f9059d627f09560c81a11e8f046570d220a24e169489cad6c9/giturlparse-0.15.0.tar.gz", hash = "sha256:9af3f1fd5c4a0cac94ddb283593635005646393ee0debbe330d1bdff8866bf2c", size = 16138, upload-time = "2026-06-16T07:28:56.021Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/96/147a2771ab655b9353781fb2f95c94eaf1d8576dccc991c1a61d0d355067/giturlparse-0.15.0-py2.py3-none-any.whl", hash = "sha256:76d2e6983b037356ab99b30683e533ac3db96409b68e2163a20fc3aff6446f10", size = 16683, upload-time = "2026-06-16T07:28:55.184Z" },
]
[[package]]
name = "idna"
version = "3.11"