Compare commits

...
7 Commits
9 changed files with 125 additions and 22 deletions
+15
View File
@@ -1,3 +1,18 @@
## 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"},
+4 -3
View File
@@ -43,9 +43,10 @@ COMMIT_TYPE_INCREMENT = {
"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)
+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"