diff --git a/README.md b/README.md index d465afa..58d5c3a 100644 --- a/README.md +++ b/README.md @@ -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 hace nada. - 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 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 diff --git a/src/git_flow/__init__.py b/src/git_flow/__init__.py index b41221e..d2e284b 100644 --- a/src/git_flow/__init__.py +++ b/src/git_flow/__init__.py @@ -1,15 +1,6 @@ GitFlowError = ValueError BRANCH_TYPES = [ - "feature", - "refactor", - "bugfix", - "hotfix", - "chore", - "docs", -] - -COMMIT_TYPES = [ "feature", "bugfix", "hotfix", @@ -18,6 +9,24 @@ COMMIT_TYPES = [ "docs", "style", "test", + "build", + "ci", + "chore", +] + +COMMIT_TYPES = [ + "feature", + "bugfix", + "hotfix", + "refactor", + "perf", + "revert", + "docs", + "style", + "test", + "build", + "ci", + "chore", "wip", ] @@ -32,15 +41,25 @@ SEMVER_PATCH = 1 # Commits con estos tipos no alteran la versión SEMVER_SKIP = 0 +# Un commit marcado como breaking change (!: ) incrementa SEMVER_MAJOR sin importar +# el tipo. Los tipos que no figuran aquí no alteran la versión. COMMIT_TYPE_INCREMENT = { "feature": SEMVER_MINOR, "bugfix": SEMVER_PATCH, "hotfix": SEMVER_PATCH, "refactor": SEMVER_PATCH, "perf": SEMVER_PATCH, + "revert": SEMVER_PATCH, "docs": SEMVER_SKIP, "style": 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 = [ diff --git a/src/git_flow/command/base.py b/src/git_flow/command/base.py index 7c3b0c4..9e6e8ae 100644 --- a/src/git_flow/command/base.py +++ b/src/git_flow/command/base.py @@ -9,6 +9,7 @@ from rich.table import Table from git_flow import ( BRANCH_TYPES, + COMMIT_TYPE_INCREMENT, COMMIT_TYPES, FLOWCONFIG_FILENAME, FLOWCONFIG_VERSION, @@ -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: - 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( "Mensaje de commit", 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: # [(alcance)][!]: , 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): @@ -188,7 +211,8 @@ def show_commits(title: str, revision_range: str): for line in lines: timestamp, subject = line.split("\t", 1) 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) console.print(table) diff --git a/src/git_flow/command/tag.py b/src/git_flow/command/tag.py index 6abb53b..0d413ec 100644 --- a/src/git_flow/command/tag.py +++ b/src/git_flow/command/tag.py @@ -128,8 +128,14 @@ def get_next_tag_from_commits( ) -> str | None: increment = SEMVER_SKIP for commit in commits: - commit_type = commit.partition(":")[0] - increment = max(increment, COMMIT_TYPE_INCREMENT.get(commit_type, SEMVER_SKIP)) + match = base.COMMIT_SUBJECT_RE.match(commit) + + 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: return None