Compare commits

..
11 Commits
Author SHA1 Message Date
gitea-actions d4dc694db6 chore: bump version and update CHANGELOG.md [skip ci] 2026-09-14 00:23:39 +00:00
jt 9120698900 Merge pull request 'refactor: remove release prompts' (#15) from refactor/remove-release-prompts into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 17s
Reviewed-on: #15
2026-09-14 00:23:28 +00:00
jt c035a8867e refactor: cambiar incremento de commits refactor y perf 2026-09-13 21:22:41 -03:00
jt 0c31c747ff refactor: eliminar preguntas innecesarias de release y merge 2026-09-13 21:22:14 -03:00
gitea-actions 12ce68d954 chore: bump version and update CHANGELOG.md [skip ci] 2026-09-13 23:56:52 +00:00
jt 29eacfdc4d Merge pull request 'feature: add gitlab remote support' (#14) from feature/gitlab-remote-support into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 2m40s
Reviewed-on: #14
2026-09-13 23:54:15 +00:00
jt 748d4201cc feature: add gitlab remote support 2026-09-13 20:48:49 -03:00
gitea-actions 94a219ed82 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-31 15:25:28 +00:00
jt 3f3650360a Merge pull request 'Feature/parse git remote url with library' (#12) from feature/parse-git-remote-url-with-library into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 55s
Reviewed-on: #12
2026-07-31 15:24:35 +00:00
Jonathan Teran 11a48a922c style: format with ruff 2026-07-31 12:22:39 -03:00
Jonathan Teran f4adb431db feature: parse git remote url with giturlparse 2026-07-31 12:22:14 -03:00
11 changed files with 141 additions and 64 deletions
+23
View File
@@ -1,3 +1,26 @@
## v1.20.1 - 2026-09-14
- **refactor**: cambiar incremento de commits refactor y perf [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (c035a88)
- **refactor**: eliminar preguntas innecesarias de release y merge [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (0c31c74)
---
## v1.20.0 - 2026-09-13
- **feature**: add gitlab remote support [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (748d420)
---
## v1.19.0 - 2026-07-31
- **style**: format with ruff [[jteran](mailto:jteran@renatre.org.ar)] (11a48a9)
- **feature**: parse git remote url with giturlparse [[jteran](mailto:jteran@renatre.org.ar)] (f4adb43)
---
## v1.18.0 - 2026-07-29 ## v1.18.0 - 2026-07-29
- **bugfix**: change checkout version to v7 [[jteran](mailto:jteran@renatre.org.ar)] (c58c4ce) - **bugfix**: change checkout version to v7 [[jteran](mailto:jteran@renatre.org.ar)] (c58c4ce)
+1
View File
@@ -49,6 +49,7 @@ El comando solicitará 2 opciones:
- [x] Bitbucket - [x] Bitbucket
- [x] Github - [x] Github
- [x] Gitea - [x] Gitea
- [x] Gitlab
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", "rich", "questionary" "requests", "typer", "rich", "questionary", "giturlparse"
] ]
authors = [ authors = [
{name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"}, {name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"},
+6 -5
View File
@@ -36,16 +36,17 @@ COMMIT_TYPE_INCREMENT = {
"feature": SEMVER_MINOR, "feature": SEMVER_MINOR,
"bugfix": SEMVER_PATCH, "bugfix": SEMVER_PATCH,
"hotfix": SEMVER_PATCH, "hotfix": SEMVER_PATCH,
"refactor": SEMVER_SKIP, "refactor": SEMVER_PATCH,
"perf": SEMVER_SKIP, "perf": SEMVER_PATCH,
"docs": SEMVER_SKIP, "docs": SEMVER_SKIP,
"style": SEMVER_SKIP, "style": SEMVER_SKIP,
"test": SEMVER_SKIP, "test": SEMVER_SKIP,
} }
SUPPORTED_REMOTE_APIS = [ SUPPORTED_REMOTE_URL_SCHEMAS = [
"bitbucket.org", "ssh",
"github.com", "http",
"https"
] ]
REPOSITORY_TOKEN_FILENAME = ".repository-token" REPOSITORY_TOKEN_FILENAME = ".repository-token"
+4
View File
@@ -11,6 +11,7 @@ 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.remote.gitea import GiteaRemoteAPI
from git_flow.remote.gitlab import GitlabRemoteAPI
from git_flow.io import * from git_flow.io import *
from os.path import isfile from os.path import isfile
@@ -123,6 +124,9 @@ def get_remote_api(token: str) -> RemoteAPI:
if remote_type == "gitea": if remote_type == "gitea":
remote_schema = "https://" if schema == "ssh://" else schema remote_schema = "https://" if schema == "ssh://" else schema
return GiteaRemoteAPI(remote_schema + host, repository, token) return GiteaRemoteAPI(remote_schema + host, repository, token)
elif remote_type == "gitlab":
remote_schema = "https://" if schema == "ssh://" else schema
return GitlabRemoteAPI(remote_schema + host, repository, token)
elif remote_type == "bitbucket": elif remote_type == "bitbucket":
return BitbucketRemoteAPI(repository, token) return BitbucketRemoteAPI(repository, token)
elif remote_type == "github": elif remote_type == "github":
+1 -1
View File
@@ -101,7 +101,7 @@ uno nuevo.""",
remote = base.io_choice("Remoto", remotes) remote = base.io_choice("Remoto", remotes)
remote_type = base.io_choice( remote_type = base.io_choice(
"Tipo de remoto:", ["bitbucket", "github", "gitea"] "Tipo de remoto:", ["bitbucket", "github", "gitea", "gitlab"]
) )
return (remote, remote_type) return (remote, remote_type)
-4
View File
@@ -119,10 +119,6 @@ def create_pull_request(token: str, branch: str, target: str):
base.io_success(message, "Rama creada") base.io_success(message, "Rama creada")
if base.io_confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"):
Git("switch", target).exec(print="Cambiando a rama objetivo")
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 base.environments: if branch in base.environments:
+6 -36
View File
@@ -1,16 +1,14 @@
from typing import Optional
import typer import typer
import git_flow.command.base as base import git_flow.command.base as base
from git_flow import COMMIT_TYPES, GitFlowError from git_flow import GitFlowError
from git_flow.git import Git from git_flow.git import Git
app = typer.Typer() app = typer.Typer()
@app.command() @app.command()
def release(group: Optional[str] = None): 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()
@@ -44,45 +42,17 @@ def release(group: Optional[str] = None):
for commit in commits: for commit in commits:
print("- " + commit) print("- " + commit)
grouping = group is not None if has_remote and Git.get_tracking_branch(next_env):
Git("switch", next_env).exec(print="Cambiando a la rama objetivo")
if not group:
if base.io_confirm("¿Desea agrupar este release con otra rama?", False):
grouping = True
group = base.io_choice(
"Grupo release: ", Git.get_branches("release/" + next_env + "/")
)
else:
group = next_env
if has_remote and Git.get_tracking_branch(group):
Git("switch", group).exec(print="Cambiando a la rama objetivo")
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 base.io_confirm( Git("switch", next_branch, create=True).exec(print="Creando rama release")
f"¿Desea reemplazar los {len(commits)} commits de la rama por uno solo?", False
):
base.io_info("Debe ingresar el mensaje del commit a crear.")
message = base.get_commit_message(COMMIT_TYPES[:-1])
Git("switch", next_branch, fork_point, create=True).exec( status = Git("rebase", fork_point, next_branch, onto=next_env).code(
print="Creando rama release en base"
)
Git("merge", branch, squash=True).exec(print="Squasheando commits en uno solo")
Git("commit", m=message).exec(print="Creando commit único")
else:
Git("switch", next_branch, create=True).exec(print="Creando rama release")
status = Git("rebase", fork_point, next_branch, onto=group).code(
print="Moviendo cambios hacia el siguiente ambiente" print="Moviendo cambios hacia el siguiente ambiente"
) )
if status: if status:
Git("rebase", abort=True).exec(print="Deshaciendo cambios por conflictos") Git("rebase", abort=True).exec(print="Deshaciendo cambios por conflictos")
raise GitFlowError("Error al realizar pasaje de cambios a rama objetivo") raise GitFlowError("Error al realizar pasaje de cambios a rama objetivo")
elif grouping:
Git("switch", group).exec(print="Cambiando a rama objetivo")
Git("merge", next_branch, ff_only=True).exec(
print="Mergeando cambios de la nueva rama"
)
+29 -17
View File
@@ -1,6 +1,8 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from git_flow import SUPPORTED_REMOTE_APIS, GitFlowError import giturlparse
from git_flow import SUPPORTED_REMOTE_URL_SCHEMAS, GitFlowError
from git_flow.git import Git from git_flow.git import Git
@@ -11,25 +13,35 @@ class RemoteAPI(ABC):
@staticmethod @staticmethod
def parse(remote: str): def parse(remote: str):
"""Soportamos 3 tipos de URL: http, https y ssh. Si no se especifica el esquema, asumimos ssh:
Ejemplos:
- http://gitea/user/repo
- https://gitea.com/user/repo.git
- git@bitbucket.org:user/repo
- git.mygiteainstance.com:user/repo.git
- ssh://git@git.mygiteainstance.com:1234/user/repo.git
"""
url = Git("remote", "get-url", remote).firstline() url = Git("remote", "get-url", remote).firstline()
if url.startswith("git@"): parsed_url = giturlparse.parse(url) # Validamos que la URL sea válida
at_pos = url.index("@")
colon_pos = url.index(":") if not parsed_url.valid:
schema = "ssh://"
host = url[at_pos+1:colon_pos]
repository = url[colon_pos+1:]
elif url.startswith("http://") or url.startswith("https://"):
start_pos = url.index("://") + 3
uri_start = url.index("/", start_pos)
schema = url[:start_pos]
host = url[start_pos:uri_start]
repository = url[uri_start+1:]
else:
raise GitFlowError(f"La URL del remoto {remote} es inválida: {url}") raise GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
repository = repository.removesuffix(".git") if not parsed_url.protocol in SUPPORTED_REMOTE_URL_SCHEMAS:
return [schema, host, repository] raise GitFlowError(
f"La URL del remoto {remote} no tiene un esquema soportado: {url}"
)
# Cuando el host es seteado por pipeline puede ser "http://gitea/OWNER/REPO", en ese caso
# la libreria parsea OWNER="" y REPO="OWNER/REPO"
repository = (
"/".join(filter(None, [parsed_url.owner, parsed_url.repo]))
.removeprefix("/")
.removesuffix(".git")
)
return [parsed_url.protocol + "://", parsed_url.host, repository]
@abstractmethod @abstractmethod
def create_pull_request( def create_pull_request(
@@ -38,7 +50,7 @@ class RemoteAPI(ABC):
destination: str, destination: str,
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
close_source_branch: bool = True close_source_branch: bool = True,
) -> str: ) -> str:
pass pass
+59
View File
@@ -0,0 +1,59 @@
from urllib.parse import quote
import requests
from git_flow import GitFlowError
from git_flow.remote.base import RemoteAPI
class GitlabRemoteAPI(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("/merge_requests"),
headers=self.get_headers(),
json={
"title": title,
"description": description,
"source_branch": source,
"target_branch": destination,
"remove_source_branch": close_source_branch,
},
)
if response.ok:
return "PR creado exitosamente: " + response.json()["web_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("/repository/tags"),
headers=self.get_headers(),
json={"tag_name": tag, "ref": 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:
project_id = quote(self.repository, safe="")
return f"{self.base_url}/api/v4/projects/{project_id}{resource}"
def get_headers(self):
return {
"Accept": "application/json",
"Content-Type": "application/json",
"PRIVATE-TOKEN": self.token,
}
Generated
+11
View File
@@ -70,6 +70,7 @@ wheels = [
name = "git-flow-envs" name = "git-flow-envs"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "giturlparse" },
{ name = "questionary" }, { name = "questionary" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" }, { name = "rich" },
@@ -78,12 +79,22 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "giturlparse" },
{ name = "questionary" }, { name = "questionary" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" }, { name = "rich" },
{ name = "typer" }, { name = "typer" },
] ]
[[package]]
name = "giturlparse"
version = "0.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/8a/b70b84cc78f9059d627f09560c81a11e8f046570d220a24e169489cad6c9/giturlparse-0.15.0.tar.gz", hash = "sha256:9af3f1fd5c4a0cac94ddb283593635005646393ee0debbe330d1bdff8866bf2c", size = 16138, upload-time = "2026-06-16T07:28:56.021Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/96/147a2771ab655b9353781fb2f95c94eaf1d8576dccc991c1a61d0d355067/giturlparse-0.15.0-py2.py3-none-any.whl", hash = "sha256:76d2e6983b037356ab99b30683e533ac3db96409b68e2163a20fc3aff6446f10", size = 16683, upload-time = "2026-06-16T07:28:55.184Z" },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.11" version = "3.11"