Compare commits
82
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31513a3e3a | ||
|
|
c45f065e77 | ||
|
|
7f7dd7b538 | ||
|
|
adcb53156a | ||
|
|
5ca817c796 | ||
|
|
2be9b50ac4 | ||
|
|
347b8b9556 | ||
|
|
9b547bd6a9 | ||
|
|
f05582b50f | ||
|
|
a1a9a7acac | ||
|
|
190b4b8408 | ||
|
|
f9fb5a62d1 | ||
|
|
09d0de2860 | ||
|
|
f233adfd1d | ||
|
|
0cdc01291c | ||
|
|
098a4724ca | ||
|
|
9167390098 | ||
|
|
a08666cac3 | ||
|
|
726df217ec | ||
|
|
07d4ff960e | ||
|
|
919e680e75 | ||
|
|
add4cd2747 | ||
|
|
e4833c9ed9 | ||
|
|
7315681b23 | ||
|
|
ba1c9bd68d | ||
|
|
1a7ae338ff | ||
|
|
0120faac9e | ||
|
|
febd18c3b3 | ||
|
|
069c33bc17 | ||
|
|
1d186e117c | ||
|
|
c0f7642687 | ||
|
|
a884c28094 | ||
|
|
0782cfb041 | ||
|
|
320d40a211 | ||
|
|
a5a753f429 | ||
|
|
4968667dad | ||
|
|
79ae88b012 | ||
|
|
4666503f32 | ||
|
|
4c089eb8da | ||
|
|
dc858d026f | ||
|
|
5989699f08 | ||
|
|
ecfe058f8d | ||
|
|
ffe813bf06 | ||
|
|
6cd6771241 | ||
|
|
1cc2de0b75 | ||
|
|
0e669019f4 | ||
|
|
d456565b2f | ||
|
|
74a78d30a1 | ||
|
|
ad8984cb28 | ||
|
|
53b02f1f8f | ||
|
|
bf6a0e8443 | ||
|
|
12351e64ba | ||
|
|
cb742425c1 | ||
|
|
bd47b40aff | ||
|
|
e94ee2ccc7 | ||
|
|
069c62b697 | ||
|
|
c18ca454c0 | ||
|
|
0fcf53bcb5 | ||
|
|
f9dddbc4d6 | ||
|
|
c7888c74f1 | ||
|
|
87c5ba81c4 | ||
|
|
a10e9fe4fe | ||
|
|
c93833769b | ||
|
|
c536deb54d | ||
|
|
3ff9f76a6c | ||
|
|
1c319beff4 | ||
|
|
f885c27e08 | ||
|
|
094bc1a698 | ||
|
|
9b0d7d6166 | ||
|
|
f52edf03e0 | ||
|
|
8ee01290ef | ||
|
|
9c89f4d638 | ||
|
|
8d35610c00 | ||
|
|
de124baf1a | ||
|
|
2676f41def | ||
|
|
22fd68c5b6 | ||
|
|
04c9a4b449 | ||
|
|
417021b36e | ||
|
|
be245114d5 | ||
|
|
184b135886 | ||
|
|
acc0e8666a | ||
|
|
5806aeab27 |
@@ -2,3 +2,5 @@
|
|||||||
remote = origin
|
remote = origin
|
||||||
branches = main
|
branches = main
|
||||||
initialized = true
|
initialized = true
|
||||||
|
version = 2
|
||||||
|
remote-type = gitea
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: Publish to PyPI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Publish-to-PyPI:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Configure git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "gitea-actions"
|
||||||
|
git config --global user.email "actions@git.jonathanteran.dev"
|
||||||
|
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||||
|
|
||||||
|
- name: "Set up Python"
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version-file: ".python-version"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v8.3.2
|
||||||
|
|
||||||
|
- name: Count commits
|
||||||
|
run: |
|
||||||
|
echo "Commit count: $(git rev-list --count main)"
|
||||||
|
echo "Tags available:"
|
||||||
|
git tag
|
||||||
|
echo "Remote tags available:"
|
||||||
|
git ls-remote --tags
|
||||||
|
|
||||||
|
- name: Bump version
|
||||||
|
run: |
|
||||||
|
uv run git-flow tag --token="${{ secrets.GITEA_TOKEN }}"
|
||||||
|
|
||||||
|
- name: Build and publish
|
||||||
|
run: |
|
||||||
|
uv build
|
||||||
|
uv publish --token="${{ secrets.PYPI_TOKEN }}"
|
||||||
+116
@@ -1,3 +1,119 @@
|
|||||||
|
## v0.1.0-main - 2026-07-29
|
||||||
|
|
||||||
|
- **feature**: show commit count and list tags on workflow [[jteran](mailto:jteran@renatre.org.ar)] (f05582b)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.0.1-main - 2026-07-18
|
||||||
|
|
||||||
|
- **bugfix**: add fetch tags to checkout step [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (f9fb5a6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.0-main - 2026-07-18
|
||||||
|
|
||||||
|
- **feature**: print current tag command output [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (0cdc012)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.0-main - 2026-07-18
|
||||||
|
|
||||||
|
- **style**: organize imports [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (a08666c)
|
||||||
|
- **style**: format and organize imports [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (726df21)
|
||||||
|
- **refactor**: use base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (07d4ff9)
|
||||||
|
- **refactor**: use base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (919e680)
|
||||||
|
- **refactor**: use base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (add4cd2)
|
||||||
|
- **style**: run black formatter on merge.py [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e4833c9)
|
||||||
|
- **style**: run black formatter on commit.py [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (7315681)
|
||||||
|
- **style**: run black formatter on new.py [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ba1c9bd)
|
||||||
|
- **style**: run black formatter [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1a7ae33)
|
||||||
|
- **refactor**: use new base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (0120faa)
|
||||||
|
- **refactor**: use unsafe_ask in favor of global error catch [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (febd18c)
|
||||||
|
- **style**: run black formatter [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (069c33b)
|
||||||
|
- **refactor**: reword current branch check, move if around so it's better placed [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1d186e1)
|
||||||
|
- **feature**: change unicode status icons [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (c0f7642)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v1.17.6 - 2026-07-09
|
||||||
|
|
||||||
|
- **bugfix**: actualizo versiones de checkout, setup-python, clonar depth completo [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (4666503)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.0-main - 2026-07-09
|
||||||
|
|
||||||
|
- **bugfix**: usar https por defecto como esquema si se detecta ssh [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (5989699)
|
||||||
|
- **feature**: usar esquema de remoto para host de api gitea [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ecfe058)
|
||||||
|
- **bugfix**: agregar setup previo a ejecucion de git tag [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1cc2de0)
|
||||||
|
- **bugfix**: arreglo acceso a secretos [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d456565)
|
||||||
|
- **bugfix**: agrego fetch-depth a checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ad8984c)
|
||||||
|
- **bugfix**: agrego log de git luego del checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (bf6a0e8)
|
||||||
|
- **bugfix**: cambio version de action setup-uv [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (cb74242)
|
||||||
|
- **feature**: agrego workflow para subir paquete a pypi [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e94ee2c)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.0-main - 2026-07-09
|
||||||
|
|
||||||
|
- **bugfix**: agregar setup previo a ejecucion de git tag [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1cc2de0)
|
||||||
|
- **bugfix**: arreglo acceso a secretos [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d456565)
|
||||||
|
- **bugfix**: agrego fetch-depth a checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ad8984c)
|
||||||
|
- **bugfix**: agrego log de git luego del checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (bf6a0e8)
|
||||||
|
- **bugfix**: cambio version de action setup-uv [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (cb74242)
|
||||||
|
- **feature**: agrego workflow para subir paquete a pypi [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e94ee2c)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v1.17.5 - 2026-06-03
|
||||||
|
|
||||||
|
- **bugfix**: allow commits not compliant with conventional commits in changelog generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (3ff9f76)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v1.17.4 - 2026-05-24
|
||||||
|
|
||||||
|
- **bugfix**: use rest of branch name instead of ticket for pr title generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (094bc1a)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v1.17.3 - 2026-05-22
|
||||||
|
|
||||||
|
- **bugfix**: dont close branches on merges between environments [[jteran](mailto:jteran@renatre.org.ar)] (8ee0129)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v1.17.2 - 2026-05-22
|
||||||
|
|
||||||
|
- **bugfix**: change pr title on merges between environments [[jteran](mailto:jteran@renatre.org.ar)] (de124ba)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v1.17.1 - 2026-05-20
|
||||||
|
|
||||||
|
- **bugfix**: dont push environment branches since they are already created on remote [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (04c9a4b)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v1.17.0 - 2026-05-20
|
||||||
|
|
||||||
|
- **feature**: allow merges between environments [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (5806aea)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
## v1.16.1 - 2026-05-20
|
## v1.16.1 - 2026-05-20
|
||||||
|
|
||||||
- **bugfix**: arreglo split de branch para armar correctamente el titulo del PR [[jteran](mailto:jteran@renatre.org.ar)] (a0d3028)
|
- **bugfix**: arreglo split de branch para armar correctamente el titulo del PR [[jteran](mailto:jteran@renatre.org.ar)] (a0d3028)
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ El comando solicitará 2 opciones:
|
|||||||
automáticamente. Por el momento, se soportan los siguientes remotos:
|
automáticamente. Por el momento, se soportan los siguientes remotos:
|
||||||
|
|
||||||
- [x] Bitbucket
|
- [x] Bitbucket
|
||||||
- [ ] Github: planeado
|
- [x] Github
|
||||||
|
- [x] Gitea
|
||||||
|
|
||||||
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
@@ -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"
|
"requests", "typer", "rich", "questionary"
|
||||||
]
|
]
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"},
|
{name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"},
|
||||||
|
|||||||
@@ -51,3 +51,5 @@ SUPPORTED_REMOTE_APIS = [
|
|||||||
REPOSITORY_TOKEN_FILENAME = ".repository-token"
|
REPOSITORY_TOKEN_FILENAME = ".repository-token"
|
||||||
|
|
||||||
FLOWCONFIG_FILENAME = ".flowconfig"
|
FLOWCONFIG_FILENAME = ".flowconfig"
|
||||||
|
|
||||||
|
FLOWCONFIG_VERSION = 2
|
||||||
|
|||||||
@@ -28,11 +28,15 @@ class Changelog:
|
|||||||
message = Git("show", commit, patch=False, format="%s").firstline()
|
message = Git("show", commit, patch=False, format="%s").firstline()
|
||||||
email = Git("show", commit, patch=False, format="%ae").firstline()
|
email = Git("show", commit, patch=False, format="%ae").firstline()
|
||||||
username = email[: email.index("@")]
|
username = email[: email.index("@")]
|
||||||
index = message.index(":")
|
description = message
|
||||||
commit_type = message[:index]
|
|
||||||
commit_message = message[index + 1 :]
|
|
||||||
|
|
||||||
content += f"- **{commit_type}**: {commit_message} [[{username}](mailto:{email})] ({commit})\n"
|
if ":" in message:
|
||||||
|
index = message.index(":")
|
||||||
|
commit_type = message[:index]
|
||||||
|
commit_message = message[index + 1 :]
|
||||||
|
description = f"**{commit_type}**: {commit_message}"
|
||||||
|
|
||||||
|
content += f"- {description} [[{username}](mailto:{email})] ({commit})\n"
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
+114
-20
@@ -2,22 +2,27 @@ from git_flow import (
|
|||||||
BRANCH_TYPES,
|
BRANCH_TYPES,
|
||||||
REPOSITORY_TOKEN_FILENAME,
|
REPOSITORY_TOKEN_FILENAME,
|
||||||
FLOWCONFIG_FILENAME,
|
FLOWCONFIG_FILENAME,
|
||||||
|
FLOWCONFIG_VERSION,
|
||||||
|
COMMIT_TYPES,
|
||||||
GitFlowError,
|
GitFlowError,
|
||||||
)
|
)
|
||||||
from git_flow.git import Git
|
from git_flow.git import Git
|
||||||
from git_flow.remote.base import RemoteAPI
|
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.io import *
|
from git_flow.io import *
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
|
|
||||||
TYPE_SUCCESS = 0
|
from typing import Optional
|
||||||
TYPE_WARNING = 1
|
import rich
|
||||||
TYPE_ERROR = 2
|
import rich.panel
|
||||||
TYPE_INFO = 3
|
import questionary
|
||||||
|
|
||||||
|
|
||||||
flowconfig = Git.get_config(FLOWCONFIG_FILENAME) if isfile(FLOWCONFIG_FILENAME) else {}
|
flowconfig = Git.get_config(FLOWCONFIG_FILENAME) if isfile(FLOWCONFIG_FILENAME) else {}
|
||||||
|
environments = (
|
||||||
|
flowconfig["flow.branches"].split(",") if "flow.branches" in flowconfig else []
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def ensure_initialized():
|
def ensure_initialized():
|
||||||
@@ -32,13 +37,21 @@ def ensure_initialized():
|
|||||||
f"El valor de `flow.initialized` ({initialized}) es inválido."
|
f"El valor de `flow.initialized` ({initialized}) es inválido."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
version = int(flowconfig.get("flow.version", "1"))
|
||||||
|
|
||||||
|
if version != FLOWCONFIG_VERSION:
|
||||||
|
raise GitFlowError(
|
||||||
|
f"La versión del flowconfig ({version}) no es compatible con esta versión de git-flow ({FLOWCONFIG_VERSION}). "
|
||||||
|
f"Debe reinicializar el repositorio."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def ensure_right_branch():
|
def ensure_right_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 confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"):
|
elif io_confirm(f"Rama actual: {branch}. ¿Es correcto?"):
|
||||||
return branch
|
return branch
|
||||||
else:
|
else:
|
||||||
raise GitFlowError("Ejecución cancelada.")
|
raise GitFlowError("Ejecución cancelada.")
|
||||||
@@ -46,7 +59,6 @@ def ensure_right_branch():
|
|||||||
|
|
||||||
def get_branch_env_and_type(branch: str) -> tuple[str, str]:
|
def get_branch_env_and_type(branch: str) -> tuple[str, str]:
|
||||||
components = branch.split("/")
|
components = branch.split("/")
|
||||||
target_branches = flowconfig["flow.branches"].split(",")
|
|
||||||
|
|
||||||
if len(components) == 2:
|
if len(components) == 2:
|
||||||
if components[0] not in BRANCH_TYPES:
|
if components[0] not in BRANCH_TYPES:
|
||||||
@@ -54,13 +66,13 @@ def get_branch_env_and_type(branch: str) -> tuple[str, str]:
|
|||||||
f"Branch inválida, el tipo '{components[0]}' no es válido."
|
f"Branch inválida, el tipo '{components[0]}' no es válido."
|
||||||
)
|
)
|
||||||
|
|
||||||
return (target_branches[0], components[0])
|
return (environments[0], components[0])
|
||||||
elif len(components) == 4:
|
elif len(components) == 4:
|
||||||
target_branches = target_branches[1:]
|
valid_environments = environments[1:]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
components[0] != "release"
|
components[0] != "release"
|
||||||
or components[1] not in target_branches
|
or components[1] not in valid_environments
|
||||||
or components[2] not in BRANCH_TYPES
|
or components[2] not in BRANCH_TYPES
|
||||||
):
|
):
|
||||||
raise GitFlowError(
|
raise GitFlowError(
|
||||||
@@ -93,26 +105,31 @@ def ensure_clean_worktree(has_remote: bool):
|
|||||||
|
|
||||||
if not_empty:
|
if not_empty:
|
||||||
if not has_remote:
|
if not has_remote:
|
||||||
warning("Existen cambios en tu entorno de trabajo sin commitear.")
|
io_warning("Existen cambios en tu entorno de trabajo sin commitear.")
|
||||||
|
|
||||||
|
if not io_confirm("¿Desea continuar?", False):
|
||||||
|
raise GitFlowError("Ejecución abortada")
|
||||||
else:
|
else:
|
||||||
raise GitFlowError("No se puede continuar con cambios pendientes.")
|
raise GitFlowError("No se puede continuar con cambios pendientes.")
|
||||||
|
|
||||||
if not confirm("¿Desea continuar?", False):
|
|
||||||
raise GitFlowError("Ejecución abortada")
|
|
||||||
|
|
||||||
|
|
||||||
def get_remote_api(token: str) -> RemoteAPI:
|
def get_remote_api(token: str) -> RemoteAPI:
|
||||||
if "flow.remote" not in flowconfig:
|
if "flow.remote" not in flowconfig:
|
||||||
raise GitFlowError("El repositorio no tiene configurado un remoto.")
|
raise GitFlowError("El repositorio no tiene configurado un remoto.")
|
||||||
|
|
||||||
[host, repository] = RemoteAPI.parse(flowconfig["flow.remote"])
|
remote_type = flowconfig.get("flow.remote-type", "").lower()
|
||||||
|
[schema, host, repository] = RemoteAPI.parse(flowconfig["flow.remote"])
|
||||||
|
|
||||||
if host == "bitbucket.org":
|
if remote_type == "gitea":
|
||||||
|
remote_schema = "https://" if schema == "ssh://" else schema
|
||||||
|
return GiteaRemoteAPI(remote_schema + host, repository, token)
|
||||||
|
elif remote_type == "bitbucket":
|
||||||
return BitbucketRemoteAPI(repository, token)
|
return BitbucketRemoteAPI(repository, token)
|
||||||
elif host == "github.com":
|
elif remote_type == "github":
|
||||||
return GithubRemoteAPI(repository, token)
|
return GithubRemoteAPI(repository, token)
|
||||||
else:
|
else:
|
||||||
raise GitFlowError("El host del repositorio remoto es inválido")
|
raise GitFlowError("El tipo del remoto es inválido")
|
||||||
|
|
||||||
|
|
||||||
def is_valid_ticket(ticket: str) -> bool:
|
def is_valid_ticket(ticket: str) -> bool:
|
||||||
"""Check if the specified string is a valid ticket number (ABC-123)"""
|
"""Check if the specified string is a valid ticket number (ABC-123)"""
|
||||||
@@ -120,8 +137,85 @@ def is_valid_ticket(ticket: str) -> bool:
|
|||||||
|
|
||||||
if len(components) != 2:
|
if len(components) != 2:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
ticket_project = components[0]
|
ticket_project = components[0]
|
||||||
ticket_number = components[1]
|
ticket_number = components[1]
|
||||||
|
|
||||||
return ticket_project.isalpha() and ticket_project.isupper() and ticket_number.isnumeric()
|
return (
|
||||||
|
ticket_project.isalpha()
|
||||||
|
and ticket_project.isupper()
|
||||||
|
and ticket_number.isnumeric()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_commit_message(commit_types: list[str] = COMMIT_TYPES) -> str:
|
||||||
|
commit_type = io_choice("Tipo de commit", commit_types)
|
||||||
|
commit_message = io_prompt(
|
||||||
|
"Mensaje de commit",
|
||||||
|
validator=lambda s: 0 < len(s) and len(s) < 100,
|
||||||
|
instruction="100 caracteres máximo",
|
||||||
|
)
|
||||||
|
return commit_type + ": " + commit_message
|
||||||
|
|
||||||
|
|
||||||
|
def io_error(message: str, title: Optional[str] = None):
|
||||||
|
_io_status(message, "✖ Error: ", "red", title)
|
||||||
|
|
||||||
|
|
||||||
|
def io_warning(message: str, title: Optional[str] = None):
|
||||||
|
_io_status(message, "⚠ Warning: ", "yellow", title)
|
||||||
|
|
||||||
|
|
||||||
|
def io_info(message: str, title: Optional[str] = None):
|
||||||
|
_io_status(message, "ℹ Info: ", "blue", title)
|
||||||
|
|
||||||
|
|
||||||
|
def io_success(message: str, title: Optional[str] = None):
|
||||||
|
_io_status(message, "✔ Success: ", "green", title)
|
||||||
|
|
||||||
|
|
||||||
|
def _io_status(message: str, prefix: str, style: str, title: Optional[str] = None):
|
||||||
|
if title:
|
||||||
|
rich.print(
|
||||||
|
rich.panel.Panel(
|
||||||
|
message,
|
||||||
|
title=prefix + title,
|
||||||
|
style=style,
|
||||||
|
expand=False,
|
||||||
|
title_align="left",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rich.print(f"[{style}]{prefix}{message}[/{style}]")
|
||||||
|
|
||||||
|
|
||||||
|
def io_confirm(question: str, default: bool = True) -> bool:
|
||||||
|
return questionary.confirm(question, default=default, auto_enter=False).unsafe_ask()
|
||||||
|
|
||||||
|
|
||||||
|
def io_choice(prompt: str, options: list[str]) -> str:
|
||||||
|
return questionary.select(
|
||||||
|
prompt,
|
||||||
|
options,
|
||||||
|
use_search_filter=True,
|
||||||
|
use_jk_keys=False,
|
||||||
|
instruction="Tipear o usar flechas",
|
||||||
|
).unsafe_ask()
|
||||||
|
|
||||||
|
|
||||||
|
def io_prompt(
|
||||||
|
message: str,
|
||||||
|
default: str = "",
|
||||||
|
persistent: bool = False,
|
||||||
|
strip: bool = False,
|
||||||
|
validator=None,
|
||||||
|
instruction: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
if persistent and not validator:
|
||||||
|
validator = lambda s: len(s.strip()) > 0
|
||||||
|
|
||||||
|
value = questionary.text(
|
||||||
|
message, default, validate=validator, instruction=instruction
|
||||||
|
).unsafe_ask()
|
||||||
|
|
||||||
|
return value.strip() if strip else value
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
import git_flow.command.base as base
|
||||||
from git_flow import (
|
from git_flow import (
|
||||||
TRASH_BRANCH_PREFIX,
|
TRASH_BRANCH_PREFIX,
|
||||||
WIP_BRANCH_PREFIX,
|
WIP_BRANCH_PREFIX,
|
||||||
GitFlowError,
|
GitFlowError,
|
||||||
)
|
)
|
||||||
from git_flow.command.base import *
|
|
||||||
from git_flow.git import Git
|
from git_flow.git import Git
|
||||||
from typing import Annotated
|
|
||||||
import typer
|
|
||||||
|
|
||||||
BRANCH_FORMAT = "%(refname:short)"
|
BRANCH_FORMAT = "%(refname:short)"
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
@@ -38,29 +42,28 @@ def branch(
|
|||||||
] = False,
|
] = False,
|
||||||
):
|
):
|
||||||
"""Lista ramas del repositorio, agrupandolas por entorno objetivo"""
|
"""Lista ramas del repositorio, agrupandolas por entorno objetivo"""
|
||||||
ensure_initialized()
|
base.ensure_initialized()
|
||||||
|
|
||||||
current_branch = Git.get_current_branch()
|
current_branch = Git.get_current_branch()
|
||||||
environments = flowconfig["flow.branches"].split(",")
|
|
||||||
|
|
||||||
if current_branch in environments:
|
if current_branch in base.environments:
|
||||||
current_environment = current_branch
|
current_environment = current_branch
|
||||||
else:
|
else:
|
||||||
current_environment, _ = get_branch_env_and_type(current_branch)
|
current_environment, _ = base.get_branch_env_and_type(current_branch)
|
||||||
|
|
||||||
if trash:
|
if trash:
|
||||||
show_branches(TRASH_BRANCH_PREFIX, current_branch)
|
show_branches(TRASH_BRANCH_PREFIX, current_branch)
|
||||||
elif wip:
|
elif wip:
|
||||||
show_branches(WIP_BRANCH_PREFIX, current_branch)
|
show_branches(WIP_BRANCH_PREFIX, current_branch)
|
||||||
elif all:
|
elif all:
|
||||||
show_envs(environments, current_branch)
|
show_envs(base.environments, current_branch)
|
||||||
show_all_branches(environments, current_branch)
|
show_all_branches(base.environments, current_branch)
|
||||||
elif environment is None:
|
elif environment is None:
|
||||||
show_envs(environments, current_branch)
|
show_envs(base.environments, current_branch)
|
||||||
show_env_branches(current_environment, environments, current_branch)
|
show_env_branches(current_environment, base.environments, current_branch)
|
||||||
elif environment in environments:
|
elif environment in base.environments:
|
||||||
show_envs(environments, current_branch)
|
show_envs(base.environments, current_branch)
|
||||||
show_env_branches(environment, environments, current_branch)
|
show_env_branches(environment, base.environments, current_branch)
|
||||||
else:
|
else:
|
||||||
raise GitFlowError(f"'{environment}' no es un entorno válido.")
|
raise GitFlowError(f"'{environment}' no es un entorno válido.")
|
||||||
|
|
||||||
@@ -79,7 +82,9 @@ def show_branches(branch_prefix: str, current_branch: str):
|
|||||||
|
|
||||||
|
|
||||||
def get_branches(branch_prefix: str):
|
def get_branches(branch_prefix: str):
|
||||||
return Git("for-each-ref", "refs/heads/" + branch_prefix, format=BRANCH_FORMAT).lines()
|
return Git(
|
||||||
|
"for-each-ref", "refs/heads/" + branch_prefix, format=BRANCH_FORMAT
|
||||||
|
).lines()
|
||||||
|
|
||||||
|
|
||||||
def show_all_branches(environments: list[str], current_branch: str):
|
def show_all_branches(environments: list[str], current_branch: str):
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import typer
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from git_flow import COMMIT_TYPES, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
import git_flow.command.base as base
|
||||||
|
from git_flow import TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
|
||||||
from git_flow.git import Git
|
from git_flow.git import Git
|
||||||
from git_flow.command.base import *
|
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
@@ -10,21 +12,18 @@ app = typer.Typer()
|
|||||||
@app.command()
|
@app.command()
|
||||||
def commit():
|
def commit():
|
||||||
"""Crea un commit siguiendo el formato de Conventional Commits"""
|
"""Crea un commit siguiendo el formato de Conventional Commits"""
|
||||||
ensure_initialized()
|
base.ensure_initialized()
|
||||||
branch = ensure_right_branch()
|
branch = base.ensure_right_branch()
|
||||||
|
|
||||||
if not _has_files_staged():
|
if not _has_files_staged():
|
||||||
warning("No hay cambios en el indice para commitear.")
|
base.io_warning("No hay cambios en el indice para commitear.")
|
||||||
if confirm("¿Desea agregar la carpeta actual?"):
|
if base.io_confirm("¿Desea agregar la carpeta actual?"):
|
||||||
Git("add", ".").exec(print="Agregando cambios")
|
Git("add", ".").exec(print="Agregando cambios")
|
||||||
else:
|
else:
|
||||||
raise GitFlowError("Debe agregar algún cambio al indice para continuar.")
|
raise GitFlowError("Debe agregar algún cambio al indice para continuar.")
|
||||||
|
|
||||||
commit_type = choice("Tipo de commit", COMMIT_TYPES)
|
message = base.get_commit_message()
|
||||||
commit_message = prompt(
|
commit_type = message[: message.index(":")]
|
||||||
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
|
|
||||||
)
|
|
||||||
message = commit_type + ": " + commit_message
|
|
||||||
|
|
||||||
if branch.startswith(WIP_BRANCH_PREFIX):
|
if branch.startswith(WIP_BRANCH_PREFIX):
|
||||||
original_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
|
original_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import typer
|
import typer
|
||||||
|
|
||||||
from git_flow import FLOWCONFIG_FILENAME, GitFlowError
|
import git_flow.command.base as base
|
||||||
|
from git_flow import FLOWCONFIG_FILENAME, FLOWCONFIG_VERSION, GitFlowError
|
||||||
from git_flow.git import Git
|
from git_flow.git import Git
|
||||||
from git_flow.command.base import *
|
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
@@ -15,15 +15,17 @@ def init():
|
|||||||
_ensure_not_already_initialized()
|
_ensure_not_already_initialized()
|
||||||
|
|
||||||
branches = _setup_flow_branches()
|
branches = _setup_flow_branches()
|
||||||
remote = _setup_flow_remote()
|
remote, remote_type = _setup_flow_remote()
|
||||||
|
|
||||||
flowconfig = {
|
flowconfig = {
|
||||||
|
"flow.version": str(FLOWCONFIG_VERSION),
|
||||||
"flow.initialized": "true",
|
"flow.initialized": "true",
|
||||||
"flow.branches": ",".join(branches),
|
"flow.branches": ",".join(branches),
|
||||||
}
|
}
|
||||||
|
|
||||||
if remote:
|
if remote and remote_type:
|
||||||
flowconfig["flow.remote"] = remote
|
flowconfig["flow.remote"] = remote
|
||||||
|
flowconfig["flow.remote-type"] = remote_type
|
||||||
|
|
||||||
Git.set_config(flowconfig, FLOWCONFIG_FILENAME)
|
Git.set_config(flowconfig, FLOWCONFIG_FILENAME)
|
||||||
Git("add", FLOWCONFIG_FILENAME).exec()
|
Git("add", FLOWCONFIG_FILENAME).exec()
|
||||||
@@ -34,32 +36,34 @@ def init():
|
|||||||
|
|
||||||
def _ensure_is_repository():
|
def _ensure_is_repository():
|
||||||
if not Git.is_repository():
|
if not Git.is_repository():
|
||||||
warning("El directorio actual no es un repositorio.")
|
base.io_warning("El directorio actual no es un repositorio.")
|
||||||
|
|
||||||
if not confirm("¿Desea inicializarlo?"):
|
if not base.io_confirm("¿Desea inicializarlo?"):
|
||||||
raise GitFlowError("No se puede continuar sin inicializar el repositorio")
|
raise GitFlowError("No se puede continuar sin inicializar el repositorio")
|
||||||
|
|
||||||
Git("init").exec()
|
Git("init").exec()
|
||||||
|
|
||||||
|
|
||||||
def _ensure_not_already_initialized():
|
def _ensure_not_already_initialized():
|
||||||
if not flowconfig:
|
if not base.flowconfig:
|
||||||
return
|
return
|
||||||
elif flowconfig.get("flow.initialized") == "true":
|
elif base.flowconfig.get("flow.initialized") == "true":
|
||||||
raise GitFlowError("El repositorio ya fue inicializado para usar git-flow")
|
raise GitFlowError("El repositorio ya fue inicializado para usar git-flow")
|
||||||
else:
|
else:
|
||||||
raise GitFlowError("Valor de 'flow.initialized' es inválido")
|
raise GitFlowError("Valor de 'flow.initialized' es inválido")
|
||||||
|
|
||||||
|
|
||||||
def _setup_flow_branches():
|
def _setup_flow_branches():
|
||||||
panel(
|
base.io_info(
|
||||||
"Configuración de entornos",
|
|
||||||
"""Ingrese las ramas que representan los entornos de deploy del proyecto
|
"""Ingrese las ramas que representan los entornos de deploy del proyecto
|
||||||
en orden creciente de cercanía al entorno productivo, y separados por
|
en orden creciente de cercanía al entorno productivo, y separados por
|
||||||
coma. Por ejemplo: "dev, test, prod".""",
|
coma. Por ejemplo: "dev, test, prod".""",
|
||||||
|
title="Configuración de entornos",
|
||||||
)
|
)
|
||||||
|
|
||||||
branches = list(map(lambda b: b.strip(), prompt("Ramas", "main").split(",")))
|
branches = list(
|
||||||
|
map(lambda b: b.strip(), base.io_prompt("Ramas", "main").split(","))
|
||||||
|
)
|
||||||
|
|
||||||
if not all(branches):
|
if not all(branches):
|
||||||
raise GitFlowError("No puede ingresar una rama vacia")
|
raise GitFlowError("No puede ingresar una rama vacia")
|
||||||
@@ -68,20 +72,23 @@ coma. Por ejemplo: "dev, test, prod".""",
|
|||||||
|
|
||||||
|
|
||||||
def _setup_flow_remote():
|
def _setup_flow_remote():
|
||||||
panel(
|
base.io_info(
|
||||||
"Configuración de remoto",
|
"""Configurar un repositorio remoto le permite generar PRs automáticamente para el
|
||||||
"Puede configurar un repositorio remoto para generar PRs automáticamente",
|
entorno correspondiente. Deberá seleccionar uno de los remotos actuales, o crear
|
||||||
|
uno nuevo.""",
|
||||||
|
title="Configuración de remoto",
|
||||||
)
|
)
|
||||||
|
|
||||||
remote = None
|
remote = None
|
||||||
|
remote_type = None
|
||||||
|
|
||||||
if confirm("¿Configurar repositorio remoto?"):
|
if base.io_confirm("¿Configurar repositorio remoto?"):
|
||||||
remotes = Git("remote").lines(print="Listando remotos disponibles")
|
remotes = Git("remote").lines(print="Listando remotos disponibles")
|
||||||
|
|
||||||
if not remotes:
|
if not remotes:
|
||||||
info("No tiene ningún repositorio remoto, se creará uno.")
|
base.io_info("No tiene ningún repositorio remoto, se creará uno.")
|
||||||
|
|
||||||
url = prompt(
|
url = base.io_prompt(
|
||||||
"Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)",
|
"Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)",
|
||||||
persistent=True,
|
persistent=True,
|
||||||
)
|
)
|
||||||
@@ -90,20 +97,24 @@ def _setup_flow_remote():
|
|||||||
elif len(remotes) == 1:
|
elif len(remotes) == 1:
|
||||||
remote = remotes[0]
|
remote = remotes[0]
|
||||||
else:
|
else:
|
||||||
info("Tiene más de un remoto, seleccione el que va a utilizar.")
|
base.io_info("Tiene más de un remoto, seleccione el que va a utilizar.")
|
||||||
remote = choice("Remoto", remotes)
|
remote = base.io_choice("Remoto", remotes)
|
||||||
|
|
||||||
return remote
|
remote_type = base.io_choice(
|
||||||
|
"Tipo de remoto:", ["bitbucket", "github", "gitea"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (remote, remote_type)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None):
|
def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None):
|
||||||
existing_branches = Git.get_branches()
|
existing_branches = Git.get_branches()
|
||||||
|
|
||||||
if not all(map(lambda b: b in existing_branches, branches)):
|
if not all(map(lambda b: b in existing_branches, branches)):
|
||||||
warning("Algunas de las ramas de entornos expecificadas no existen.")
|
base.io_warning("Algunas de las ramas de entornos expecificadas no existen.")
|
||||||
info("Debe indicar sobre que rama se crearán las ramas de entorno.")
|
base.io_info("Debe indicar sobre que rama se crearán las ramas de entorno.")
|
||||||
|
|
||||||
target_branch = choice("Rama", existing_branches)
|
target_branch = base.io_choice("Rama", existing_branches)
|
||||||
|
|
||||||
for branch in branches:
|
for branch in branches:
|
||||||
if branch not in existing_branches:
|
if branch not in existing_branches:
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import typer
|
import typer
|
||||||
|
|
||||||
|
import git_flow.command.base as base
|
||||||
from git_flow import GitFlowError
|
from git_flow import GitFlowError
|
||||||
from git_flow.changelog import Changelog
|
from git_flow.changelog import Changelog
|
||||||
from git_flow.command.base import *
|
|
||||||
from git_flow.git import Git
|
from git_flow.git import Git
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
@@ -10,21 +11,36 @@ app = typer.Typer()
|
|||||||
@app.command()
|
@app.command()
|
||||||
def merge():
|
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"""
|
||||||
ensure_initialized()
|
base.ensure_initialized()
|
||||||
|
|
||||||
branch = ensure_right_branch()
|
branch = base.ensure_right_branch()
|
||||||
target, _ = get_branch_env_and_type(branch)
|
|
||||||
|
|
||||||
if "flow.remote" in flowconfig:
|
if branch in base.environments:
|
||||||
remote = flowconfig["flow.remote"]
|
if branch == base.environments[-1]:
|
||||||
token = ensure_repository_token()
|
raise GitFlowError("No se puede hacer un merge del último entorno.")
|
||||||
|
|
||||||
|
base.io_info(
|
||||||
|
"""Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""",
|
||||||
|
"Merge de entornos",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not base.io_confirm("¿Desea continuar?"):
|
||||||
|
raise GitFlowError("Ejecución abortada")
|
||||||
|
|
||||||
|
target = base.environments[base.environments.index(branch) + 1]
|
||||||
|
else:
|
||||||
|
target, _ = base.get_branch_env_and_type(branch)
|
||||||
|
|
||||||
|
if "flow.remote" in base.flowconfig:
|
||||||
|
remote = base.flowconfig["flow.remote"]
|
||||||
|
token = base.ensure_repository_token()
|
||||||
run_remote(remote, token, branch, target)
|
run_remote(remote, token, branch, target)
|
||||||
else:
|
else:
|
||||||
run_local(branch, target)
|
run_local(branch, target)
|
||||||
|
|
||||||
|
|
||||||
def run_remote(remote: str, token: str, branch: str, target: str):
|
def run_remote(remote: str, token: str, branch: str, target: str):
|
||||||
ensure_clean_worktree(True)
|
base.ensure_clean_worktree(True)
|
||||||
|
|
||||||
Git("switch", target).exec(print="Cambiando a rama destino")
|
Git("switch", target).exec(print="Cambiando a rama destino")
|
||||||
Git("pull").exec(print="Obteniendo ultimos cambios del remoto")
|
Git("pull").exec(print="Obteniendo ultimos cambios del remoto")
|
||||||
@@ -33,20 +49,21 @@ def run_remote(remote: str, token: str, branch: str, target: str):
|
|||||||
check_merge_conflicts(target)
|
check_merge_conflicts(target)
|
||||||
show_commits_to_merge(target)
|
show_commits_to_merge(target)
|
||||||
|
|
||||||
if confirm(f"¿Crear PR de '{branch}' a '{target}'?"):
|
if base.io_confirm(f"¿Crear PR de '{branch}' a '{target}'?"):
|
||||||
Git("push", remote, branch, set_upstream=True).exec(
|
if branch not in base.environments:
|
||||||
print="Subiendo rama al remoto para crear PR"
|
Git("push", remote, branch, set_upstream=True).exec(
|
||||||
)
|
print="Subiendo rama al remoto para crear PR"
|
||||||
|
)
|
||||||
|
|
||||||
create_pull_request(token, branch, target)
|
create_pull_request(token, branch, target)
|
||||||
|
|
||||||
|
|
||||||
def run_local(branch: str, target: str):
|
def run_local(branch: str, target: str):
|
||||||
ensure_clean_worktree(False)
|
base.ensure_clean_worktree(False)
|
||||||
check_merge_conflicts(target)
|
check_merge_conflicts(target)
|
||||||
show_commits_to_merge(target)
|
show_commits_to_merge(target)
|
||||||
|
|
||||||
if confirm(f"¿Mergear rama '{branch}' a '{target}'?"):
|
if base.io_confirm(f"¿Mergear rama '{branch}' a '{target}'?"):
|
||||||
Git("switch", target).exec(print="Cambiando a rama destino")
|
Git("switch", target).exec(print="Cambiando a rama destino")
|
||||||
Git("merge", branch, ff=False).exec(print="Mergeando")
|
Git("merge", branch, ff=False).exec(print="Mergeando")
|
||||||
|
|
||||||
@@ -71,13 +88,15 @@ def check_merge_conflicts(target: str):
|
|||||||
Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False)
|
Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False)
|
||||||
|
|
||||||
if merge_conflicts:
|
if merge_conflicts:
|
||||||
error(f"La rama actual tiene conflictos con {target}")
|
base.io_error(
|
||||||
info(
|
f"La rama actual tiene conflictos con {target}.\n"
|
||||||
f"""Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente, y ejecutar nuevamente este comando"""
|
f"Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente,\n"
|
||||||
|
"y ejecutar nuevamente este comando",
|
||||||
|
title="Conflictos de merge",
|
||||||
)
|
)
|
||||||
raise GitFlowError("Ejecución abortada.")
|
raise GitFlowError("Ejecución abortada.")
|
||||||
else:
|
else:
|
||||||
success("No se detectaron merge conflicts.")
|
base.io_success("No se detectaron merge conflicts.")
|
||||||
|
|
||||||
|
|
||||||
def create_pull_request(token: str, branch: str, target: str):
|
def create_pull_request(token: str, branch: str, target: str):
|
||||||
@@ -85,26 +104,32 @@ def create_pull_request(token: str, branch: str, target: str):
|
|||||||
commits = Git("log", target + "..", format="%s").lines()
|
commits = Git("log", target + "..", format="%s").lines()
|
||||||
title = commits[0] if len(commits) == 1 else _get_pr_title_from_branch(branch)
|
title = commits[0] if len(commits) == 1 else _get_pr_title_from_branch(branch)
|
||||||
|
|
||||||
panel("Título del PR", title)
|
base.panel("Título del PR", title)
|
||||||
|
|
||||||
if confirm("¿Desea cambiar el título del PR?", False):
|
if base.io_confirm("¿Desea cambiar el título del PR?", False):
|
||||||
title = prompt("Título del PR")
|
title = base.io_prompt("Título del PR", persistent=True)
|
||||||
|
|
||||||
message = get_remote_api(token).create_pull_request(
|
message = base.get_remote_api(token).create_pull_request(
|
||||||
branch,
|
branch,
|
||||||
target,
|
target,
|
||||||
title,
|
title,
|
||||||
changelog.generate_content(target, branch),
|
changelog.generate_content(target, branch),
|
||||||
|
branch not in base.environments,
|
||||||
)
|
)
|
||||||
|
|
||||||
success(message)
|
base.io_success(message, "Rama creada")
|
||||||
|
|
||||||
if confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"):
|
if base.io_confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"):
|
||||||
Git("switch", target).exec(print="Cambiando a rama objetivo")
|
Git("switch", target).exec(print="Cambiando a rama objetivo")
|
||||||
Git("pull").exec(print="Obteniendo cambios")
|
Git("pull").exec(print="Obteniendo cambios")
|
||||||
|
|
||||||
|
|
||||||
def _get_pr_title_from_branch(branch: str) -> str:
|
def _get_pr_title_from_branch(branch: str) -> str:
|
||||||
components = branch.split("/") # <type>/<desc> or release/<env>/<type>/<desc>
|
if branch in base.environments:
|
||||||
|
target = base.environments[base.environments.index(branch) + 1]
|
||||||
|
return f"Sincronización de entorno {branch} a {target}"
|
||||||
|
|
||||||
|
components = branch.split("/") # <type>/<desc> or release/<env>/<type>/<desc>
|
||||||
branch_type = components[-2]
|
branch_type = components[-2]
|
||||||
branch_desc = components[-1]
|
branch_desc = components[-1]
|
||||||
|
|
||||||
@@ -115,13 +140,18 @@ def _get_pr_title_from_branch(branch: str) -> str:
|
|||||||
|
|
||||||
if first_dash >= 0 and second_dash >= 0:
|
if first_dash >= 0 and second_dash >= 0:
|
||||||
maybe_ticket = branch_desc[:second_dash]
|
maybe_ticket = branch_desc[:second_dash]
|
||||||
if is_valid_ticket(maybe_ticket):
|
if base.is_valid_ticket(maybe_ticket):
|
||||||
branch_ticket = maybe_ticket
|
branch_ticket = maybe_ticket
|
||||||
branch_desc = branch_desc[:second_dash+1]
|
branch_desc = branch_desc[second_dash + 1 :]
|
||||||
|
|
||||||
return " ".join(filter(None, [
|
return " ".join(
|
||||||
f"[Pasaje a {components[1]}]" if len(components) == 4 else None,
|
filter(
|
||||||
branch_type + ":",
|
None,
|
||||||
branch_ticket,
|
[
|
||||||
branch_desc.replace("-", " ")
|
f"[Pasaje a {components[1]}]" if len(components) == 4 else None,
|
||||||
]))
|
branch_type + ":",
|
||||||
|
branch_ticket,
|
||||||
|
branch_desc.replace("-", " "),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
+34
-30
@@ -1,9 +1,11 @@
|
|||||||
import typer
|
|
||||||
from git_flow import BRANCH_TYPES
|
|
||||||
from git_flow.command.base import *
|
|
||||||
from git_flow.git import Git
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
import git_flow.command.base as base
|
||||||
|
from git_flow import BRANCH_TYPES
|
||||||
|
from git_flow.git import Git
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
@@ -11,53 +13,55 @@ app = typer.Typer()
|
|||||||
def new():
|
def new():
|
||||||
"""Crea una nueva rama siguiendo Conventional Branches"""
|
"""Crea una nueva rama siguiendo Conventional Branches"""
|
||||||
|
|
||||||
ensure_initialized()
|
base.ensure_initialized()
|
||||||
branch = ensure_right_branch()
|
branch = base.ensure_right_branch()
|
||||||
envs = flowconfig["flow.branches"].split(",")
|
|
||||||
|
|
||||||
if branch not in envs:
|
if branch not in base.environments:
|
||||||
warning("La rama actual no corresponde a un entorno configurado.")
|
base.io_warning("La rama actual no corresponde a un entorno configurado.")
|
||||||
|
|
||||||
info("Se creará una nueva rama de trabajo sobre la rama actual.")
|
base.io_info(
|
||||||
info("Debe seleccionar el tipo de cambio a realizar.")
|
"Se creará una nueva rama de trabajo sobre la rama actual.\n"
|
||||||
|
"Debe seleccionar el tipo de cambio a realizar.",
|
||||||
|
title="Tipo de cambio",
|
||||||
|
)
|
||||||
|
|
||||||
branch_type = choice("Tipos de cambio", BRANCH_TYPES)
|
branch_type = base.io_choice("Tipos de cambio", BRANCH_TYPES)
|
||||||
ticket = _get_optional_ticket()
|
ticket = _get_optional_ticket()
|
||||||
new_branch = _get_unique_branch_name(branch_type, ticket)
|
new_branch = _get_unique_branch_name(branch_type, ticket)
|
||||||
|
|
||||||
if "flow.remote" in flowconfig and Git.get_tracking_branch(branch):
|
if "flow.remote" in base.flowconfig and Git.get_tracking_branch(branch):
|
||||||
Git("pull").exec(print="Actualizando rama actual")
|
Git("pull").exec(print="Actualizando rama actual")
|
||||||
|
|
||||||
if confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"):
|
if base.io_confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"):
|
||||||
Git("switch", new_branch, create=True).exec(print="Creando nueva rama")
|
Git("switch", new_branch, create=True).exec(print="Creando nueva rama")
|
||||||
|
|
||||||
def _get_optional_ticket() -> Optional[str]:
|
|
||||||
ticket = prompt("[Opcional] Ticket que respalda el cambio (en formato ABC-123)")
|
|
||||||
|
|
||||||
if not ticket:
|
def _get_optional_ticket() -> Optional[str]:
|
||||||
return None
|
return base.io_prompt(
|
||||||
if not is_valid_ticket(ticket):
|
"Ticket que respalda el cambio (en formato ABC-123)",
|
||||||
warning("Formato de ticket inválido, ignorando.")
|
validator=lambda x: len(x.strip()) == 0 or base.is_valid_ticket(x),
|
||||||
return None
|
)
|
||||||
|
|
||||||
return ticket
|
|
||||||
|
|
||||||
def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str:
|
def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str:
|
||||||
info(f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.")
|
base.io_info(
|
||||||
|
f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.\n"
|
||||||
|
"Las mismas se utilizaran para generar el nombre de la rama.",
|
||||||
|
title="Nombre de rama",
|
||||||
|
)
|
||||||
branch = None
|
branch = None
|
||||||
branches = Git.get_branches()
|
branches = Git.get_branches()
|
||||||
|
|
||||||
while branch is None:
|
while branch is None:
|
||||||
keywords = prompt(
|
keywords = base.io_prompt("Palabras clave", persistent=True)
|
||||||
"Palabras clave del cambio (por ejemplo: 'create worker form')",
|
|
||||||
persistent=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
keywords = filter(lambda k: len(k) > 0, keywords.split(" "))
|
keywords = filter(lambda k: len(k) > 0, keywords.split(" "))
|
||||||
branch = branch_type + "/" + (ticket + "-" if ticket else "") + "-".join(keywords)
|
branch = (
|
||||||
|
branch_type + "/" + (ticket + "-" if ticket else "") + "-".join(keywords)
|
||||||
|
)
|
||||||
|
|
||||||
if branch in branches:
|
if branch in branches:
|
||||||
error(
|
base.io_error(
|
||||||
f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra"
|
f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra"
|
||||||
)
|
)
|
||||||
branch = None
|
branch = None
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
from git_flow import COMMIT_TYPES, GitFlowError
|
|
||||||
from git_flow.command.base import *
|
|
||||||
from git_flow.git import Git
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
|
import git_flow.command.base as base
|
||||||
|
from git_flow import COMMIT_TYPES, GitFlowError
|
||||||
|
from git_flow.git import Git
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def release(group: Optional[str] = None):
|
def release(group: Optional[str] = None):
|
||||||
"""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"""
|
||||||
ensure_initialized()
|
base.ensure_initialized()
|
||||||
|
|
||||||
branch = ensure_right_branch()
|
branch = base.ensure_right_branch()
|
||||||
envs = flowconfig["flow.branches"].split(",")
|
env, _ = base.get_branch_env_and_type(branch)
|
||||||
env, _ = get_branch_env_and_type(branch)
|
|
||||||
|
|
||||||
if env == envs[-1]:
|
if env == base.environments[-1]:
|
||||||
raise GitFlowError(
|
raise GitFlowError(
|
||||||
"No se puede hacer release de una rama en el ultimo entorno."
|
"No se puede hacer release de una rama en el ultimo entorno."
|
||||||
)
|
)
|
||||||
|
|
||||||
has_remote = "flow.remote" in flowconfig
|
has_remote = "flow.remote" in base.flowconfig
|
||||||
|
|
||||||
ensure_clean_worktree(has_remote)
|
base.ensure_clean_worktree(has_remote)
|
||||||
|
|
||||||
next_env = envs[envs.index(env) + 1]
|
next_env = base.environments[base.environments.index(env) + 1]
|
||||||
next_branch = (
|
next_branch = (
|
||||||
branch.replace(f"/{env}/", f"/{next_env}/")
|
branch.replace(f"/{env}/", f"/{next_env}/")
|
||||||
if branch.startswith("release/")
|
if branch.startswith("release/")
|
||||||
else f"release/{next_env}/{branch}"
|
else f"release/{next_env}/{branch}"
|
||||||
)
|
)
|
||||||
|
|
||||||
base = Git.get_first_fork_point(branch, env)
|
fork_point = Git.get_first_fork_point(branch, env)
|
||||||
commits = Git("log", base + "..", format="%s").lines()
|
commits = Git("log", fork_point + "..", format="%s").lines()
|
||||||
|
|
||||||
if not commits:
|
if not commits:
|
||||||
raise GitFlowError("No hay cambios a mergear.")
|
raise GitFlowError("No hay cambios a mergear.")
|
||||||
@@ -46,9 +47,9 @@ def release(group: Optional[str] = None):
|
|||||||
grouping = group is not None
|
grouping = group is not None
|
||||||
|
|
||||||
if not group:
|
if not group:
|
||||||
if confirm("¿Desea agrupar este release con otra rama?", False):
|
if base.io_confirm("¿Desea agrupar este release con otra rama?", False):
|
||||||
grouping = True
|
grouping = True
|
||||||
group = choice(
|
group = base.io_choice(
|
||||||
"Grupo release: ", Git.get_branches("release/" + next_env + "/")
|
"Grupo release: ", Git.get_branches("release/" + next_env + "/")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -59,18 +60,13 @@ def release(group: Optional[str] = None):
|
|||||||
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 confirm(
|
if len(commits) > 1 and base.io_confirm(
|
||||||
f"¿Desea reemplazar los {len(commits)} commits de la rama por uno solo?", False
|
f"¿Desea reemplazar los {len(commits)} commits de la rama por uno solo?", False
|
||||||
):
|
):
|
||||||
info("Debe ingresar el mensaje del commit a crear.")
|
base.io_info("Debe ingresar el mensaje del commit a crear.")
|
||||||
|
message = base.get_commit_message(COMMIT_TYPES[:-1])
|
||||||
|
|
||||||
commit_type = choice("Tipo de commit", COMMIT_TYPES[:-1])
|
Git("switch", next_branch, fork_point, create=True).exec(
|
||||||
commit_message = prompt(
|
|
||||||
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
|
|
||||||
)
|
|
||||||
message = commit_type + ": " + commit_message
|
|
||||||
|
|
||||||
Git("switch", next_branch, base, create=True).exec(
|
|
||||||
print="Creando rama release en base"
|
print="Creando rama release en base"
|
||||||
)
|
)
|
||||||
Git("merge", branch, squash=True).exec(print="Squasheando commits en uno solo")
|
Git("merge", branch, squash=True).exec(print="Squasheando commits en uno solo")
|
||||||
@@ -78,7 +74,7 @@ def release(group: Optional[str] = None):
|
|||||||
else:
|
else:
|
||||||
Git("switch", next_branch, create=True).exec(print="Creando rama release")
|
Git("switch", next_branch, create=True).exec(print="Creando rama release")
|
||||||
|
|
||||||
status = Git("rebase", base, next_branch, onto=group).code(
|
status = Git("rebase", fork_point, next_branch, onto=group).code(
|
||||||
print="Moviendo cambios hacia el siguiente ambiente"
|
print="Moviendo cambios hacia el siguiente ambiente"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+13
-12
@@ -1,3 +1,8 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
import git_flow.command.base as base
|
||||||
from git_flow import (
|
from git_flow import (
|
||||||
COMMIT_TYPE_INCREMENT,
|
COMMIT_TYPE_INCREMENT,
|
||||||
SEMVER_MAJOR,
|
SEMVER_MAJOR,
|
||||||
@@ -7,10 +12,7 @@ from git_flow import (
|
|||||||
GitFlowError,
|
GitFlowError,
|
||||||
)
|
)
|
||||||
from git_flow.changelog import Changelog
|
from git_flow.changelog import Changelog
|
||||||
from git_flow.command.base import *
|
|
||||||
from git_flow.git import Git
|
from git_flow.git import Git
|
||||||
from typing import Optional
|
|
||||||
import typer
|
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
@@ -18,23 +20,22 @@ app = typer.Typer()
|
|||||||
@app.command()
|
@app.command()
|
||||||
def tag(token: Optional[str] = None):
|
def tag(token: Optional[str] = None):
|
||||||
"""Crea un nuevo tag para el último merge."""
|
"""Crea un nuevo tag para el último merge."""
|
||||||
ensure_initialized()
|
base.ensure_initialized()
|
||||||
|
|
||||||
envs = flowconfig["flow.branches"].split(",")
|
|
||||||
target = Git.get_current_branch()
|
target = Git.get_current_branch()
|
||||||
|
|
||||||
if target not in envs:
|
if target not in base.environments:
|
||||||
raise GitFlowError("Solo se pueden taggear commits en ramas principales.")
|
raise GitFlowError("Solo se pueden taggear commits en ramas principales.")
|
||||||
|
|
||||||
branch = Git(
|
branch = Git(
|
||||||
"show", get_last_merge_commit() + "^2", patch=False, format="%h"
|
"show", get_last_merge_commit() + "^2", patch=False, format="%h"
|
||||||
).firstline()
|
).firstline()
|
||||||
base = Git.get_first_fork_point(branch, target)
|
fork_point = Git.get_first_fork_point(branch, target)
|
||||||
commits = Git("log", base + ".." + branch, format="%s").lines()
|
commits = Git("log", fork_point + ".." + branch, format="%s").lines()
|
||||||
next_tag = get_next_tag_from_commits(commits, target)
|
next_tag = get_next_tag_from_commits(commits, target)
|
||||||
|
|
||||||
if not next_tag:
|
if not next_tag:
|
||||||
info(
|
base.io_info(
|
||||||
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
|
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -46,21 +47,21 @@ def tag(token: Optional[str] = None):
|
|||||||
|
|
||||||
changelog = Changelog()
|
changelog = Changelog()
|
||||||
|
|
||||||
changelog.update(next_tag, base, branch)
|
changelog.update(next_tag, fork_point, branch)
|
||||||
Git("add", Changelog.FILENAME).exec(print="Agregando nuevo CHANGELOG.md")
|
Git("add", Changelog.FILENAME).exec(print="Agregando nuevo CHANGELOG.md")
|
||||||
Git("commit", message=Changelog.COMMIT_MESSAGE).exec(print="Creando commit")
|
Git("commit", message=Changelog.COMMIT_MESSAGE).exec(print="Creando commit")
|
||||||
tag_commit = Git("show", "HEAD", patch=False, format="%h").firstline()
|
tag_commit = Git("show", "HEAD", patch=False, format="%h").firstline()
|
||||||
|
|
||||||
if ci:
|
if ci:
|
||||||
Git("push").exec(print="Subiendo commit al remoto para taggearlo")
|
Git("push").exec(print="Subiendo commit al remoto para taggearlo")
|
||||||
success(get_remote_api(token).create_tag(next_tag, tag_commit))
|
base.io_success(base.get_remote_api(token).create_tag(next_tag, tag_commit))
|
||||||
|
|
||||||
Git("tag", next_tag, tag_commit).exec(print="Creando tag localmente")
|
Git("tag", next_tag, tag_commit).exec(print="Creando tag localmente")
|
||||||
|
|
||||||
|
|
||||||
def get_token_and_ci(token: Optional[str]):
|
def get_token_and_ci(token: Optional[str]):
|
||||||
try:
|
try:
|
||||||
return (ensure_repository_token(), False)
|
return (base.ensure_repository_token(), False)
|
||||||
except GitFlowError:
|
except GitFlowError:
|
||||||
return (token, True)
|
return (token, True)
|
||||||
|
|
||||||
|
|||||||
+6
-16
@@ -53,7 +53,7 @@ class Git:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_current_tag():
|
def get_current_tag():
|
||||||
return Git("describe", abbrev="0", tags=True).firstline(check=False)
|
return Git("describe", abbrev="0", tags=True).firstline(check=False, print="Obteniendo tag actual")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_first_fork_point(branch: str, env: str):
|
def get_first_fork_point(branch: str, env: str):
|
||||||
@@ -117,7 +117,7 @@ class Git:
|
|||||||
|
|
||||||
return lines if not strip else list(map(lambda l: l.strip(), lines))
|
return lines if not strip else list(map(lambda l: l.strip(), lines))
|
||||||
|
|
||||||
def firstline(self, **kwargs: bool):
|
def firstline(self, **kwargs):
|
||||||
lines = self.lines(**kwargs)
|
lines = self.lines(**kwargs)
|
||||||
|
|
||||||
return lines[0] if lines else ""
|
return lines[0] if lines else ""
|
||||||
@@ -140,17 +140,7 @@ class Git:
|
|||||||
return process
|
return process
|
||||||
|
|
||||||
def _run(self, print: str | None = None, check: bool = True):
|
def _run(self, print: str | None = None, check: bool = True):
|
||||||
process = subprocess.run(
|
return self._get(print, check)
|
||||||
self.command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
||||||
)
|
|
||||||
|
|
||||||
if print is not None:
|
|
||||||
self._print_process("", "", title=print)
|
|
||||||
|
|
||||||
if check:
|
|
||||||
process.check_returncode()
|
|
||||||
|
|
||||||
return process
|
|
||||||
|
|
||||||
def _print_process(self, stdout: str, stderr: str, title: str):
|
def _print_process(self, stdout: str, stderr: str, title: str):
|
||||||
command = []
|
command = []
|
||||||
@@ -164,12 +154,12 @@ class Git:
|
|||||||
|
|
||||||
command.append(arg)
|
command.append(arg)
|
||||||
|
|
||||||
text = f"[yellow]$ {" ".join(command)}[/yellow]"
|
text = f"[green]$ {" ".join(command)}[/green]"
|
||||||
|
|
||||||
for line in stdout.splitlines():
|
for line in stdout.splitlines():
|
||||||
text += "\n\\[out] " + line
|
text += "\n" + line
|
||||||
|
|
||||||
for line in stderr.splitlines():
|
for line in stderr.splitlines():
|
||||||
text += "\n[red]\\[err][/red] " + line
|
text += "\n[red]" + line + "[/red]"
|
||||||
|
|
||||||
panel(title or "Ejecutando", text)
|
panel(title or "Ejecutando", text)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import typer
|
import typer
|
||||||
|
|
||||||
from git_flow import GitFlowError
|
from git_flow import GitFlowError
|
||||||
from git_flow.command.base import *
|
import git_flow.command.base as base
|
||||||
from git_flow.command import init, new, commit, merge, tag, release, branch
|
from git_flow.command import init, new, commit, merge, tag, release, branch
|
||||||
|
|
||||||
REPOSITORY_TOKEN_PATH = ".repository-token"
|
REPOSITORY_TOKEN_PATH = ".repository-token"
|
||||||
@@ -23,10 +23,10 @@ app.add_typer(branch.app)
|
|||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
app()
|
app()
|
||||||
except GitFlowError as e:
|
|
||||||
error(str(e))
|
|
||||||
except Exception as e:
|
|
||||||
error("Ocurrió un error inesperado: " + str(e))
|
|
||||||
except KeyboardInterrupt as e:
|
|
||||||
print()
|
print()
|
||||||
error("Ejecución abortada")
|
except GitFlowError as e:
|
||||||
|
base.io_error(str(e), title="Git Flow")
|
||||||
|
except Exception as e:
|
||||||
|
base.io_error(str(e), title="Inesperado")
|
||||||
|
except KeyboardInterrupt as e:
|
||||||
|
base.io_error("Ejecución abortada")
|
||||||
|
|||||||
+11
-28
@@ -12,42 +12,24 @@ class RemoteAPI(ABC):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def parse(remote: str):
|
def parse(remote: str):
|
||||||
url = Git("remote", "get-url", remote).firstline()
|
url = Git("remote", "get-url", remote).firstline()
|
||||||
invalid_url_error = GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
|
|
||||||
url_type = None
|
|
||||||
|
|
||||||
if url.startswith("git@"):
|
if url.startswith("git@"):
|
||||||
url_type = "ssh"
|
|
||||||
elif url.startswith("http://") or url.startswith("https://"):
|
|
||||||
url_type = "https"
|
|
||||||
else:
|
|
||||||
raise invalid_url_error
|
|
||||||
|
|
||||||
if url_type == "ssh":
|
|
||||||
# git@host:user/repo[.git]
|
|
||||||
at_pos = url.index("@")
|
at_pos = url.index("@")
|
||||||
colon_pos = url.index(":")
|
colon_pos = url.index(":")
|
||||||
|
schema = "ssh://"
|
||||||
host = url[at_pos+1:colon_pos]
|
host = url[at_pos+1:colon_pos]
|
||||||
repository = url[colon_pos+1:]
|
repository = url[colon_pos+1:]
|
||||||
else:
|
elif url.startswith("http://") or url.startswith("https://"):
|
||||||
# https://host/user/repo[.git]
|
start_pos = url.index("://") + 3
|
||||||
host_start = url.index("://") + 3
|
uri_start = url.index("/", start_pos)
|
||||||
uri_start = url.index("/", host_start)
|
schema = url[:start_pos]
|
||||||
host = url[host_start:uri_start]
|
host = url[start_pos:uri_start]
|
||||||
repository = url[uri_start+1:]
|
repository = url[uri_start+1:]
|
||||||
|
else:
|
||||||
|
raise GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
|
||||||
|
|
||||||
# Esto permite configurar hosts "imaginarios" para permitir el uso de multiples claves ssh
|
repository = repository.removesuffix(".git")
|
||||||
if host.endswith(".bitbucket.org"):
|
return [schema, host, repository]
|
||||||
host = "bitbucket.org"
|
|
||||||
elif host.endswith(".github.com"):
|
|
||||||
host = "github.com"
|
|
||||||
|
|
||||||
if host not in SUPPORTED_REMOTE_APIS:
|
|
||||||
raise GitFlowError("El host del repositorio remoto es inválido")
|
|
||||||
|
|
||||||
repository.removesuffix(".git")
|
|
||||||
|
|
||||||
return [host, repository]
|
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def create_pull_request(
|
def create_pull_request(
|
||||||
@@ -56,6 +38,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
|
||||||
) -> str:
|
) -> str:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class BitbucketRemoteAPI(RemoteAPI):
|
|||||||
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
|
||||||
):
|
):
|
||||||
request = requests.post(
|
request = requests.post(
|
||||||
self.get_endpoint("/pullrequests"),
|
self.get_endpoint("/pullrequests"),
|
||||||
@@ -21,7 +22,7 @@ class BitbucketRemoteAPI(RemoteAPI):
|
|||||||
"description": description,
|
"description": description,
|
||||||
"source": {"branch": {"name": source}},
|
"source": {"branch": {"name": source}},
|
||||||
"destination": {"branch": {"name": destination}},
|
"destination": {"branch": {"name": destination}},
|
||||||
"close_source_branch": True,
|
"close_source_branch": close_source_branch,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import requests
|
||||||
|
from git_flow import GitFlowError
|
||||||
|
from git_flow.remote.base import RemoteAPI
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaRemoteAPI(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("/pulls"),
|
||||||
|
headers=self.get_headers(),
|
||||||
|
json={
|
||||||
|
"title": title,
|
||||||
|
"body": description,
|
||||||
|
"head": source,
|
||||||
|
"base": destination,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
return "PR creado exitosamente: " + response.json()["html_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("/tags"),
|
||||||
|
headers=self.get_headers(),
|
||||||
|
json={"tag_name": tag, "target": 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:
|
||||||
|
return f"{self.base_url}/api/v1/repos/{self.repository}{resource}"
|
||||||
|
|
||||||
|
def get_headers(self):
|
||||||
|
return {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "token " + self.token,
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import requests
|
||||||
from git_flow import GitFlowError
|
from git_flow import GitFlowError
|
||||||
from git_flow.remote.base import RemoteAPI
|
from git_flow.remote.base import RemoteAPI
|
||||||
|
|
||||||
|
|
||||||
class GithubRemoteAPI(RemoteAPI):
|
class GithubRemoteAPI(RemoteAPI):
|
||||||
API_ENDPOINT = "https://github.com"
|
API_ENDPOINT = "https://api.github.com/repos"
|
||||||
|
|
||||||
def create_pull_request(
|
def create_pull_request(
|
||||||
self,
|
self,
|
||||||
@@ -11,11 +12,35 @@ class GithubRemoteAPI(RemoteAPI):
|
|||||||
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
|
||||||
) -> str:
|
) -> str:
|
||||||
raise GitFlowError("not implemented yet!")
|
response = requests.post(
|
||||||
|
self.get_endpoint("/pulls"),
|
||||||
|
headers=self.get_headers(),
|
||||||
|
json={
|
||||||
|
"title": title,
|
||||||
|
"body": description,
|
||||||
|
"head": source,
|
||||||
|
"base": destination,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
return "PR creado exitosamente: " + response.json()["html_url"]
|
||||||
|
else:
|
||||||
|
raise GitFlowError("Ocurrió un error al crear el PR: " + response.text)
|
||||||
|
|
||||||
def create_tag(self, tag: str, commit: str) -> str:
|
def create_tag(self, tag: str, commit: str) -> str:
|
||||||
raise GitFlowError("not implemented yet!")
|
response = requests.post(
|
||||||
|
self.get_endpoint("/git/refs"),
|
||||||
|
headers=self.get_headers(),
|
||||||
|
json={"ref": f"refs/tags/{tag}", "sha": 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:
|
def get_endpoint(self, resource: str) -> str:
|
||||||
return GithubRemoteAPI.API_ENDPOINT
|
return f"{GithubRemoteAPI.API_ENDPOINT}/{self.repository}{resource}"
|
||||||
|
|||||||
@@ -70,13 +70,17 @@ wheels = [
|
|||||||
name = "git-flow-envs"
|
name = "git-flow-envs"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "questionary" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
|
{ name = "rich" },
|
||||||
{ name = "typer" },
|
{ name = "typer" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "questionary" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
|
{ name = "rich" },
|
||||||
{ name = "typer" },
|
{ name = "typer" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -110,6 +114,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prompt-toolkit"
|
||||||
|
version = "3.0.52"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "wcwidth" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.20.0"
|
version = "2.20.0"
|
||||||
@@ -119,6 +135,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "questionary"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "prompt-toolkit" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.32.5"
|
||||||
@@ -179,3 +207,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6
|
|||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wcwidth"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user