Compare commits

..
10 Commits
Author SHA1 Message Date
jt 98f71d5481 Merge pull request 'bugfix: commit small improvements' (#69) from bugfix/commit-small-improvements into main
Publish to PyPI / Bump-Build-Publish (push) Successful in 45s
Reviewed-on: #69
2026-09-28 17:27:53 +00:00
jt 76dd177421 refactor: inform instead of confirming current branch 2026-09-28 14:27:28 -03:00
jt e2e1c4b417 feature: change table border style 2026-09-28 14:23:49 -03:00
jt e13beae279 feature: show table with staged changes before comit 2026-09-28 14:22:49 -03:00
jt d54c740748 Merge pull request 'Bugfix/missing click module' (#68) from bugfix/missing-click-module into main
Publish to PyPI / Bump-Build-Publish (push) Successful in 44s
Reviewed-on: #68
2026-09-28 13:04:28 +00:00
jt 86a2768c81 bugfix: remove import click and supress typer errors only 2026-09-28 10:03:39 -03:00
jt 9dee43705a clean: delete test csv 2026-09-28 09:57:30 -03:00
jt aacc5b90e2 bugfix: prevent commits on environments 2026-09-26 21:15:20 -03:00
jt 103eb1137a Merge pull request 'feature: add branch types, commit types, support for breaking changes' (#67) from feature/extend-branch-and-commit-types into main
Publish to PyPI / Bump-Build-Publish (push) Successful in 16s
Reviewed-on: #67
2026-09-26 21:12:39 +00:00
jt e9d88b8113 feature: add branch types, commit types, support for breaking changes 2026-09-26 18:12:14 -03:00
11 changed files with 150 additions and 39 deletions
+13 -2
View File
@@ -53,7 +53,7 @@ más información):
- `init`: Inicializa el repositorio para utilizar git-flow (ramas principales y remoto). - `init`: Inicializa el repositorio para utilizar git-flow (ramas principales y remoto).
- `new`: Crea una nueva rama siguiendo CB. - `new`: Crea una nueva rama siguiendo CB.
- `commit`: Crea un commit siguiendo CC. - `commit`: Crea un commit siguiendo CC. No permite commitear directamente sobre un entorno.
- `merge`: Mergea la rama actual a uno de los entornos. - `merge`: Mergea la rama actual a uno de los entornos.
- `tag`: Crea un tag sobre el ultimo merge siguiendo SemVer (ver [Tag](#tag)). - `tag`: Crea un tag sobre el ultimo merge siguiendo SemVer (ver [Tag](#tag)).
- `release`: Prepara la rama actual para pasarse al siguiente entorno configurado. - `release`: Prepara la rama actual para pasarse al siguiente entorno configurado.
@@ -102,7 +102,18 @@ Comportamiento:
- El tag se crea sobre el último merge (first-parent) de la rama. Si ese merge ya tiene un tag, no se - El tag se crea sobre el último merge (first-parent) de la rama. Si ese merge ya tiene un tag, no se
hace nada. hace nada.
- La versión se calcula a partir de los tipos de commit (CC) del último merge. Si ningún commit - La versión se calcula a partir de los tipos de commit (CC) del último merge. Si ningún commit
implica un salto de versión, se avisa y el comando termina con código de salida 1. implica un salto de versión, se avisa y el comando termina con código de salida 1. Se toma el
mayor incremento entre los commits:
| Tipo de commit | Incremento |
| -------------------------------------------------------------- | ---------- |
| cualquier tipo marcado como breaking (`feature!: ...`) | major |
| `feature` (o `feat`) | minor |
| `bugfix` (o `fix`), `hotfix`, `refactor`, `perf`, `revert` | patch |
| `docs`, `style`, `test`, `build`, `ci`, `chore`, `wip` | ninguno |
| personalizado (opción `otro` en `git-flow commit`) | ninguno |
`git-flow commit` pregunta si el commit es un breaking change (salvo en commits `wip`).
- Con más de un entorno configurado, cada rama versiona por separado con el nombre del entorno como - Con más de un entorno configurado, cada rama versiona por separado con el nombre del entorno como
sufijo (por ejemplo, `v1.2.0-prod`). Con un solo entorno no se agrega sufijo. sufijo (por ejemplo, `v1.2.0-prod`). Con un solo entorno no se agrega sufijo.
- Si el merge viene del entorno anterior (por ejemplo, `dev` a `test`), no se calcula una nueva - Si el merge viene del entorno anterior (por ejemplo, `dev` a `test`), no se calcula una nueva
-2
View File
@@ -1,2 +0,0 @@
name,commit sha,author,date
worktree-kind-watching-hearth,e8c0c0b777d31fa94cd3f01fbf9c68b165d77015,Jonathan Teran Carballo,2026-09-13T23:18:47-03:00
1 name commit sha author date
2 worktree-kind-watching-hearth e8c0c0b777d31fa94cd3f01fbf9c68b165d77015 Jonathan Teran Carballo 2026-09-13T23:18:47-03:00
+28 -9
View File
@@ -1,15 +1,6 @@
GitFlowError = ValueError GitFlowError = ValueError
BRANCH_TYPES = [ BRANCH_TYPES = [
"feature",
"refactor",
"bugfix",
"hotfix",
"chore",
"docs",
]
COMMIT_TYPES = [
"feature", "feature",
"bugfix", "bugfix",
"hotfix", "hotfix",
@@ -18,6 +9,24 @@ COMMIT_TYPES = [
"docs", "docs",
"style", "style",
"test", "test",
"build",
"ci",
"chore",
]
COMMIT_TYPES = [
"feature",
"bugfix",
"hotfix",
"refactor",
"perf",
"revert",
"docs",
"style",
"test",
"build",
"ci",
"chore",
"wip", "wip",
] ]
@@ -32,15 +41,25 @@ SEMVER_PATCH = 1
# Commits con estos tipos no alteran la versión # Commits con estos tipos no alteran la versión
SEMVER_SKIP = 0 SEMVER_SKIP = 0
# Un commit marcado como breaking change (<tipo>!: <mensaje>) incrementa SEMVER_MAJOR sin importar
# el tipo. Los tipos que no figuran aquí no alteran la versión.
COMMIT_TYPE_INCREMENT = { COMMIT_TYPE_INCREMENT = {
"feature": SEMVER_MINOR, "feature": SEMVER_MINOR,
"bugfix": SEMVER_PATCH, "bugfix": SEMVER_PATCH,
"hotfix": SEMVER_PATCH, "hotfix": SEMVER_PATCH,
"refactor": SEMVER_PATCH, "refactor": SEMVER_PATCH,
"perf": SEMVER_PATCH, "perf": SEMVER_PATCH,
"revert": SEMVER_PATCH,
"docs": SEMVER_SKIP, "docs": SEMVER_SKIP,
"style": SEMVER_SKIP, "style": SEMVER_SKIP,
"test": SEMVER_SKIP, "test": SEMVER_SKIP,
"build": SEMVER_SKIP,
"ci": SEMVER_SKIP,
"chore": SEMVER_SKIP,
"wip": SEMVER_SKIP,
# Alias de Conventional Commits, para commits creados fuera de git-flow
"feat": SEMVER_MINOR,
"fix": SEMVER_PATCH,
} }
SUPPORTED_REMOTE_URL_SCHEMAS = [ SUPPORTED_REMOTE_URL_SCHEMAS = [
+33 -9
View File
@@ -5,10 +5,12 @@ from os.path import isfile
import questionary import questionary
import rich import rich
import rich.panel import rich.panel
from rich import box
from rich.table import Table from rich.table import Table
from git_flow import ( from git_flow import (
BRANCH_TYPES, BRANCH_TYPES,
COMMIT_TYPE_INCREMENT,
COMMIT_TYPES, COMMIT_TYPES,
FLOWCONFIG_FILENAME, FLOWCONFIG_FILENAME,
FLOWCONFIG_VERSION, FLOWCONFIG_VERSION,
@@ -50,15 +52,14 @@ def ensure_initialized():
) )
def ensure_right_branch(): def ensure_on_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 io_confirm(f"Rama actual: {branch}. ¿Es correcto?"):
return branch io_info(f"Rama actual: {branch}")
else: return branch
raise GitFlowError("Ejecución cancelada.")
def get_branch_env_and_type(branch: str) -> tuple[str, str]: def get_branch_env_and_type(branch: str) -> tuple[str, str]:
@@ -155,8 +156,30 @@ def is_valid_ticket(ticket: str) -> bool:
) )
CUSTOM_COMMIT_TYPE_OPTION = "otro"
def get_commit_message(commit_types: list[str] = COMMIT_TYPES) -> str: def get_commit_message(commit_types: list[str] = COMMIT_TYPES) -> str:
commit_type = io_choice("Tipo de commit", commit_types) commit_type = io_choice("Tipo de commit", commit_types + [CUSTOM_COMMIT_TYPE_OPTION])
if commit_type == CUSTOM_COMMIT_TYPE_OPTION:
commit_type = io_prompt(
"Tipo de commit personalizado",
strip=True,
validator=lambda s: re.fullmatch(r"\w+", s.strip()) is not None,
instruction="letras, números o _",
)
if commit_type not in COMMIT_TYPE_INCREMENT:
io_warning(
f"El tipo '{commit_type}' no produce un salto de versión, "
"salvo que se marque como breaking change."
)
# Los commits WIP se squashean en uno final, que es el que puede marcarse como breaking
if commit_type != "wip" and io_confirm("¿Es un breaking change?", False):
commit_type += "!"
commit_message = io_prompt( commit_message = io_prompt(
"Mensaje de commit", "Mensaje de commit",
validator=lambda s: 0 < len(s) and len(s) < 100, validator=lambda s: 0 < len(s) and len(s) < 100,
@@ -166,7 +189,7 @@ def get_commit_message(commit_types: list[str] = COMMIT_TYPES) -> str:
# <tipo>[(alcance)][!]: <mensaje>, para no confundir "Merge pull request 'feature: ...'" con un tipo # <tipo>[(alcance)][!]: <mensaje>, para no confundir "Merge pull request 'feature: ...'" con un tipo
COMMIT_SUBJECT_RE = re.compile(r"^(\w+)(?:\([^)]*\))?!?:\s*(.*)$") COMMIT_SUBJECT_RE = re.compile(r"^(\w+)(?:\([^)]*\))?(!)?:\s*(.*)$")
def show_commits(title: str, revision_range: str): def show_commits(title: str, revision_range: str):
@@ -178,7 +201,7 @@ def show_commits(title: str, revision_range: str):
if not lines: if not lines:
raise GitFlowError("No hay cambios a mergear.") raise GitFlowError("No hay cambios a mergear.")
table = Table(title=title, title_justify="left", title_style="bold") table = Table(title=title, title_justify="left", title_style="bold", box=box.SQUARE)
table.add_column("Fecha", style="dim", no_wrap=True) table.add_column("Fecha", style="dim", no_wrap=True)
table.add_column("Tipo", style="cyan", no_wrap=True) table.add_column("Tipo", style="cyan", no_wrap=True)
table.add_column("Mensaje") table.add_column("Mensaje")
@@ -188,7 +211,8 @@ def show_commits(title: str, revision_range: str):
for line in lines: for line in lines:
timestamp, subject = line.split("\t", 1) timestamp, subject = line.split("\t", 1)
match = COMMIT_SUBJECT_RE.match(subject) match = COMMIT_SUBJECT_RE.match(subject)
commit_type, message = match.groups() if match else ("-", subject) commit_type, breaking, message = match.groups() if match else ("-", None, subject)
commit_type += breaking or ""
table.add_row(format_time_ago(now - int(timestamp)), commit_type, message) table.add_row(format_time_ago(now - int(timestamp)), commit_type, message)
console.print(table) console.print(table)
+60 -7
View File
@@ -1,10 +1,13 @@
from datetime import datetime from datetime import datetime
import typer import typer
from rich import box
from rich.table import Table
import git_flow.command.base as base
from git_flow import TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError from git_flow import TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
from git_flow.command import base
from git_flow.git import Git from git_flow.git import Git
from git_flow.io import console
app = typer.Typer() app = typer.Typer()
@@ -13,7 +16,16 @@ app = typer.Typer()
def commit(): def commit():
"""Crea un commit siguiendo el formato de Conventional Commits""" """Crea un commit siguiendo el formato de Conventional Commits"""
base.ensure_initialized() base.ensure_initialized()
branch = base.ensure_right_branch() branch = base.ensure_on_branch()
# Una rama WIP termina squasheando en su rama original, que tampoco puede ser un entorno
target_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
if target_branch in base.environments:
raise GitFlowError(
f"No se puede commitear directamente sobre el entorno '{target_branch}'. "
"Debe crear una rama de trabajo con el comando `new`."
)
if not _has_files_staged(): if not _has_files_staged():
base.io_warning("No hay cambios en el indice para commitear.") base.io_warning("No hay cambios en el indice para commitear.")
@@ -22,6 +34,11 @@ def commit():
else: else:
raise GitFlowError("Debe agregar algún cambio al indice para continuar.") raise GitFlowError("Debe agregar algún cambio al indice para continuar.")
_show_index()
if not base.io_confirm("¿Desea commitear estos cambios?"):
raise GitFlowError("Ejecución cancelada.")
message = base.get_commit_message() message = base.get_commit_message()
commit_type = message[: message.index(":")] commit_type = message[: message.index(":")]
@@ -54,10 +71,46 @@ def _has_files_staged():
if not status: if not status:
raise GitFlowError("No hay cambios para commitear.") raise GitFlowError("No hay cambios para commitear.")
files_in_index = [] return any(_is_staged(s) for s in status)
for s in status:
if s.worktree != "?" and s.worktree != " ":
files_in_index.append(s.file)
return len(files_in_index) > 0 def _is_staged(s: Git.Status) -> bool:
return s.index != "?" and s.index != " "
INDEX_STATES = {
"M": "modificado",
"T": "cambio de tipo",
"A": "agregado",
"D": "eliminado",
"R": "renombrado",
"C": "copiado",
"U": "en conflicto",
}
def _show_index():
"""Muestra en una tabla los archivos del indice (los que entran en el commit), e informa
cuántos archivos quedan con cambios fuera del indice."""
status = Git.status()
staged = [s for s in status if _is_staged(s)]
not_staged = [s for s in status if s.worktree != " "]
table = Table(
title="Cambios a commitear",
title_justify="left",
title_style="bold",
box=box.SQUARE,
)
table.add_column("Estado", style="cyan", no_wrap=True)
table.add_column("Archivo")
for s in staged:
table.add_row(INDEX_STATES.get(s.index, s.index), s.file)
console.print(table)
if not_staged:
base.io_info(
f"{len(not_staged)} archivo(s) con cambios fuera del indice no se incluirán en el commit."
)
+1 -1
View File
@@ -15,7 +15,7 @@ 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"""
base.ensure_initialized() base.ensure_initialized()
branch = base.ensure_right_branch() branch = base.ensure_on_branch()
if branch in base.environments: if branch in base.environments:
if branch == base.environments[-1]: if branch == base.environments[-1]:
+1 -1
View File
@@ -14,7 +14,7 @@ def new():
"""Crea una nueva rama siguiendo Conventional Branches""" """Crea una nueva rama siguiendo Conventional Branches"""
base.ensure_initialized() base.ensure_initialized()
branch = base.ensure_right_branch() branch = base.ensure_on_branch()
if branch not in base.environments: if branch not in base.environments:
base.io_warning("La rama actual no corresponde a un entorno configurado.") base.io_warning("La rama actual no corresponde a un entorno configurado.")
+1 -1
View File
@@ -12,7 +12,7 @@ def release():
"""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"""
base.ensure_initialized() base.ensure_initialized()
branch = base.ensure_right_branch() branch = base.ensure_on_branch()
env, _ = base.get_branch_env_and_type(branch) env, _ = base.get_branch_env_and_type(branch)
if env == base.environments[-1]: if env == base.environments[-1]:
+8 -2
View File
@@ -128,8 +128,14 @@ def get_next_tag_from_commits(
) -> str | None: ) -> str | None:
increment = SEMVER_SKIP increment = SEMVER_SKIP
for commit in commits: for commit in commits:
commit_type = commit.partition(":")[0] match = base.COMMIT_SUBJECT_RE.match(commit)
increment = max(increment, COMMIT_TYPE_INCREMENT.get(commit_type, SEMVER_SKIP))
if not match:
continue
commit_type, breaking, _ = match.groups()
commit_increment = SEMVER_MAJOR if breaking else COMMIT_TYPE_INCREMENT.get(commit_type)
increment = max(increment, commit_increment or SEMVER_SKIP)
if increment == SEMVER_SKIP: if increment == SEMVER_SKIP:
return None return None
+4 -3
View File
@@ -26,8 +26,9 @@ class Git:
file: str file: str
def __init__(self, line: str) -> None: def __init__(self, line: str) -> None:
self.worktree = line[0] # Formato porcelain "XY <file>": X es el estado en el indice, Y en el worktree
self.index = line[1] self.index = line[0]
self.worktree = line[1]
self.file = line[3:].strip("\n") self.file = line[3:].strip("\n")
command: list[str] command: list[str]
@@ -170,7 +171,7 @@ class Git:
process = self._get(**kwargs) process = self._get(**kwargs)
lines = process.stdout.splitlines() lines = process.stdout.splitlines()
return lines if not strip else list(map(lambda l: l.strip(), lines)) return lines if not strip else [l.strip() for l in lines]
def firstline(self, **kwargs): def firstline(self, **kwargs):
lines = self.lines(**kwargs) lines = self.lines(**kwargs)
+1 -2
View File
@@ -3,7 +3,6 @@
from subprocess import CalledProcessError, SubprocessError from subprocess import CalledProcessError, SubprocessError
from typing import Annotated from typing import Annotated
import click
import typer import typer
from rich.markup import escape from rich.markup import escape
from rich.traceback import Traceback from rich.traceback import Traceback
@@ -70,7 +69,7 @@ def _print_error(message: str, title: str, e: BaseException):
base.io_error(message, title=title) base.io_error(message, title=title)
console.print( console.print(
Traceback.from_exception( Traceback.from_exception(
type(e), e, e.__traceback__, suppress=[click, typer] type(e), e, e.__traceback__, suppress=[typer]
) )
) )
else: else: