From 22ba8f5e5f600236b6ec43d306f346ec603f1656 Mon Sep 17 00:00:00 2001 From: Jonathan Teran Carballo Date: Mon, 21 Sep 2026 09:15:48 -0300 Subject: [PATCH] refactor: update remote tag fetching behaviour so it uses api instead of shallow clone --- src/git_flow/command/tag.py | 29 +++++++++++++++++++---------- src/git_flow/git.py | 11 +++++++++-- src/git_flow/remote/base.py | 32 ++++++++++++++++++++++++++++++-- src/git_flow/remote/bitbucket.py | 16 ++++++++++++++++ src/git_flow/remote/gitea.py | 5 +++++ src/git_flow/remote/github.py | 3 +++ src/git_flow/remote/gitlab.py | 7 +++++++ 7 files changed, 89 insertions(+), 14 deletions(-) diff --git a/src/git_flow/command/tag.py b/src/git_flow/command/tag.py index dea2ce5..2c944f3 100644 --- a/src/git_flow/command/tag.py +++ b/src/git_flow/command/tag.py @@ -33,17 +33,26 @@ def tag(token: Optional[str] = None): raise GitFlowError("No hay token disponible") merge_commit = get_last_merge_commit() + # Con varios entornos cada uno versiona por separado (v1.0.0-prod), con uno solo no hace falta + suffix = f"-{target}" if len(base.environments) > 1 else "" if ci: - # Pipeline: no asumimos historial completo, los commits del PR se piden a la API + # Commits se obtienen por API del ultimo PR / MR mergeado commits = base.get_remote_api(token).get_merge_commits(merge_commit) commits = [c for c in commits if not c.is_merge] + # Último tag se obtiene por API filtrando por sufijo + current_tag = base.get_remote_api(token).get_latest_tag(suffix) else: + # Commits se obtienen del último merge, listando hasta el primer forkpoint branch = Git("show", merge_commit + "^2", patch=False, format="%h").firstline() fork_point = Git.get_first_fork_point(branch, target) commits = Git.get_commits(fork_point, branch) + # Último tag se obtiene localmente + current_tag = Git.get_current_tag(suffix) - next_tag = get_next_tag_from_commits([c.subject for c in commits], target) + next_tag = get_next_tag_from_commits( + [c.subject for c in commits], suffix, current_tag + ) if not next_tag: base.io_info( @@ -90,17 +99,19 @@ def check_not_tagged(commit: str): raise GitFlowError("El commit a taggear ya tiene tag: " + tag) -def get_next_tag_from_commits(commits: list[str], branch: str) -> str | None: +def get_next_tag_from_commits( + commits: list[str], suffix: str, current_tag: str | None +) -> 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)) - tag = Git.get_current_tag() or f"v0.0.0-{branch}" + if increment == SEMVER_SKIP: + return None - until_dash = tag.index("-") if "-" in tag else None - suffix = tag[until_dash:] if until_dash is not None else "" - [major, minor, patch] = map(int, tag[1:until_dash].split(".")) + version = (current_tag or "v0.0.0" + suffix).removeprefix("v").removesuffix(suffix) + [major, minor, patch] = map(int, version.split(".")) if increment == SEMVER_MAJOR: major += 1 @@ -111,7 +122,5 @@ def get_next_tag_from_commits(commits: list[str], branch: str) -> str | None: patch = 0 elif increment == SEMVER_PATCH: patch += 1 - elif increment == SEMVER_SKIP: - suffix = "-r" + Git("rev-list", "HEAD", count=True).firstline() - return None if increment == SEMVER_SKIP else f"v{major}.{minor}.{patch}{suffix}" + return f"v{major}.{minor}.{patch}{suffix}" diff --git a/src/git_flow/git.py b/src/git_flow/git.py index 8b2e29a..0c263f8 100644 --- a/src/git_flow/git.py +++ b/src/git_flow/git.py @@ -63,8 +63,15 @@ class Git: return list(map(Git.Status, Git("status", porcelain=True).lines(False))) @staticmethod - def get_current_tag(): - return Git("describe", abbrev="0", tags=True).firstline(check=False, print="Obteniendo tag actual") + def get_current_tag(suffix: str = ""): + """Último tag SemVer alcanzable desde HEAD, con el sufijo exacto indicado.""" + return Git( + "describe", + abbrev="0", + tags=True, + match="v[0-9]*.[0-9]*.[0-9]*" + suffix, + exclude=None if suffix else "*-*", + ).firstline(check=False, print="Obteniendo tag actual") @staticmethod def get_first_fork_point(branch: str, env: str): diff --git a/src/git_flow/remote/base.py b/src/git_flow/remote/base.py index df959be..7a239b0 100644 --- a/src/git_flow/remote/base.py +++ b/src/git_flow/remote/base.py @@ -1,5 +1,7 @@ from abc import ABC, abstractmethod +import re + import giturlparse import requests @@ -72,6 +74,12 @@ class RemoteAPI(ABC): viejo), sin depender del historial local.""" pass + @abstractmethod + def list_tags(self, suffix: str = "") -> list[str]: + """Nombres de los tags del repositorio, sin orden garantizado. `suffix` es solo una pista + para que la API filtre; el filtrado definitivo se hace en `get_latest_tag`.""" + pass + @abstractmethod def get_endpoint(self, resource: str) -> str: pass @@ -91,13 +99,33 @@ class RemoteAPI(ABC): return response - def get_all_pages(self, url: str, page_size: int = 50) -> list: + def get_latest_tag(self, suffix: str = "") -> str | None: + """Tag SemVer más alto (vX.Y.Z + suffix exacto), o None si no hay ninguno.""" + pattern = re.compile(rf"^v(\d+)\.(\d+)\.(\d+){re.escape(suffix)}$") + versions = {} + + for name in self.list_tags(suffix): + match = pattern.match(name) + + if match: + versions[tuple(map(int, match.groups()))] = name + + return versions[max(versions)] if versions else None + + def get_all_pages( + self, + url: str, + page_size: int = 50, + params: dict | None = None, + size_param: str = "per_page", + ) -> list: """Recorre un endpoint paginado por número de página (page / per_page).""" items: list = [] page = 1 while True: - batch = self.get_json(url, {"page": page, "per_page": page_size}).json() + query = {**(params or {}), "page": page, size_param: page_size} + batch = self.get_json(url, query).json() items += batch if len(batch) < page_size: diff --git a/src/git_flow/remote/bitbucket.py b/src/git_flow/remote/bitbucket.py index 85be4db..d7b6ce1 100644 --- a/src/git_flow/remote/bitbucket.py +++ b/src/git_flow/remote/bitbucket.py @@ -77,5 +77,21 @@ class BitbucketRemoteAPI(RemoteAPI): return commits + def list_tags(self, suffix: str = "") -> list[str]: + names: list[str] = [] + url: str | None = self.get_endpoint("refs/tags") + params: dict[str, int|str] | None = {"pagelen": 100} + + if suffix: + params["q"] = f'name ~ "{suffix}"' + + while url: + page = self.get_json(url, params).json() + names += [t["name"] for t in page["values"]] + url = page.get("next") + params = None # "next" ya incluye la query + + return names + def get_endpoint(self, resource: str) -> str: return f"{BitbucketRemoteAPI.API_ENDPOINT}/{self.repository}/{resource}" diff --git a/src/git_flow/remote/gitea.py b/src/git_flow/remote/gitea.py index e99c38e..3169fbd 100644 --- a/src/git_flow/remote/gitea.py +++ b/src/git_flow/remote/gitea.py @@ -74,6 +74,11 @@ class GiteaRemoteAPI(RemoteAPI): for c in reversed(items) ] + def list_tags(self, suffix: str = "") -> list[str]: + tags = self.get_all_pages(self.get_endpoint("/tags"), 50, size_param="limit") + + return [t["name"] for t in tags] + def get_endpoint(self, resource: str) -> str: return f"{self.base_url}/api/v1/repos/{self.repository}{resource}" diff --git a/src/git_flow/remote/github.py b/src/git_flow/remote/github.py index c660d95..aed1541 100644 --- a/src/git_flow/remote/github.py +++ b/src/git_flow/remote/github.py @@ -63,5 +63,8 @@ class GithubRemoteAPI(RemoteAPI): for c in reversed(items) ] + def list_tags(self, suffix: str = "") -> list[str]: + return [t["name"] for t in self.get_all_pages(self.get_endpoint("/tags"), 100)] + def get_endpoint(self, resource: str) -> str: return f"{GithubRemoteAPI.API_ENDPOINT}/{self.repository}{resource}" diff --git a/src/git_flow/remote/gitlab.py b/src/git_flow/remote/gitlab.py index 820f017..98e05d2 100644 --- a/src/git_flow/remote/gitlab.py +++ b/src/git_flow/remote/gitlab.py @@ -70,6 +70,13 @@ class GitlabRemoteAPI(RemoteAPI): for c in items ] + def list_tags(self, suffix: str = "") -> list[str]: + # "search" soporta "$" para anclar al final del nombre + params = {"search": suffix + "$"} if suffix else None + tags = self.get_all_pages(self.get_endpoint("/repository/tags"), 100, params) + + return [t["name"] for t in tags] + def get_endpoint(self, resource: str) -> str: project_id = quote(self.repository, safe="") return f"{self.base_url}/api/v4/projects/{project_id}{resource}" -- 2.54.0