Compare commits

..
61 Commits
Author SHA1 Message Date
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
jt a884c28094 Merge pull request 'refactor: use questionary package' (#1) from refactor/use-questionary-package into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 12s
Reviewed-on: #1
2026-07-14 03:32:09 +00:00
jt 0782cfb041 refactor: migrar comando merge para que utilice questionary 2026-07-14 00:28:20 -03:00
jt 320d40a211 refactor: migrar comando commit para que utilice questionary 2026-07-14 00:23:36 -03:00
jt a5a753f429 refactor: migrar comando new para que utilice metodos usando questionary 2026-07-14 00:22:22 -03:00
gitea-actions 4968667dad chore: bump version and update CHANGELOG.md [skip ci] 2026-07-09 21:15:32 +00:00
jt 79ae88b012 Merge pull request 'bugfix: actualizo versiones de checkout, setup-python, clonar depth completo' (#10) from bugfix/publish-pypi-workflow into main
Reviewed-on: #10
2026-07-09 21:15:00 +00:00
jt 4666503f32 bugfix: actualizo versiones de checkout, setup-python, clonar depth completo 2026-07-09 18:14:38 -03:00
gitea-actions 4c089eb8da chore: bump version and update CHANGELOG.md [skip ci] 2026-07-09 21:05:19 +00:00
jt dc858d026f Merge pull request 'feature: add publish pypi package workflow' (#9) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #9
2026-07-09 21:05:03 +00:00
jt 5989699f08 bugfix: usar https por defecto como esquema si se detecta ssh 2026-07-09 18:04:40 -03:00
jt ecfe058f8d feature: usar esquema de remoto para host de api gitea 2026-07-09 18:00:39 -03:00
gitea-actions ffe813bf06 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-09 01:21:44 +00:00
jt 6cd6771241 Merge pull request 'bugfix: agregar setup previo a ejecucion de git tag' (#8) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #8
2026-07-09 01:21:35 +00:00
jt 1cc2de0b75 bugfix: agregar setup previo a ejecucion de git tag 2026-07-08 22:21:17 -03:00
jt 0e669019f4 Merge pull request 'bugfix: arreglo acceso a secretos' (#7) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #7
2026-07-09 01:11:06 +00:00
jt d456565b2f bugfix: arreglo acceso a secretos 2026-07-08 22:10:35 -03:00
jt 74a78d30a1 Merge pull request 'bugfix: agrego fetch-depth a checkout' (#6) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #6
2026-07-09 01:08:20 +00:00
jt ad8984cb28 bugfix: agrego fetch-depth a checkout 2026-07-08 22:08:02 -03:00
jt 53b02f1f8f Merge pull request 'bugfix: agrego log de git luego del checkout' (#5) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #5
2026-07-09 01:06:41 +00:00
jt bf6a0e8443 bugfix: agrego log de git luego del checkout 2026-07-08 22:06:24 -03:00
jt 12351e64ba Merge pull request 'bugfix: cambio version de action setup-uv' (#4) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #4
2026-07-09 00:59:09 +00:00
jt cb742425c1 bugfix: cambio version de action setup-uv 2026-07-08 21:58:52 -03:00
jt bd47b40aff Merge pull request 'feature: agrego workflow para subir paquete a pypi' (#3) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #3
2026-07-09 00:55:53 +00:00
jt e94ee2ccc7 feature: agrego workflow para subir paquete a pypi 2026-07-08 21:55:16 -03:00
jt 069c62b697 Merge pull request 'feature: agrego implementacion de github remote api' (#2) from feature/add-github-remote-api into main
Reviewed-on: #2
2026-07-09 00:28:52 +00:00
jt c18ca454c0 feature: agrego implementacion de github remote api 2026-07-08 21:27:42 -03:00
jt 0fcf53bcb5 Merge pull request 'feature: add gitea remote api' (#1) from feature/add-gitea-remote-api into main
Reviewed-on: #1
2026-07-09 00:22:34 +00:00
jt f9dddbc4d6 bugfix: agregar schema a host de gitea api 2026-07-08 21:20:43 -03:00
jt c7888c74f1 bugfix: corregir headers de gitea 2026-07-08 21:19:24 -03:00
jt 87c5ba81c4 feature: agrego soporte para apis gitea 2026-07-08 21:12:06 -03:00
jt a10e9fe4fe Add .gitea/workflows/demo.yaml 2026-07-08 02:56:08 +00:00
bitbucket-pipelines c93833769b chore: bump version and update CHANGELOG.md [skip ci] 2026-06-03 00:40:26 +00:00
JonathanGitFlowandjt c536deb54d Merged in bugfix/allow-commit-messages-non-conventional (pull request #77)
bugfix: allow commits not compliant with conventional commits in changelog generation

Approved-by: Jonathan Teran
2026-06-03 00:39:43 +00:00
jt 3ff9f76a6c bugfix: allow commits not compliant with conventional commits in changelog generation 2026-06-02 21:38:18 -03:00
21 changed files with 574 additions and 231 deletions
+2
View File
@@ -2,3 +2,5 @@
remote = origin remote = origin
branches = main branches = main
initialized = true initialized = true
version = 2
remote-type = gitea
+41
View File
@@ -0,0 +1,41 @@
name: Publish to PyPI
on:
push:
branches:
- main
jobs:
Publish-to-PyPI:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v7
- name: Configure git
run: |
git config --global user.name "gitea-actions"
git config --global user.email "actions@git.jonathanteran.dev"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@v8.3.2
- name: Count commits
run: |
echo "Commit count: $(git rev-list --count main)"
echo "Tags available:"
git tag
- name: Bump version
run: |
uv run git-flow tag --token="${{ secrets.GITEA_TOKEN }}"
- name: Build and publish
run: |
uv build
uv publish --token="${{ secrets.PYPI_TOKEN }}"
+81
View File
@@ -1,3 +1,84 @@
## 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)
---
## v0.1.0-main - 2026-07-09
- **bugfix**: usar https por defecto como esquema si se detecta ssh [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (5989699)
- **feature**: usar esquema de remoto para host de api gitea [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ecfe058)
- **bugfix**: agregar setup previo a ejecucion de git tag [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1cc2de0)
- **bugfix**: arreglo acceso a secretos [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d456565)
- **bugfix**: agrego fetch-depth a checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ad8984c)
- **bugfix**: agrego log de git luego del checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (bf6a0e8)
- **bugfix**: cambio version de action setup-uv [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (cb74242)
- **feature**: agrego workflow para subir paquete a pypi [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e94ee2c)
---
## v0.1.0-main - 2026-07-09
- **bugfix**: agregar setup previo a ejecucion de git tag [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1cc2de0)
- **bugfix**: arreglo acceso a secretos [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d456565)
- **bugfix**: agrego fetch-depth a checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ad8984c)
- **bugfix**: agrego log de git luego del checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (bf6a0e8)
- **bugfix**: cambio version de action setup-uv [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (cb74242)
- **feature**: agrego workflow para subir paquete a pypi [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e94ee2c)
---
## v1.17.5 - 2026-06-03
- **bugfix**: allow commits not compliant with conventional commits in changelog generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (3ff9f76)
---
## v1.17.4 - 2026-05-24 ## v1.17.4 - 2026-05-24
- **bugfix**: use rest of branch name instead of ticket for pr title generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (094bc1a) - **bugfix**: use rest of branch name instead of ticket for pr title generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (094bc1a)
+2 -1
View File
@@ -47,7 +47,8 @@ El comando solicitará 2 opciones:
automáticamente. Por el momento, se soportan los siguientes remotos: automáticamente. Por el momento, se soportan los siguientes remotos:
- [x] Bitbucket - [x] Bitbucket
- [ ] Github: planeado - [x] Github
- [x] Gitea
La configuración del remoto requiere tener un Access Token para poder crear los PRs automáticamente. La configuración del remoto requiere tener un Access Token para poder crear los PRs automáticamente.
El mismo se puede crear desde el repositorio al que se quiere dar acceso: *"Repository settings" > El mismo se puede crear desde el repositorio al que se quiere dar acceso: *"Repository settings" >
+1 -1
View File
@@ -8,7 +8,7 @@ description = "Git workflow automation to follow best practices when working wit
dynamic = ["version"] dynamic = ["version"]
requires-python = ">= 3.14" requires-python = ">= 3.14"
dependencies = [ dependencies = [
"requests", "typer" "requests", "typer", "rich", "questionary"
] ]
authors = [ authors = [
{name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"}, {name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"},
+2
View File
@@ -51,3 +51,5 @@ SUPPORTED_REMOTE_APIS = [
REPOSITORY_TOKEN_FILENAME = ".repository-token" REPOSITORY_TOKEN_FILENAME = ".repository-token"
FLOWCONFIG_FILENAME = ".flowconfig" FLOWCONFIG_FILENAME = ".flowconfig"
FLOWCONFIG_VERSION = 2
+8 -4
View File
@@ -28,11 +28,15 @@ class Changelog:
message = Git("show", commit, patch=False, format="%s").firstline() message = Git("show", commit, patch=False, format="%s").firstline()
email = Git("show", commit, patch=False, format="%ae").firstline() email = Git("show", commit, patch=False, format="%ae").firstline()
username = email[: email.index("@")] username = email[: email.index("@")]
index = message.index(":") description = message
commit_type = message[:index]
commit_message = message[index + 1 :]
content += f"- **{commit_type}**: {commit_message} [[{username}](mailto:{email})] ({commit})\n" if ":" in message:
index = message.index(":")
commit_type = message[:index]
commit_message = message[index + 1 :]
description = f"**{commit_type}**: {commit_message}"
content += f"- {description} [[{username}](mailto:{email})] ({commit})\n"
return content return content
+111 -16
View File
@@ -2,23 +2,28 @@ from git_flow import (
BRANCH_TYPES, BRANCH_TYPES,
REPOSITORY_TOKEN_FILENAME, REPOSITORY_TOKEN_FILENAME,
FLOWCONFIG_FILENAME, FLOWCONFIG_FILENAME,
FLOWCONFIG_VERSION,
COMMIT_TYPES,
GitFlowError, GitFlowError,
) )
from git_flow.git import Git from git_flow.git import Git
from git_flow.remote.base import RemoteAPI from git_flow.remote.base import RemoteAPI
from git_flow.remote.bitbucket import BitbucketRemoteAPI from git_flow.remote.bitbucket import BitbucketRemoteAPI
from git_flow.remote.github import GithubRemoteAPI from git_flow.remote.github import GithubRemoteAPI
from git_flow.remote.gitea import GiteaRemoteAPI
from git_flow.io import * from git_flow.io import *
from os.path import isfile from os.path import isfile
TYPE_SUCCESS = 0 from typing import Optional
TYPE_WARNING = 1 import rich
TYPE_ERROR = 2 import rich.panel
TYPE_INFO = 3 import questionary
flowconfig = Git.get_config(FLOWCONFIG_FILENAME) if isfile(FLOWCONFIG_FILENAME) else {} 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(): def ensure_initialized():
initialized = flowconfig["flow.initialized"] if flowconfig else None initialized = flowconfig["flow.initialized"] if flowconfig else None
@@ -32,13 +37,21 @@ def ensure_initialized():
f"El valor de `flow.initialized` ({initialized}) es inválido." f"El valor de `flow.initialized` ({initialized}) es inválido."
) )
version = int(flowconfig.get("flow.version", "1"))
if version != FLOWCONFIG_VERSION:
raise GitFlowError(
f"La versión del flowconfig ({version}) no es compatible con esta versión de git-flow ({FLOWCONFIG_VERSION}). "
f"Debe reinicializar el repositorio."
)
def ensure_right_branch(): def ensure_right_branch():
branch = Git.get_current_branch() branch = Git.get_current_branch()
if branch == "HEAD": if branch == "HEAD":
raise GitFlowError("No se encuentra parado sobre una rama.") raise GitFlowError("No se encuentra parado sobre una rama.")
elif confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"): elif io_confirm(f"Rama actual: {branch}. ¿Es correcto?"):
return branch return branch
else: else:
raise GitFlowError("Ejecución cancelada.") raise GitFlowError("Ejecución cancelada.")
@@ -92,26 +105,31 @@ def ensure_clean_worktree(has_remote: bool):
if not_empty: if not_empty:
if not has_remote: if not has_remote:
warning("Existen cambios en tu entorno de trabajo sin commitear.") io_warning("Existen cambios en tu entorno de trabajo sin commitear.")
if not io_confirm("¿Desea continuar?", False):
raise GitFlowError("Ejecución abortada")
else: else:
raise GitFlowError("No se puede continuar con cambios pendientes.") raise GitFlowError("No se puede continuar con cambios pendientes.")
if not confirm("¿Desea continuar?", False):
raise GitFlowError("Ejecución abortada")
def get_remote_api(token: str) -> RemoteAPI: def get_remote_api(token: str) -> RemoteAPI:
if "flow.remote" not in flowconfig: if "flow.remote" not in flowconfig:
raise GitFlowError("El repositorio no tiene configurado un remoto.") raise GitFlowError("El repositorio no tiene configurado un remoto.")
[host, repository] = RemoteAPI.parse(flowconfig["flow.remote"]) remote_type = flowconfig.get("flow.remote-type", "").lower()
[schema, host, repository] = RemoteAPI.parse(flowconfig["flow.remote"])
if host == "bitbucket.org": if remote_type == "gitea":
remote_schema = "https://" if schema == "ssh://" else schema
return GiteaRemoteAPI(remote_schema + host, repository, token)
elif remote_type == "bitbucket":
return BitbucketRemoteAPI(repository, token) return BitbucketRemoteAPI(repository, token)
elif host == "github.com": elif remote_type == "github":
return GithubRemoteAPI(repository, token) return GithubRemoteAPI(repository, token)
else: else:
raise GitFlowError("El host del repositorio remoto es inválido") raise GitFlowError("El tipo del remoto es inválido")
def is_valid_ticket(ticket: str) -> bool: def is_valid_ticket(ticket: str) -> bool:
"""Check if the specified string is a valid ticket number (ABC-123)""" """Check if the specified string is a valid ticket number (ABC-123)"""
@@ -123,4 +141,81 @@ def is_valid_ticket(ticket: str) -> bool:
ticket_project = components[0] ticket_project = components[0]
ticket_number = components[1] 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)
def io_warning(message: str, title: Optional[str] = None):
_io_status(message, "⚠ Warning: ", "yellow", title)
def io_info(message: str, title: Optional[str] = None):
_io_status(message, " Info: ", "blue", title)
def io_success(message: str, title: Optional[str] = None):
_io_status(message, "✔ Success: ", "green", title)
def _io_status(message: str, prefix: str, style: str, title: Optional[str] = None):
if title:
rich.print(
rich.panel.Panel(
message,
title=prefix + title,
style=style,
expand=False,
title_align="left",
)
)
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).unsafe_ask()
def io_choice(prompt: str, options: list[str]) -> str:
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,
instruction: str | None = None,
) -> str:
if persistent and not validator:
validator = lambda s: len(s.strip()) > 0
value = questionary.text(
message, default, validate=validator, instruction=instruction
).unsafe_ask()
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 ( from git_flow import (
TRASH_BRANCH_PREFIX, TRASH_BRANCH_PREFIX,
WIP_BRANCH_PREFIX, WIP_BRANCH_PREFIX,
GitFlowError, GitFlowError,
) )
from git_flow.command.base import *
from git_flow.git import Git from git_flow.git import Git
from typing import Annotated
import typer
BRANCH_FORMAT = "%(refname:short)" BRANCH_FORMAT = "%(refname:short)"
app = typer.Typer() app = typer.Typer()
console = Console()
@app.command() @app.command()
@@ -38,28 +42,28 @@ def branch(
] = False, ] = False,
): ):
"""Lista ramas del repositorio, agrupandolas por entorno objetivo""" """Lista ramas del repositorio, agrupandolas por entorno objetivo"""
ensure_initialized() base.ensure_initialized()
current_branch = Git.get_current_branch() current_branch = Git.get_current_branch()
if current_branch in environments: if current_branch in base.environments:
current_environment = current_branch current_environment = current_branch
else: else:
current_environment, _ = get_branch_env_and_type(current_branch) current_environment, _ = base.get_branch_env_and_type(current_branch)
if trash: if trash:
show_branches(TRASH_BRANCH_PREFIX, current_branch) show_branches(TRASH_BRANCH_PREFIX, current_branch)
elif wip: elif wip:
show_branches(WIP_BRANCH_PREFIX, current_branch) show_branches(WIP_BRANCH_PREFIX, current_branch)
elif all: elif all:
show_envs(environments, current_branch) show_envs(base.environments, current_branch)
show_all_branches(environments, current_branch) show_all_branches(base.environments, current_branch)
elif environment is None: elif environment is None:
show_envs(environments, current_branch) show_envs(base.environments, current_branch)
show_env_branches(current_environment, environments, current_branch) show_env_branches(current_environment, base.environments, current_branch)
elif environment in environments: elif environment in base.environments:
show_envs(environments, current_branch) show_envs(base.environments, current_branch)
show_env_branches(environment, environments, current_branch) show_env_branches(environment, base.environments, current_branch)
else: else:
raise GitFlowError(f"'{environment}' no es un entorno válido.") 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): 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): def show_all_branches(environments: list[str], current_branch: str):
+11 -12
View File
@@ -1,8 +1,10 @@
import typer
from datetime import datetime from datetime import datetime
from git_flow import COMMIT_TYPES, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
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 from git_flow.git import Git
from git_flow.command.base import *
app = typer.Typer() app = typer.Typer()
@@ -10,21 +12,18 @@ app = typer.Typer()
@app.command() @app.command()
def commit(): def commit():
"""Crea un commit siguiendo el formato de Conventional Commits""" """Crea un commit siguiendo el formato de Conventional Commits"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
if not _has_files_staged(): if not _has_files_staged():
warning("No hay cambios en el indice para commitear.") base.io_warning("No hay cambios en el indice para commitear.")
if confirm("¿Desea agregar la carpeta actual?"): if base.io_confirm("¿Desea agregar la carpeta actual?"):
Git("add", ".").exec(print="Agregando cambios") Git("add", ".").exec(print="Agregando cambios")
else: else:
raise GitFlowError("Debe agregar algún cambio al indice para continuar.") raise GitFlowError("Debe agregar algún cambio al indice para continuar.")
commit_type = choice("Tipo de commit", COMMIT_TYPES) message = base.get_commit_message()
commit_message = prompt( commit_type = message[: message.index(":")]
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
)
message = commit_type + ": " + commit_message
if branch.startswith(WIP_BRANCH_PREFIX): if branch.startswith(WIP_BRANCH_PREFIX):
original_branch = branch.removeprefix(WIP_BRANCH_PREFIX) original_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
+34 -23
View File
@@ -1,8 +1,8 @@
import typer import typer
from git_flow import FLOWCONFIG_FILENAME, GitFlowError 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.git import Git
from git_flow.command.base import *
app = typer.Typer() app = typer.Typer()
@@ -15,15 +15,17 @@ def init():
_ensure_not_already_initialized() _ensure_not_already_initialized()
branches = _setup_flow_branches() branches = _setup_flow_branches()
remote = _setup_flow_remote() remote, remote_type = _setup_flow_remote()
flowconfig = { flowconfig = {
"flow.version": str(FLOWCONFIG_VERSION),
"flow.initialized": "true", "flow.initialized": "true",
"flow.branches": ",".join(branches), "flow.branches": ",".join(branches),
} }
if remote: if remote and remote_type:
flowconfig["flow.remote"] = remote flowconfig["flow.remote"] = remote
flowconfig["flow.remote-type"] = remote_type
Git.set_config(flowconfig, FLOWCONFIG_FILENAME) Git.set_config(flowconfig, FLOWCONFIG_FILENAME)
Git("add", FLOWCONFIG_FILENAME).exec() Git("add", FLOWCONFIG_FILENAME).exec()
@@ -34,32 +36,34 @@ def init():
def _ensure_is_repository(): def _ensure_is_repository():
if not Git.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") raise GitFlowError("No se puede continuar sin inicializar el repositorio")
Git("init").exec() Git("init").exec()
def _ensure_not_already_initialized(): def _ensure_not_already_initialized():
if not flowconfig: if not base.flowconfig:
return 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") raise GitFlowError("El repositorio ya fue inicializado para usar git-flow")
else: else:
raise GitFlowError("Valor de 'flow.initialized' es inválido") raise GitFlowError("Valor de 'flow.initialized' es inválido")
def _setup_flow_branches(): def _setup_flow_branches():
panel( base.io_info(
"Configuración de entornos",
"""Ingrese las ramas que representan los entornos de deploy del proyecto """Ingrese las ramas que representan los entornos de deploy del proyecto
en orden creciente de cercanía al entorno productivo, y separados por en orden creciente de cercanía al entorno productivo, y separados por
coma. Por ejemplo: "dev, test, prod".""", 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): if not all(branches):
raise GitFlowError("No puede ingresar una rama vacia") raise GitFlowError("No puede ingresar una rama vacia")
@@ -68,20 +72,23 @@ coma. Por ejemplo: "dev, test, prod".""",
def _setup_flow_remote(): def _setup_flow_remote():
panel( base.io_info(
"Configuración de remoto", """Configurar un repositorio remoto le permite generar PRs automáticamente para el
"Puede configurar un repositorio remoto para generar PRs automáticamente", entorno correspondiente. Deberá seleccionar uno de los remotos actuales, o crear
uno nuevo.""",
title="Configuración de remoto",
) )
remote = None 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") remotes = Git("remote").lines(print="Listando remotos disponibles")
if not remotes: 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)", "Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)",
persistent=True, persistent=True,
) )
@@ -90,20 +97,24 @@ def _setup_flow_remote():
elif len(remotes) == 1: elif len(remotes) == 1:
remote = remotes[0] remote = remotes[0]
else: else:
info("Tiene más de un remoto, seleccione el que va a utilizar.") base.io_info("Tiene más de un remoto, seleccione el que va a utilizar.")
remote = choice("Remoto", remotes) remote = base.io_choice("Remoto", remotes)
return remote remote_type = base.io_choice(
"Tipo de remoto:", ["bitbucket", "github", "gitea"]
)
return (remote, remote_type)
def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None): def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None):
existing_branches = Git.get_branches() existing_branches = Git.get_branches()
if not all(map(lambda b: b in existing_branches, branches)): if not all(map(lambda b: b in existing_branches, branches)):
warning("Algunas de las ramas de entornos expecificadas no existen.") base.io_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_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: for branch in branches:
if branch not in existing_branches: if branch not in existing_branches:
+49 -40
View File
@@ -1,7 +1,8 @@
import typer import typer
import git_flow.command.base as base
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.changelog import Changelog from git_flow.changelog import Changelog
from git_flow.command.base import *
from git_flow.git import Git from git_flow.git import Git
app = typer.Typer() app = typer.Typer()
@@ -10,36 +11,36 @@ app = typer.Typer()
@app.command() @app.command()
def merge(): def merge():
"""Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado""" """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
if branch in environments: if branch in base.environments:
if branch == environments[-1]: if branch == base.environments[-1]:
raise GitFlowError("No se puede hacer un merge del último entorno.") raise GitFlowError("No se puede hacer un merge del último entorno.")
panel( base.io_info(
"Merge de entornos",
"""Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""", """Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""",
"Merge de entornos",
) )
if not confirm("¿Desea continuar?"): if not base.io_confirm("¿Desea continuar?"):
raise GitFlowError("Ejecución abortada") raise GitFlowError("Ejecución abortada")
target = environments[environments.index(branch) + 1] target = base.environments[base.environments.index(branch) + 1]
else: else:
target, _ = get_branch_env_and_type(branch) target, _ = base.get_branch_env_and_type(branch)
if "flow.remote" in flowconfig: if "flow.remote" in base.flowconfig:
remote = flowconfig["flow.remote"] remote = base.flowconfig["flow.remote"]
token = ensure_repository_token() token = base.ensure_repository_token()
run_remote(remote, token, branch, target) run_remote(remote, token, branch, target)
else: else:
run_local(branch, target) run_local(branch, target)
def run_remote(remote: str, token: str, branch: str, target: str): def run_remote(remote: str, token: str, branch: str, target: str):
ensure_clean_worktree(True) base.ensure_clean_worktree(True)
Git("switch", target).exec(print="Cambiando a rama destino") Git("switch", target).exec(print="Cambiando a rama destino")
Git("pull").exec(print="Obteniendo ultimos cambios del remoto") Git("pull").exec(print="Obteniendo ultimos cambios del remoto")
@@ -48,8 +49,8 @@ def run_remote(remote: str, token: str, branch: str, target: str):
check_merge_conflicts(target) check_merge_conflicts(target)
show_commits_to_merge(target) show_commits_to_merge(target)
if confirm(f"¿Crear PR de '{branch}' a '{target}'?"): if base.io_confirm(f"¿Crear PR de '{branch}' a '{target}'?"):
if branch not in environments: if branch not in base.environments:
Git("push", remote, branch, set_upstream=True).exec( Git("push", remote, branch, set_upstream=True).exec(
print="Subiendo rama al remoto para crear PR" print="Subiendo rama al remoto para crear PR"
) )
@@ -58,11 +59,11 @@ def run_remote(remote: str, token: str, branch: str, target: str):
def run_local(branch: str, target: str): def run_local(branch: str, target: str):
ensure_clean_worktree(False) base.ensure_clean_worktree(False)
check_merge_conflicts(target) check_merge_conflicts(target)
show_commits_to_merge(target) show_commits_to_merge(target)
if confirm(f"¿Mergear rama '{branch}' a '{target}'?"): if base.io_confirm(f"¿Mergear rama '{branch}' a '{target}'?"):
Git("switch", target).exec(print="Cambiando a rama destino") Git("switch", target).exec(print="Cambiando a rama destino")
Git("merge", branch, ff=False).exec(print="Mergeando") Git("merge", branch, ff=False).exec(print="Mergeando")
@@ -87,13 +88,15 @@ def check_merge_conflicts(target: str):
Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False) Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False)
if merge_conflicts: if merge_conflicts:
error(f"La rama actual tiene conflictos con {target}") base.io_error(
info( 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""" 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.") raise GitFlowError("Ejecución abortada.")
else: else:
success("No se detectaron merge conflicts.") base.io_success("No se detectaron merge conflicts.")
def create_pull_request(token: str, branch: str, target: str): def create_pull_request(token: str, branch: str, target: str):
@@ -101,31 +104,32 @@ def create_pull_request(token: str, branch: str, target: str):
commits = Git("log", target + "..", format="%s").lines() commits = Git("log", target + "..", format="%s").lines()
title = commits[0] if len(commits) == 1 else _get_pr_title_from_branch(branch) title = commits[0] if len(commits) == 1 else _get_pr_title_from_branch(branch)
panel("Título del PR", title) base.panel("Título del PR", title)
if confirm("¿Desea cambiar el título del PR?", False): if base.io_confirm("¿Desea cambiar el título del PR?", False):
title = prompt("Título del PR") title = base.io_prompt("Título del PR", persistent=True)
message = get_remote_api(token).create_pull_request( message = base.get_remote_api(token).create_pull_request(
branch, branch,
target, target,
title, title,
changelog.generate_content(target, branch), changelog.generate_content(target, branch),
branch not in environments branch not in base.environments,
) )
success(message) base.io_success(message, "Rama creada")
if confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"): if base.io_confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"):
Git("switch", target).exec(print="Cambiando a rama objetivo") Git("switch", target).exec(print="Cambiando a rama objetivo")
Git("pull").exec(print="Obteniendo cambios") Git("pull").exec(print="Obteniendo cambios")
def _get_pr_title_from_branch(branch: str) -> str: def _get_pr_title_from_branch(branch: str) -> str:
if branch in environments: if branch in base.environments:
target = environments[environments.index(branch) + 1] target = base.environments[base.environments.index(branch) + 1]
return f"Sincronización de entorno {branch} a {target}" 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_type = components[-2]
branch_desc = components[-1] branch_desc = components[-1]
@@ -136,13 +140,18 @@ def _get_pr_title_from_branch(branch: str) -> str:
if first_dash >= 0 and second_dash >= 0: if first_dash >= 0 and second_dash >= 0:
maybe_ticket = branch_desc[:second_dash] maybe_ticket = branch_desc[:second_dash]
if is_valid_ticket(maybe_ticket): if base.is_valid_ticket(maybe_ticket):
branch_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, [ return " ".join(
f"[Pasaje a {components[1]}]" if len(components) == 4 else None, filter(
branch_type + ":", None,
branch_ticket, [
branch_desc.replace("-", " ") f"[Pasaje a {components[1]}]" if len(components) == 4 else None,
])) branch_type + ":",
branch_ticket,
branch_desc.replace("-", " "),
],
)
)
+33 -28
View File
@@ -1,9 +1,11 @@
import typer
from git_flow import BRANCH_TYPES
from git_flow.command.base import *
from git_flow.git import Git
from typing import Optional 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() app = typer.Typer()
@@ -11,52 +13,55 @@ app = typer.Typer()
def new(): def new():
"""Crea una nueva rama siguiendo Conventional Branches""" """Crea una nueva rama siguiendo Conventional Branches"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
if branch not in environments: if branch not in base.environments:
warning("La rama actual no corresponde a un entorno configurado.") base.io_warning("La rama actual no corresponde a un entorno configurado.")
info("Se creará una nueva rama de trabajo sobre la rama actual.") base.io_info(
info("Debe seleccionar el tipo de cambio a realizar.") "Se creará una nueva rama de trabajo sobre la rama actual.\n"
"Debe seleccionar el tipo de cambio a realizar.",
title="Tipo de cambio",
)
branch_type = choice("Tipos de cambio", BRANCH_TYPES) branch_type = base.io_choice("Tipos de cambio", BRANCH_TYPES)
ticket = _get_optional_ticket() ticket = _get_optional_ticket()
new_branch = _get_unique_branch_name(branch_type, ticket) new_branch = _get_unique_branch_name(branch_type, ticket)
if "flow.remote" in flowconfig and Git.get_tracking_branch(branch): if "flow.remote" in base.flowconfig and Git.get_tracking_branch(branch):
Git("pull").exec(print="Actualizando rama actual") Git("pull").exec(print="Actualizando rama actual")
if confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"): if base.io_confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"):
Git("switch", new_branch, create=True).exec(print="Creando nueva rama") Git("switch", new_branch, create=True).exec(print="Creando nueva rama")
def _get_optional_ticket() -> Optional[str]: def _get_optional_ticket() -> Optional[str]:
ticket = prompt("[Opcional] Ticket que respalda el cambio (en formato ABC-123)") 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),
)
if not ticket:
return None
if not is_valid_ticket(ticket):
warning("Formato de ticket inválido, ignorando.")
return None
return ticket
def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str: def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str:
info(f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.") base.io_info(
f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.\n"
"Las mismas se utilizaran para generar el nombre de la rama.",
title="Nombre de rama",
)
branch = None branch = None
branches = Git.get_branches() branches = Git.get_branches()
while branch is None: while branch is None:
keywords = prompt( keywords = base.io_prompt("Palabras clave", persistent=True)
"Palabras clave del cambio (por ejemplo: 'create worker form')",
persistent=True,
)
keywords = filter(lambda k: len(k) > 0, keywords.split(" ")) 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: if branch in branches:
error( base.io_error(
f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra" f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra"
) )
branch = None branch = None
+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 from typing import Optional
import typer 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 = typer.Typer()
@app.command() @app.command()
def release(group: Optional[str] = None): def release(group: Optional[str] = None):
"""Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado""" """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
env, _ = get_branch_env_and_type(branch) env, _ = base.get_branch_env_and_type(branch)
if env == environments[-1]: if env == base.environments[-1]:
raise GitFlowError( raise GitFlowError(
"No se puede hacer release de una rama en el ultimo entorno." "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 = ( next_branch = (
branch.replace(f"/{env}/", f"/{next_env}/") branch.replace(f"/{env}/", f"/{next_env}/")
if branch.startswith("release/") if branch.startswith("release/")
else f"release/{next_env}/{branch}" else f"release/{next_env}/{branch}"
) )
base = Git.get_first_fork_point(branch, env) fork_point = Git.get_first_fork_point(branch, env)
commits = Git("log", base + "..", format="%s").lines() commits = Git("log", fork_point + "..", format="%s").lines()
if not commits: if not commits:
raise GitFlowError("No hay cambios a mergear.") raise GitFlowError("No hay cambios a mergear.")
@@ -45,9 +47,9 @@ def release(group: Optional[str] = None):
grouping = group is not None grouping = group is not None
if not group: 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 grouping = True
group = choice( group = base.io_choice(
"Grupo release: ", Git.get_branches("release/" + next_env + "/") "Grupo release: ", Git.get_branches("release/" + next_env + "/")
) )
else: else:
@@ -58,18 +60,13 @@ def release(group: Optional[str] = None):
Git("pull").exec(print="Sincronizando cambios la rama objetivo") Git("pull").exec(print="Sincronizando cambios la rama objetivo")
Git("switch", "-").exec(print="Volviendo a la rama original") 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 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]) Git("switch", next_branch, fork_point, create=True).exec(
commit_message = 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" print="Creando rama release en base"
) )
Git("merge", branch, squash=True).exec(print="Squasheando commits en uno solo") Git("merge", branch, squash=True).exec(print="Squasheando commits en uno solo")
@@ -77,7 +74,7 @@ def release(group: Optional[str] = None):
else: else:
Git("switch", next_branch, create=True).exec(print="Creando rama release") 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" 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 ( from git_flow import (
COMMIT_TYPE_INCREMENT, COMMIT_TYPE_INCREMENT,
SEMVER_MAJOR, SEMVER_MAJOR,
@@ -7,10 +12,7 @@ from git_flow import (
GitFlowError, GitFlowError,
) )
from git_flow.changelog import Changelog from git_flow.changelog import Changelog
from git_flow.command.base import *
from git_flow.git import Git from git_flow.git import Git
from typing import Optional
import typer
app = typer.Typer() app = typer.Typer()
@@ -18,22 +20,22 @@ app = typer.Typer()
@app.command() @app.command()
def tag(token: Optional[str] = None): def tag(token: Optional[str] = None):
"""Crea un nuevo tag para el último merge.""" """Crea un nuevo tag para el último merge."""
ensure_initialized() base.ensure_initialized()
target = Git.get_current_branch() 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.") raise GitFlowError("Solo se pueden taggear commits en ramas principales.")
branch = Git( branch = Git(
"show", get_last_merge_commit() + "^2", patch=False, format="%h" "show", get_last_merge_commit() + "^2", patch=False, format="%h"
).firstline() ).firstline()
base = Git.get_first_fork_point(branch, target) fork_point = Git.get_first_fork_point(branch, target)
commits = Git("log", base + ".." + branch, format="%s").lines() commits = Git("log", fork_point + ".." + branch, format="%s").lines()
next_tag = get_next_tag_from_commits(commits, target) next_tag = get_next_tag_from_commits(commits, target)
if not next_tag: if not next_tag:
info( base.io_info(
"Los cambios realizados no implican un salto de versión, se mantiene la anterior." "Los cambios realizados no implican un salto de versión, se mantiene la anterior."
) )
return return
@@ -45,21 +47,21 @@ def tag(token: Optional[str] = None):
changelog = Changelog() 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("add", Changelog.FILENAME).exec(print="Agregando nuevo CHANGELOG.md")
Git("commit", message=Changelog.COMMIT_MESSAGE).exec(print="Creando commit") Git("commit", message=Changelog.COMMIT_MESSAGE).exec(print="Creando commit")
tag_commit = Git("show", "HEAD", patch=False, format="%h").firstline() tag_commit = Git("show", "HEAD", patch=False, format="%h").firstline()
if ci: if ci:
Git("push").exec(print="Subiendo commit al remoto para taggearlo") 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") Git("tag", next_tag, tag_commit).exec(print="Creando tag localmente")
def get_token_and_ci(token: Optional[str]): def get_token_and_ci(token: Optional[str]):
try: try:
return (ensure_repository_token(), False) return (base.ensure_repository_token(), False)
except GitFlowError: except GitFlowError:
return (token, True) return (token, True)
+6 -16
View File
@@ -53,7 +53,7 @@ class Git:
@staticmethod @staticmethod
def get_current_tag(): 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 @staticmethod
def get_first_fork_point(branch: str, env: str): 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)) 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) lines = self.lines(**kwargs)
return lines[0] if lines else "" return lines[0] if lines else ""
@@ -140,17 +140,7 @@ class Git:
return process return process
def _run(self, print: str | None = None, check: bool = True): def _run(self, print: str | None = None, check: bool = True):
process = subprocess.run( return self._get(print, check)
self.command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
if print is not None:
self._print_process("", "", title=print)
if check:
process.check_returncode()
return process
def _print_process(self, stdout: str, stderr: str, title: str): def _print_process(self, stdout: str, stderr: str, title: str):
command = [] command = []
@@ -164,12 +154,12 @@ class Git:
command.append(arg) command.append(arg)
text = f"[yellow]$ {" ".join(command)}[/yellow]" text = f"[green]$ {" ".join(command)}[/green]"
for line in stdout.splitlines(): for line in stdout.splitlines():
text += "\n\\[out] " + line text += "\n" + line
for line in stderr.splitlines(): for line in stderr.splitlines():
text += "\n[red]\\[err][/red] " + line text += "\n[red]" + line + "[/red]"
panel(title or "Ejecutando", text) panel(title or "Ejecutando", text)
+7 -7
View File
@@ -3,7 +3,7 @@
import typer import typer
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.command.base import * import git_flow.command.base as base
from git_flow.command import init, new, commit, merge, tag, release, branch from git_flow.command import init, new, commit, merge, tag, release, branch
REPOSITORY_TOKEN_PATH = ".repository-token" REPOSITORY_TOKEN_PATH = ".repository-token"
@@ -23,10 +23,10 @@ app.add_typer(branch.app)
def main(): def main():
try: try:
app() app()
except GitFlowError as e:
error(str(e))
except Exception as e:
error("Ocurrió un error inesperado: " + str(e))
except KeyboardInterrupt as e:
print() print()
error("Ejecución abortada") except GitFlowError as e:
base.io_error(str(e), title="Git Flow")
except Exception as e:
base.io_error(str(e), title="Inesperado")
except KeyboardInterrupt as e:
base.io_error("Ejecución abortada")
+10 -28
View File
@@ -12,42 +12,24 @@ class RemoteAPI(ABC):
@staticmethod @staticmethod
def parse(remote: str): def parse(remote: str):
url = Git("remote", "get-url", remote).firstline() url = Git("remote", "get-url", remote).firstline()
invalid_url_error = GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
url_type = None
if url.startswith("git@"): if url.startswith("git@"):
url_type = "ssh"
elif url.startswith("http://") or url.startswith("https://"):
url_type = "https"
else:
raise invalid_url_error
if url_type == "ssh":
# git@host:user/repo[.git]
at_pos = url.index("@") at_pos = url.index("@")
colon_pos = url.index(":") colon_pos = url.index(":")
schema = "ssh://"
host = url[at_pos+1:colon_pos] host = url[at_pos+1:colon_pos]
repository = url[colon_pos+1:] repository = url[colon_pos+1:]
else: elif url.startswith("http://") or url.startswith("https://"):
# https://host/user/repo[.git] start_pos = url.index("://") + 3
host_start = url.index("://") + 3 uri_start = url.index("/", start_pos)
uri_start = url.index("/", host_start) schema = url[:start_pos]
host = url[host_start:uri_start] host = url[start_pos:uri_start]
repository = url[uri_start+1:] repository = url[uri_start+1:]
else:
raise GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
# Esto permite configurar hosts "imaginarios" para permitir el uso de multiples claves ssh repository = repository.removesuffix(".git")
if host.endswith(".bitbucket.org"): return [schema, host, repository]
host = "bitbucket.org"
elif host.endswith(".github.com"):
host = "github.com"
if host not in SUPPORTED_REMOTE_APIS:
raise GitFlowError("El host del repositorio remoto es inválido")
repository.removesuffix(".git")
return [host, repository]
@abstractmethod @abstractmethod
def create_pull_request( def create_pull_request(
+55
View File
@@ -0,0 +1,55 @@
import requests
from git_flow import GitFlowError
from git_flow.remote.base import RemoteAPI
class GiteaRemoteAPI(RemoteAPI):
def __init__(self, base_url: str, repository: str, token: str) -> None:
super().__init__(repository, token)
self.base_url = base_url
def create_pull_request(
self,
source: str,
destination: str,
title: str | None = None,
description: str | None = None,
close_source_branch: bool = True
) -> str:
response = requests.post(
self.get_endpoint("/pulls"),
headers=self.get_headers(),
json={
"title": title,
"body": description,
"head": source,
"base": destination,
},
)
if response.ok:
return "PR creado exitosamente: " + response.json()["html_url"]
else:
raise GitFlowError("Ocurrió un error al crear el PR: " + response.text)
def create_tag(self, tag: str, commit: str) -> str:
response = requests.post(
self.get_endpoint("/tags"),
headers=self.get_headers(),
json={"tag_name": tag, "target": commit},
)
if response.ok:
return "Tag creado exitosamente: " + tag
else:
raise GitFlowError("Ocurrió un error al crear el tag: " + response.text)
def get_endpoint(self, resource: str) -> str:
return f"{self.base_url}/api/v1/repos/{self.repository}{resource}"
def get_headers(self):
return {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "token " + self.token,
}
+28 -4
View File
@@ -1,9 +1,10 @@
import requests
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.remote.base import RemoteAPI from git_flow.remote.base import RemoteAPI
class GithubRemoteAPI(RemoteAPI): class GithubRemoteAPI(RemoteAPI):
API_ENDPOINT = "https://github.com" API_ENDPOINT = "https://api.github.com/repos"
def create_pull_request( def create_pull_request(
self, self,
@@ -13,10 +14,33 @@ class GithubRemoteAPI(RemoteAPI):
description: str | None = None, description: str | None = None,
close_source_branch: bool = True close_source_branch: bool = True
) -> str: ) -> str:
raise GitFlowError("not implemented yet!") response = requests.post(
self.get_endpoint("/pulls"),
headers=self.get_headers(),
json={
"title": title,
"body": description,
"head": source,
"base": destination,
},
)
if response.ok:
return "PR creado exitosamente: " + response.json()["html_url"]
else:
raise GitFlowError("Ocurrió un error al crear el PR: " + response.text)
def create_tag(self, tag: str, commit: str) -> str: def create_tag(self, tag: str, commit: str) -> str:
raise GitFlowError("not implemented yet!") response = requests.post(
self.get_endpoint("/git/refs"),
headers=self.get_headers(),
json={"ref": f"refs/tags/{tag}", "sha": commit},
)
if response.ok:
return "Tag creado exitosamente: " + tag
else:
raise GitFlowError("Ocurrió un error al crear el tag: " + response.text)
def get_endpoint(self, resource: str) -> str: def get_endpoint(self, resource: str) -> str:
return GithubRemoteAPI.API_ENDPOINT return f"{GithubRemoteAPI.API_ENDPOINT}/{self.repository}{resource}"
Generated
+37
View File
@@ -70,13 +70,17 @@ wheels = [
name = "git-flow-envs" name = "git-flow-envs"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "questionary" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" },
{ name = "typer" }, { name = "typer" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "questionary" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" },
{ name = "typer" }, { name = "typer" },
] ]
@@ -110,6 +114,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
] ]
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.20.0" version = "2.20.0"
@@ -119,6 +135,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
] ]
[[package]]
name = "questionary"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "prompt-toolkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.5" version = "2.32.5"
@@ -179,3 +207,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
] ]
[[package]]
name = "wcwidth"
version = "0.8.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
]