Compare commits

..
1 Commits
26 changed files with 996 additions and 1226 deletions
-2
View File
@@ -2,5 +2,3 @@
remote = origin remote = origin
branches = main branches = main
initialized = true initialized = true
version = 2
remote-type = gitea
-37
View File
@@ -1,37 +0,0 @@
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@v7
with:
fetch-depth: 0
- 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: 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 }}"
-3
View File
@@ -9,6 +9,3 @@ dist/
build/ build/
**/__pycache__/ **/__pycache__/
**/*.egg-info/ **/*.egg-info/
# MacOS
.DS_Store
+1 -1
View File
@@ -1 +1 @@
3.14 3.10
+2 -98
View File
@@ -1,102 +1,6 @@
## v1.17.6 - 2026-07-09 ## v1.13.2 - 2026-02-11
- **bugfix**: actualizo versiones de checkout, setup-python, clonar depth completo [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (4666503) - **bugfix**: test pipeline change [[jteran](mailto:jteran@renatre.org.ar)] (f2eb5af)
---
## 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
- **bugfix**: arreglo split de branch para armar correctamente el titulo del PR [[jteran](mailto:jteran@renatre.org.ar)] (a0d3028)
---
## v1.16.0 - 2026-05-20
- **feature**: agrego soporte para especificar tickets en nombre e rama [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (f389118)
---
## v1.15.0 - 2026-05-17
- **feature**: include revlist count in version tag when skipping bump [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d275ac0)
---
## v1.14.0 - 2026-02-11
- **feature**: actualizar nombre de proyecto en uv.lock [[jteran](mailto:jteran@renatre.org.ar)] (9cd0b25)
--- ---
+1 -2
View File
@@ -47,8 +47,7 @@ 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
- [x] Github - [ ] Github: planeado
- [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" >
+3 -1
View File
@@ -1,4 +1,4 @@
image: python:3.14 image: python:3.10
pipelines: pipelines:
branches: branches:
@@ -10,5 +10,7 @@ pipelines:
script: script:
- pip install uv - pip install uv
- uv run git-flow tag --token=$BEARER - uv run git-flow tag --token=$BEARER
- git status
- git log --oneline -5
- uv build - uv build
- uv publish --token=$PYPI_TOKEN - uv publish --token=$PYPI_TOKEN
+2 -2
View File
@@ -6,9 +6,9 @@ build-backend = "setuptools.build_meta"
name = "git-flow-envs" name = "git-flow-envs"
description = "Git workflow automation to follow best practices when working with multiple environments" description = "Git workflow automation to follow best practices when working with multiple environments"
dynamic = ["version"] dynamic = ["version"]
requires-python = ">= 3.14" requires-python = ">= 3.10"
dependencies = [ dependencies = [
"requests", "typer", "rich", "questionary" "requests"
] ]
authors = [ authors = [
{name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"}, {name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"},
+11 -3
View File
@@ -50,6 +50,14 @@ SUPPORTED_REMOTE_APIS = [
REPOSITORY_TOKEN_FILENAME = ".repository-token" REPOSITORY_TOKEN_FILENAME = ".repository-token"
FLOWCONFIG_FILENAME = ".flowconfig" COLOR_BLACK = "\033[30m"
COLOR_RED = "\033[31m"
FLOWCONFIG_VERSION = 2 COLOR_GREEN = "\033[32m"
COLOR_YELLOW = "\033[33m"
COLOR_BLUE = "\033[34m"
COLOR_5 = "\033[35m"
COLOR_6 = "\033[36m"
COLOR_7 = "\033[37m"
COLOR_BLACK_BOLD = "\033[1;30m"
COLOR_WHITE_BOLD = "\033[1;37m"
COLOR_RESET = "\033[0m"
+4 -8
View File
@@ -28,15 +28,11 @@ 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("@")]
description = message index = message.index(":")
commit_type = message[:index]
commit_message = message[index + 1 :]
if ":" in message: content += f"- **{commit_type}**: {commit_message} [[{username}](mailto:{email})] ({commit})\n"
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
+200 -174
View File
@@ -1,210 +1,236 @@
from argparse import Namespace, ArgumentParser
from abc import ABC, abstractmethod
from os.path import isfile
from git_flow import ( from git_flow import (
BRANCH_TYPES, BRANCH_TYPES,
COLOR_BLUE,
COLOR_GREEN,
COLOR_RED,
COLOR_RESET,
COLOR_YELLOW,
REPOSITORY_TOKEN_FILENAME, REPOSITORY_TOKEN_FILENAME,
FLOWCONFIG_FILENAME,
FLOWCONFIG_VERSION,
GitFlowError, GitFlowError,
) )
from git_flow.git import Git from git_flow.git import FLOWCONFIG_FILE, 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 os.path import isfile
from typing import Optional TYPE_SUCCESS = 0
import rich TYPE_WARNING = 1
import rich.panel TYPE_ERROR = 2
import questionary TYPE_INFO = 3
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(): class Command(ABC):
initialized = flowconfig["flow.initialized"] if flowconfig else None flowconfig: dict[str, str]
if not initialized: @abstractmethod
raise GitFlowError( def name(self) -> str:
"El repositorio no fue inicializado, debe ejecutar el comando `init`." pass
)
elif initialized != "true": @abstractmethod
raise GitFlowError( def description(self) -> str:
f"El valor de `flow.initialized` ({initialized}) es inválido." pass
@abstractmethod
def run(self, args: Namespace = Namespace()):
pass
def init(self):
self.flowconfig = (
Git.get_config(FLOWCONFIG_FILE) if isfile(FLOWCONFIG_FILE) else {}
) )
version = int(flowconfig.get("flow.version", "1")) def success(self, msg: str):
self._print(TYPE_SUCCESS, msg)
if version != FLOWCONFIG_VERSION: def warning(self, msg: str):
raise GitFlowError( self._print(TYPE_WARNING, msg)
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 error(self, msg: str):
self._print(TYPE_ERROR, msg)
def ensure_right_branch(): def info(self, msg: str):
branch = Git.get_current_branch() self._print(TYPE_INFO, msg)
if branch == "HEAD": def prompt(
raise GitFlowError("No se encuentra parado sobre una rama.") self,
elif io_confirm(f"Rama actual: {branch}. ¿Es correcto?"): prompt: str,
return branch default: str | None = None,
else: persistent: bool = False,
raise GitFlowError("Ejecución cancelada.") strip: bool = True,
) -> str:
onetime = not persistent
while onetime or persistent:
if default:
prompt += f" [{default}]"
def get_branch_env_and_type(branch: str) -> tuple[str, str]: prompt += ": "
components = branch.split("/") result = input(prompt)
result = result.strip() if strip else result
if len(components) == 2: if result:
if components[0] not in BRANCH_TYPES: return result
elif default is not None:
return default
elif persistent:
self.error("Debe ingresar un valor no vacío.")
else:
return result
def confirm(self, question: str, default: bool = True):
suffix = " [Y/n]: " if default else " [y/N]: "
answer = input(question + suffix)
return default if len(answer) == 0 else answer.startswith("y")
def choice(self, prompt: str, options: list[str]):
print(prompt)
for i, option in enumerate(options):
print(f"\t{i+1}. {option}")
selection = None
while selection is None:
answer = input(f"Seleccione una opción [1-{len(options)}] o escribala: ")
if answer.isdigit():
answer = int(answer)
if 1 <= answer and answer <= len(options):
selection = options[answer - 1]
if not self.confirm(
f"Seleccionó la opción {answer} ({selection}), ¿es correcto?"
):
selection = None
else:
self.error(f"La opción {answer} está fuera del rango permitido.")
elif answer in options:
selection = answer
else:
self.error(f"La opción '{answer}' es inválida.")
return selection
def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
return parser
def ensure_initialized(self):
initialized = self.flowconfig["flow.initialized"] if self.flowconfig else None
if not initialized:
raise GitFlowError( raise GitFlowError(
f"Branch inválida, el tipo '{components[0]}' no es válido." "El repositorio no fue inicializado, debe ejecutar el comando `init`."
)
elif initialized != "true":
raise GitFlowError(
f"El valor de `flow.initialized` ({initialized}) es inválido."
) )
return (environments[0], components[0]) def ensure_right_branch(self):
elif len(components) == 4: branch = Git.get_current_branch()
valid_environments = environments[1:]
if ( if branch == "HEAD":
components[0] != "release" raise GitFlowError("No se encuentra parado sobre una rama.")
or components[1] not in valid_environments elif self.confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"):
or components[2] not in BRANCH_TYPES return branch
):
raise GitFlowError(
"Branch release inválido, debe tener el siguiente formato: "
"release/<env>/<type>/<name>, pero es: " + branch
)
return (components[1], components[2])
else:
raise GitFlowError("Branch inválido: " + branch)
def ensure_repository_token():
if not isfile(REPOSITORY_TOKEN_FILENAME):
raise GitFlowError(
"No existe un token para el repositorio en " + REPOSITORY_TOKEN_FILENAME
)
with open(REPOSITORY_TOKEN_FILENAME) as f:
return f.readline().strip()
def ensure_clean_worktree(has_remote: bool):
status = Git.status()
if not status:
return
not_empty = any(map(lambda s: s.index != " " or s.worktree != " ", status))
if not_empty:
if not has_remote:
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("Ejecución cancelada.")
def ensure_repository_token(self):
def get_remote_api(token: str) -> RemoteAPI: if not isfile(REPOSITORY_TOKEN_FILENAME):
if "flow.remote" not in flowconfig: raise GitFlowError(
raise GitFlowError("El repositorio no tiene configurado un remoto.") "No existe un token para el repositorio en " + REPOSITORY_TOKEN_FILENAME
remote_type = flowconfig.get("flow.remote-type", "").lower()
[schema, host, repository] = RemoteAPI.parse(flowconfig["flow.remote"])
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)
elif remote_type == "github":
return GithubRemoteAPI(repository, token)
else:
raise GitFlowError("El tipo del remoto es inválido")
def is_valid_ticket(ticket: str) -> bool:
"""Check if the specified string is a valid ticket number (ABC-123)"""
components = ticket.split("-")
if len(components) != 2:
return False
ticket_project = components[0]
ticket_number = components[1]
return (
ticket_project.isalpha()
and ticket_project.isupper()
and ticket_number.isnumeric()
)
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}]")
with open(REPOSITORY_TOKEN_FILENAME) as f:
return f.readline().strip()
def io_confirm(question: str, default: bool = True) -> bool: def ensure_clean_worktree(self, has_remote: bool):
return questionary.confirm(question, default=default, auto_enter=False).unsafe_ask() status = Git.status()
if not status:
return
def io_choice(prompt: str, options: list[str]) -> str: not_empty = any(map(lambda s: s.index != " " or s.worktree != " ", status))
return questionary.select(
prompt,
options,
use_search_filter=True,
use_jk_keys=False,
instruction="Tipear o usar flechas",
).unsafe_ask()
if not_empty:
if not has_remote:
self.warning("Existen cambios en tu entorno de trabajo sin commitear.")
else:
raise GitFlowError("No se puede continuar con cambios pendientes.")
def io_prompt( if not self.confirm("¿Desea continuar?", False):
message: str, raise GitFlowError("Ejecución abortada")
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( def get_branch_env_and_type(self, branch: str) -> tuple[str, str]:
message, default, validate=validator, instruction=instruction components = branch.split("/")
).unsafe_ask() target_branches = self.flowconfig["flow.branches"].split(",")
return value.strip() if strip else value if len(components) == 2:
if components[0] not in BRANCH_TYPES:
raise GitFlowError(
f"Branch inválida, el tipo '{components[0]}' no es válido."
)
return (target_branches[0], components[0])
elif len(components) == 4:
target_branches = target_branches[1:]
if (
components[0] != "release"
or components[1] not in target_branches
or components[2] not in BRANCH_TYPES
):
raise GitFlowError(
"Branch release inválido, debe tener el siguiente formato: "
"release/<env>/<type>/<name>, pero es: " + branch
)
return (components[1], components[2])
else:
raise GitFlowError("Branch inválido: " + branch)
def get_remote_api(self, token: str) -> RemoteAPI:
if "flow.remote" not in self.flowconfig:
raise GitFlowError("El repositorio no tiene configurado un remoto.")
[host, repository] = RemoteAPI.parse(self.flowconfig["flow.remote"])
if host == "bitbucket.org":
return BitbucketRemoteAPI(repository, token)
elif host == "github.com":
return GithubRemoteAPI(repository, token)
else:
raise GitFlowError("El host del repositorio remoto es inválido")
def _print(self, type: int, msg: str):
print(self._get_color(type) + self._get_tag(type) + " " + msg + COLOR_RESET)
def _get_color(self, type: int) -> str:
if type == TYPE_SUCCESS:
return COLOR_GREEN
elif type == TYPE_WARNING:
return COLOR_YELLOW
elif type == TYPE_ERROR:
return COLOR_RED
elif type == TYPE_INFO:
return COLOR_BLUE
return ""
def _get_tag(self, type: int) -> str:
if type == TYPE_SUCCESS:
return "[success]"
elif type == TYPE_WARNING:
return "[warning]"
elif type == TYPE_ERROR:
return "[error]"
elif type == TYPE_INFO:
return "[info]"
return ""
+101 -124
View File
@@ -1,140 +1,117 @@
from git_flow import ( from argparse import ArgumentParser, Namespace
TRASH_BRANCH_PREFIX, from git_flow import COLOR_WHITE_BOLD, COLOR_GREEN, COLOR_RED, COLOR_RESET, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
WIP_BRANCH_PREFIX, from git_flow.command.base import Command
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()
class BranchCommand(Command):
def name(self) -> str:
return "branch"
@app.command() def description(self) -> str:
def branch( return """Lista ramas del repositorio, agrupandolas por entorno objetivo"""
environment: Annotated[
str | None, def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
typer.Argument( parser.add_argument(
help="Entorno de las ramas a listar. Por defecto, es el entorno actual." "environment",
), nargs="?",
] = None, help="Entorno de las ramas a listar. Por defecto, es el entorno actual.",
trash: Annotated[ )
bool,
typer.Option( parser.add_argument(
"--trash",
action="store_true",
help=f"Listar ramas en la 'papelera de reciclaje' (ramas que empiezan con '{TRASH_BRANCH_PREFIX}')" help=f"Listar ramas en la 'papelera de reciclaje' (ramas que empiezan con '{TRASH_BRANCH_PREFIX}')"
), )
] = False,
wip: Annotated[ parser.add_argument(
bool, "--wip",
typer.Option( action="store_true",
help=f"Listar ramas 'en progreso' (ramas que empiezan con '{WIP_BRANCH_PREFIX}')" help=f"Listar ramas 'en progreso' (ramas que empiezan con '{WIP_BRANCH_PREFIX}')"
),
] = False,
all: Annotated[
bool, typer.Option(help="Listar ramas de todos los entornos")
] = False,
):
"""Lista ramas del repositorio, agrupandolas por entorno objetivo"""
ensure_initialized()
current_branch = Git.get_current_branch()
if current_branch in environments:
current_environment = current_branch
else:
current_environment, _ = get_branch_env_and_type(current_branch)
if trash:
show_branches(TRASH_BRANCH_PREFIX, current_branch)
elif wip:
show_branches(WIP_BRANCH_PREFIX, current_branch)
elif all:
show_envs(environments, current_branch)
show_all_branches(environments, current_branch)
elif environment is None:
show_envs(environments, current_branch)
show_env_branches(current_environment, environments, current_branch)
elif environment in environments:
show_envs(environments, current_branch)
show_env_branches(environment, environments, current_branch)
else:
raise GitFlowError(f"'{environment}' no es un entorno válido.")
def show_branches(branch_prefix: str, current_branch: str):
branches = get_branches(branch_prefix)
if not branches:
print("No hay ramas a mostrar.")
return
for branch in branches:
is_current = branch == current_branch
prefix = "*" if is_current else " "
console.print(prefix + " " + branch, style="green" if is_current else None)
def get_branches(branch_prefix: str):
return Git("for-each-ref", "refs/heads/" + branch_prefix, format=BRANCH_FORMAT).lines()
def show_all_branches(environments: list[str], current_branch: str):
for environment in environments:
show_env_branches(
environment, environments, current_branch, len(environments) > 1
) )
trash = len(get_branches(TRASH_BRANCH_PREFIX)) parser.add_argument(
"--all",
action="store_true",
help=f"Listar ramas de todos los entornos"
)
if trash: return parser
def run(self, args: Namespace = Namespace()):
self.ensure_initialized()
self.current_branch = Git.get_current_branch()
self.environments = self.flowconfig["flow.branches"].split(",")
if self.current_branch in self.environments:
current_environment = self.current_branch
else:
(current_environment, _) = self.get_branch_env_and_type(self.current_branch)
if args.trash:
self.show_branches(TRASH_BRANCH_PREFIX)
elif args.wip:
self.show_branches(WIP_BRANCH_PREFIX)
elif args.all:
self.show_envs()
self.show_all_branches()
elif args.environment is None:
self.show_envs()
self.show_env_branches(current_environment)
elif args.environment in self.environments:
self.show_envs()
self.show_env_branches(args.environment)
else:
raise GitFlowError(f"'{args.environment}' no es un entorno válido.")
def show_branches(self, prefix: str):
branches = self.get_branches(prefix)
if not branches:
print("No hay ramas a mostrar.")
return
for branch in branches:
prefix = (COLOR_RED + "*") if branch == self.current_branch else " "
print(prefix + " " + branch + COLOR_RESET)
def get_branches(self, prefix: str):
return Git("for-each-ref", "refs/heads/" + prefix, format=BRANCH_FORMAT).lines()
def show_all_branches(self):
for environment in self.environments:
self.show_env_branches(environment, len(self.environments) > 1)
trash = len(self.get_branches(TRASH_BRANCH_PREFIX))
if trash:
print()
print(COLOR_WHITE_BOLD + "Papelera: " + COLOR_RESET + str(trash) + " rama" + ("" if trash == 1 else "s"))
def show_envs(self):
print(COLOR_WHITE_BOLD + "Entornos" + COLOR_RESET)
for environment in self.environments:
prefix = (COLOR_GREEN + "*") if environment == self.current_branch else " "
print(prefix + " " + environment + COLOR_RESET)
print() print()
console.print(
"[bold]Papelera:[/bold] "
+ str(trash)
+ " rama"
+ ("" if trash == 1 else "s")
)
def show_env_branches(self, environment: str, show_env: bool = False):
is_first_env = environment == self.environments[0]
pattern = "refs/heads/" if is_first_env else "refs/heads/release/" + environment + "/"
pattern += "*/*"
exclude = ["refs/heads/" + TRASH_BRANCH_PREFIX, "refs/heads/release/"] if is_first_env else []
branches = Git("for-each-ref", pattern, exclude=exclude, format=BRANCH_FORMAT).lines()
def show_envs(environments: list[str], current_branch: str): print(COLOR_WHITE_BOLD + (environment if show_env else "Ramas") + COLOR_RESET)
console.print("Entornos", style="bold")
for environment in environments:
is_current = environment == current_branch
prefix = "*" if is_current else " "
console.print(prefix + " " + environment, style="green" if is_current else None)
print()
if not branches:
print("No hay ramas a mostrar.")
return
def show_env_branches( for branch in branches:
environment: str, prefix = (COLOR_GREEN + "*") if branch == self.current_branch else " "
environments: list[str], print(prefix + " " + branch + COLOR_RESET)
current_branch: str,
show_env: bool = False,
):
is_first_env = environment == environments[0]
pattern = (
"refs/heads/" if is_first_env else "refs/heads/release/" + environment + "/"
)
pattern += "*/*"
exclude = (
["refs/heads/" + TRASH_BRANCH_PREFIX, "refs/heads/release/"]
if is_first_env
else []
)
branches = Git(
"for-each-ref", pattern, exclude=exclude, format=BRANCH_FORMAT
).lines()
console.print(environment if show_env else "Ramas", style="bold")
if not branches:
print("No hay ramas a mostrar.")
return
for branch in branches:
is_current = branch == current_branch
prefix = "*" if is_current else " "
console.print(prefix + " " + branch, style="green" if is_current else None)
+49 -53
View File
@@ -1,66 +1,62 @@
import typer from argparse import Namespace
from datetime import datetime from datetime import datetime
from git_flow import COMMIT_TYPES, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError from git_flow import COMMIT_TYPES, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
from git_flow.command.base import Command
from git_flow.git import Git from git_flow.git import Git
import git_flow.command.base as base
app = typer.Typer()
@app.command() class CommitCommand(Command):
def commit(): def name(self) -> str:
"""Crea un commit siguiendo el formato de Conventional Commits""" return "commit"
base.ensure_initialized()
branch = base.ensure_right_branch()
if not _has_files_staged(): def description(self) -> str:
base.io_warning("No hay cambios en el indice para commitear.") return """Crea un commit siguiendo el formato de Conventional Commits"""
if base.io_confirm("¿Desea agregar la carpeta actual?"):
Git("add", ".").exec(print="Agregando cambios") def run(self, args: Namespace = Namespace()):
self.ensure_initialized()
branch = self.ensure_right_branch()
if not self._has_files_staged():
self.warning("No hay cambios en el indice para commitear.")
if self.confirm("¿Desea agregar la carpeta actual?"):
Git("add", ".").exec(print="Agregando cambios")
else:
raise GitFlowError(
"Debe agregar algún cambio al indice para continuar."
)
commit_type = self.choice("Tipo de commit", COMMIT_TYPES)
commit_message = self.prompt(
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
)
message = commit_type + ": " + commit_message
if branch.startswith(WIP_BRANCH_PREFIX):
original_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
Git("commit", m=message).exec(print="Creando commit en rama WIP")
if commit_type != "wip":
suffix = datetime.now().strftime("%Y%m%dT%H%M")
Git("switch", original_branch).exec(print="Volviendo a rama original")
Git("merge", branch, squash=True).exec(print="Squasheando commits WIP en uno solo")
Git("commit", m=message).exec(print="Creando commit final de rama WIP")
Git("branch", branch, TRASH_BRANCH_PREFIX + branch + "/" + suffix, move=True).exec(print="Backup de rama WIP")
else: else:
raise GitFlowError("Debe agregar algún cambio al indice para continuar.") if commit_type == "wip":
Git("switch", WIP_BRANCH_PREFIX + branch, create=True).exec(print="Creando nueva rama WIP")
commit_type = base.io_choice("Tipo de commit", COMMIT_TYPES) Git("commit", m=message).exec(print="Creando commit")
commit_message = base.io_prompt(
"Mensaje de commit",
validator=lambda s: 0 < len(s) and len(s) < 100,
instruction="100 caracteres máximo"
)
message = commit_type + ": " + commit_message
if branch.startswith(WIP_BRANCH_PREFIX): def _has_files_staged(self):
original_branch = branch.removeprefix(WIP_BRANCH_PREFIX) status = Git.status()
Git("commit", m=message).exec(print="Creando commit en rama WIP")
if commit_type != "wip": if not status:
suffix = datetime.now().strftime("%Y%m%dT%H%M") raise GitFlowError("No hay cambios para commitear.")
Git("switch", original_branch).exec(print="Volviendo a rama original")
Git("merge", branch, squash=True).exec(
print="Squasheando commits WIP en uno solo"
)
Git("commit", m=message).exec(print="Creando commit final de rama WIP")
Git(
"branch", branch, TRASH_BRANCH_PREFIX + branch + "/" + suffix, move=True
).exec(print="Backup de rama WIP")
else:
if commit_type == "wip":
Git("switch", WIP_BRANCH_PREFIX + branch, create=True).exec(
print="Creando nueva rama WIP"
)
Git("commit", m=message).exec(print="Creando commit") files_in_index = []
for s in status:
if s.worktree != "?" and s.worktree != " ":
files_in_index.append(s.file)
def _has_files_staged(): return len(files_in_index) > 0
status = Git.status()
if not status:
raise GitFlowError("No hay cambios para commitear.")
files_in_index = []
for s in status:
if s.worktree != "?" and s.worktree != " ":
files_in_index.append(s.file)
return len(files_in_index) > 0
+101 -108
View File
@@ -1,122 +1,115 @@
import typer from argparse import Namespace
from git_flow import GitFlowError
from git_flow import FLOWCONFIG_FILENAME, FLOWCONFIG_VERSION, GitFlowError from git_flow.command.base import Command
from git_flow.git import Git from git_flow.git import FLOWCONFIG_FILE, Git
from git_flow.command.base import *
app = typer.Typer()
@app.command() class InitCommand(Command):
def init(): def name(self) -> str:
"""Inicializa el repositorio para utilizar git-flow""" return "init"
_ensure_is_repository() def description(self) -> str:
_ensure_not_already_initialized() return """Inicializa el repositorio para utilizar git-flow"""
branches = _setup_flow_branches() def run(self, args: Namespace = Namespace()):
(remote, remote_type) = _setup_flow_remote() self._ensure_is_repository()
self._ensure_not_already_initialized()
flowconfig = { branches = self._setup_flow_branches()
"flow.version": str(FLOWCONFIG_VERSION), remote = self._setup_flow_remote()
"flow.initialized": "true",
"flow.branches": ",".join(branches),
}
if remote and remote_type: flowconfig = {
flowconfig["flow.remote"] = remote "flow.initialized": "true",
flowconfig["flow.remote-type"] = remote_type "flow.branches": ",".join(branches),
}
Git.set_config(flowconfig, FLOWCONFIG_FILENAME) if remote:
Git("add", FLOWCONFIG_FILENAME).exec() flowconfig["flow.remote"] = remote
Git("commit", message="feature: initialize git-flow").exec()
_ensure_all_flow_branches_exist(branches, remote) Git.set_config(flowconfig, FLOWCONFIG_FILE)
Git("add", FLOWCONFIG_FILE).exec()
Git("commit", message="feature: initialize git-flow").exec()
self._ensure_all_flow_branches_exist(branches, remote)
def _ensure_is_repository(): def _ensure_is_repository(self):
if not Git.is_repository(): if not Git.is_repository():
warning("El directorio actual no es un repositorio.") self.warning("El directorio actual no es un repositorio.")
if not confirm("¿Desea inicializarlo?"): if not self.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()
def _ensure_not_already_initialized():
if not flowconfig:
return
elif flowconfig.get("flow.initialized") == "true":
raise GitFlowError("El repositorio ya fue inicializado para usar git-flow")
else:
raise GitFlowError("Valor de 'flow.initialized' es inválido")
def _setup_flow_branches():
panel(
"Configuración de entornos",
"""Ingrese las ramas que representan los entornos de deploy del proyecto
en orden creciente de cercanía al entorno productivo, y separados por
coma. Por ejemplo: "dev, test, prod".""",
)
branches = list(map(lambda b: b.strip(), prompt("Ramas", "main").split(",")))
if not all(branches):
raise GitFlowError("No puede ingresar una rama vacia")
return branches
def _setup_flow_remote():
panel(
"Configuración de remoto",
"Puede configurar un repositorio remoto para generar PRs automáticamente",
)
remote = None
remote_type = None
if confirm("¿Configurar repositorio remoto?"):
remotes = Git("remote").lines(print="Listando remotos disponibles")
if not remotes:
info("No tiene ningún repositorio remoto, se creará uno.")
url = prompt(
"Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)",
persistent=True,
)
remote = "origin"
Git("remote", "add", remote, url).exec(print="Agregando remoto")
elif len(remotes) == 1:
remote = remotes[0]
else:
info("Tiene más de un remoto, seleccione el que va a utilizar.")
remote = choice("Remoto", remotes)
remote_type = choice("Tipo de remoto:", ["bitbucket", "github", "gitea"])
return (remote, remote_type)
def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None):
existing_branches = Git.get_branches()
if not all(map(lambda b: b in existing_branches, branches)):
warning("Algunas de las ramas de entornos expecificadas no existen.")
info("Debe indicar sobre que rama se crearán las ramas de entorno.")
target_branch = choice("Rama", existing_branches)
for branch in branches:
if branch not in existing_branches:
Git("branch", branch, target_branch).exec(
print="Creando rama inexistente"
) )
if remote: Git("init").exec()
Git("push", remote, branch, set_upstream=True).exec(
print="Creando rama en remoto" def _ensure_not_already_initialized(self):
if not self.flowconfig:
return
elif self.flowconfig.get("flow.initialized") == "true":
raise GitFlowError("El repositorio ya fue inicializado para usar git-flow")
else:
raise GitFlowError("Valor de 'flow.initialized' es inválido")
def _setup_flow_branches(self):
self.info(
"""Ingrese las ramas que representan los entornos de deploy del proyecto en
orden creciente de cercanía al entorno productivo, y separados por coma.
Por ejemplo: "dev,test,prod"."""
)
branches = list(
map(lambda b: b.strip(), self.prompt("Ramas", "main").split(","))
)
if not all(branches):
raise GitFlowError("No puede ingresar una rama vacia")
return branches
def _setup_flow_remote(self):
self.info(
"""Puede elegir o crear un repositorio remoto para generar automáticamente PRs"""
)
remote = None
if self.confirm("¿Configurar repositorio remoto?"):
remotes = Git("remote").lines(print="Listando remotos disponibles")
if not remotes:
self.info("No tiene ningún repositorio remoto, se creará uno.")
url = self.prompt(
"Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)",
persistent=True,
)
remote = "origin"
Git("remote", "add", remote, url).exec(print="Agregando remoto")
elif len(remotes) == 1:
remote = remotes[0]
else:
self.info("Tiene más de un remoto, seleccione el que va a utilizar.")
remote = self.choice("Remoto", remotes)
return remote
def _ensure_all_flow_branches_exist(self, branches: list[str], remote: str | None):
existing_branches = Git.get_branches()
if not all(map(lambda b: b in existing_branches, branches)):
self.warning("Algunas de las ramas de entornos expecificadas no existen.")
self.info("Debe indicar sobre que rama se crearán las ramas de entorno.")
target_branch = self.choice("Rama", existing_branches)
for branch in branches:
if branch not in existing_branches:
Git("branch", branch, target_branch).exec(
print="Creando rama inexistente"
) )
if remote:
Git("push", remote, branch, set_upstream=True).exec(
print="Creando rama en remoto"
)
+76 -111
View File
@@ -1,146 +1,111 @@
import typer from argparse import Namespace
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.changelog import Changelog from git_flow.changelog import Changelog
import git_flow.command.base as base from git_flow.command.base import Command
from git_flow.git import Git from git_flow.git import Git
app = typer.Typer()
class MergeCommand(Command):
def name(self) -> str:
return "merge"
@app.command() def description(self) -> str:
def merge(): return """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()
branch = base.ensure_right_branch() def run(self, args: Namespace = Namespace()):
self.ensure_initialized()
if branch in base.environments: branch = self.ensure_right_branch()
if branch == base.environments[-1]: (target, _) = self.get_branch_env_and_type(branch)
raise GitFlowError("No se puede hacer un merge del último entorno.")
base.io_info( if "flow.remote" in self.flowconfig:
"""Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""", remote = self.flowconfig["flow.remote"]
"Merge de entornos", token = self.ensure_repository_token()
) self.run_remote(remote, token, branch, target)
else:
self.run_local(branch, target)
if not base.io_confirm("¿Desea continuar?"): def run_remote(self, remote: str, token: str, branch: str, target: str):
raise GitFlowError("Ejecución abortada") self.ensure_clean_worktree(True)
target = base.environments[base.environments.index(branch) + 1] Git("switch", target).exec(print="Cambiando a rama destino")
else: Git("pull").exec(print="Obteniendo ultimos cambios del remoto")
target, _ = base.get_branch_env_and_type(branch) Git("switch", "-").exec(print="Volviendo a rama a mergear")
if "flow.remote" in base.flowconfig: self.check_merge_conflicts(target)
remote = base.flowconfig["flow.remote"] self.show_commits_to_merge(target)
token = base.ensure_repository_token()
run_remote(remote, token, branch, target)
else:
run_local(branch, target)
if self.confirm(f"¿Crear PR de '{branch}' a '{target}'?"):
def run_remote(remote: str, token: str, branch: str, target: str):
base.ensure_clean_worktree(True)
Git("switch", target).exec(print="Cambiando a rama destino")
Git("pull").exec(print="Obteniendo ultimos cambios del remoto")
Git("switch", "-").exec(print="Volviendo a rama a mergear")
check_merge_conflicts(target)
show_commits_to_merge(target)
if base.io_confirm(f"¿Crear PR de '{branch}' a '{target}'?"):
if branch not in base.environments:
Git("push", remote, branch, set_upstream=True).exec( Git("push", remote, branch, set_upstream=True).exec(
print="Subiendo rama al remoto para crear PR" print="Subiendo rama al remoto para crear PR"
) )
create_pull_request(token, branch, target) self.create_pull_request(token, branch, target)
def run_local(self, branch: str, target: str):
self.ensure_clean_worktree(False)
self.check_merge_conflicts(target)
self.show_commits_to_merge(target)
def run_local(branch: str, target: str): if self.confirm(f"¿Mergear rama '{branch}' a '{target}'?"):
base.ensure_clean_worktree(False) Git("switch", target).exec(print="Cambiando a rama destino")
check_merge_conflicts(target) Git("merge", branch, ff=False).exec(print="Mergeando")
show_commits_to_merge(target)
if base.io_confirm(f"¿Mergear rama '{branch}' a '{target}'?"): def show_commits_to_merge(self, target):
Git("switch", target).exec(print="Cambiando a rama destino") commits = Git("log", target + "..", format="%s").lines()
Git("merge", branch, ff=False).exec(print="Mergeando")
if not commits:
raise GitFlowError("No hay cambios a mergear.")
def show_commits_to_merge(target): print("Cambios a mergear:")
commits = Git("log", target + "..", format="%s").lines()
if not commits: for commit in commits:
raise GitFlowError("No hay cambios a mergear.") print("- " + commit)
print("Cambios a mergear:") def check_merge_conflicts(self, target: str):
merge_conflicts = Git("merge", target, ff=False, commit=False).code(
print="Realizando merge de prueba para verificar conflictos"
)
for commit in commits: Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False)
print("- " + commit)
if merge_conflicts:
self.error(f"La rama actual tiene conflictos con {target}")
self.info(
f"""Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente, y ejecutar nuevamente este comando"""
)
raise GitFlowError("Ejecución abortada.")
else:
self.success("No se detectaron merge conflicts.")
def check_merge_conflicts(target: str): def create_pull_request(self, token: str, branch: str, target: str):
merge_conflicts = Git("merge", target, ff=False, commit=False).code( changelog = Changelog()
print="Realizando merge de prueba para verificar conflictos" commits = Git("log", target + "..", format="%s").lines()
)
Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False) if len(commits) == 1:
title = commits[0]
else:
title = branch.replace("/", ": ").replace("-", " ")
if merge_conflicts: if title.startswith("release: "):
base.io_error(f"La rama actual tiene conflictos con {target}.\n" title = title[title.index(' ', title.index(' ') + 1) + 1:]
f"Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente, y ejecutar nuevamente este comando", "Conflictos de merge")
raise GitFlowError("Ejecución abortada.")
else:
base.io_success("No se detectaron merge conflicts.")
self.info("Título del PR por defecto: " + title)
def create_pull_request(token: str, branch: str, target: str): opt_title = self.prompt("[Opcional] Ingrese otro titulo para el PR")
changelog = Changelog()
commits = Git("log", target + "..", format="%s").lines()
title = commits[0] if len(commits) == 1 else _get_pr_title_from_branch(branch)
base.panel("Título del PR", title) if opt_title:
title = opt_title
if base.io_confirm("¿Desea cambiar el título del PR?", False): message = self.get_remote_api(token).create_pull_request(
title = base.io_prompt("Título del PR", persistent=True) branch,
target,
title,
changelog.generate_content(target, branch),
)
message = base.get_remote_api(token).create_pull_request( self.success(message)
branch,
target,
title,
changelog.generate_content(target, branch),
branch not in base.environments
)
base.io_success(message, "Rama creada") if self.confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"):
Git("switch", target).exec(print="Cambiando a rama objetivo")
if base.io_confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"): Git("pull").exec(print="Obteniendo 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:
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_desc = components[-1]
# detect if branch description has ticket as prefix: <desc> = ABC-123-my-branch-name
branch_ticket = None
first_dash = branch_desc.find("-")
second_dash = branch_desc.find("-", first_dash + 1)
if first_dash >= 0 and second_dash >= 0:
maybe_ticket = branch_desc[:second_dash]
if base.is_valid_ticket(maybe_ticket):
branch_ticket = maybe_ticket
branch_desc = branch_desc[second_dash+1:]
return " ".join(filter(None, [
f"[Pasaje a {components[1]}]" if len(components) == 4 else None,
branch_type + ":",
branch_ticket,
branch_desc.replace("-", " ")
]))
+41 -42
View File
@@ -1,57 +1,56 @@
import typer from argparse import Namespace
from git_flow import BRANCH_TYPES from git_flow import BRANCH_TYPES
import git_flow.command.base as base from git_flow.command.base import Command
from git_flow.git import Git from git_flow.git import Git
from typing import Optional
app = typer.Typer()
@app.command() class NewCommand(Command):
def new(): def name(self) -> str:
"""Crea una nueva rama siguiendo Conventional Branches""" return "new"
base.ensure_initialized() def description(self) -> str:
branch = base.ensure_right_branch() return """Crea una nueva rama siguiendo conventional branches"""
if branch not in base.environments: def run(self, args: Namespace = Namespace()):
base.io_warning("La rama actual no corresponde a un entorno configurado.") self.ensure_initialized()
branch = self.ensure_right_branch()
envs = self.flowconfig["flow.branches"].split(",")
base.io_info("Se creará una nueva rama de trabajo sobre la rama actual.\n" if branch not in envs:
"Debe seleccionar el tipo de cambio a realizar.", title="Tipo de cambio") self.warning("La rama actual no corresponde a un entorno configurado.")
branch_type = base.io_choice("Tipos de cambio", BRANCH_TYPES) self.info("Se creará una nueva rama de trabajo sobre la rama actual.")
ticket = _get_optional_ticket() self.info("Debe seleccionar el tipo de cambio a realizar.")
new_branch = _get_unique_branch_name(branch_type, ticket)
if "flow.remote" in base.flowconfig and Git.get_tracking_branch(branch): branch_type = self.choice("Tipos de cambio", BRANCH_TYPES)
Git("pull").exec(print="Actualizando rama actual") new_branch = self._get_unique_branch_name(branch_type)
if base.io_confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"): if "flow.remote" in self.flowconfig and Git.get_tracking_branch(branch):
Git("switch", new_branch, create=True).exec(print="Creando nueva rama") Git("pull").exec(print="Actualizando rama actual")
def _get_optional_ticket() -> Optional[str]: if self.confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"):
return base.io_prompt( Git("switch", new_branch, create=True).exec(print="Creando nueva rama")
"Ticket que respalda el cambio (en formato ABC-123)",
validator=lambda x: len(x.strip()) == 0 or base.is_valid_ticket(x)
)
def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str: def _get_unique_branch_name(self, branch_type: str) -> str:
base.io_info(f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.\n" self.info(
"Las mismas se utilizaran para generar el nombre de la rama.", title="Nombre de rama") f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'."
branch = None )
branches = Git.get_branches() branch = None
branches = Git.get_branches()
while branch is None: while branch is None:
keywords = base.io_prompt("Palabras clave", persistent=True) keywords = self.prompt(
"Palabras clave del cambio (por ejemplo: 'create worker form')",
keywords = filter(lambda k: len(k) > 0, keywords.split(" ")) persistent=True,
branch = branch_type + "/" + (ticket + "-" if ticket else "") + "-".join(keywords)
if branch in branches:
base.io_error(
f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra"
) )
branch = None
return branch keywords = filter(lambda k: len(k) > 0, keywords.split(" "))
branch = branch_type + "/" + "-".join(keywords)
if branch in branches:
self.error(
f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra"
)
branch = None
return branch
+89 -75
View File
@@ -1,91 +1,105 @@
from argparse import ArgumentParser, Namespace
from git_flow import COMMIT_TYPES, GitFlowError from git_flow import COMMIT_TYPES, GitFlowError
from git_flow.command.base import * from git_flow.command.base import Command
from git_flow.git import Git from git_flow.git import Git
from typing import Optional
import typer
app = typer.Typer()
@app.command() class ReleaseCommand(Command):
def release(group: Optional[str] = None): def name(self) -> str:
"""Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado""" return "release"
ensure_initialized()
branch = ensure_right_branch() def description(self) -> str:
env, _ = get_branch_env_and_type(branch) return """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
if env == environments[-1]: def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
raise GitFlowError( parser.add_argument(
"No se puede hacer release de una rama en el ultimo entorno." "--group",
nargs="?",
help="Agrupar release",
) )
has_remote = "flow.remote" in flowconfig return parser
ensure_clean_worktree(has_remote) def run(self, args: Namespace = Namespace()):
self.ensure_initialized()
next_env = environments[environments.index(env) + 1] branch = self.ensure_right_branch()
next_branch = ( envs = self.flowconfig["flow.branches"].split(",")
branch.replace(f"/{env}/", f"/{next_env}/") (env, _) = self.get_branch_env_and_type(branch)
if branch.startswith("release/")
else f"release/{next_env}/{branch}"
)
base = Git.get_first_fork_point(branch, env) if env == envs[-1]:
commits = Git("log", base + "..", format="%s").lines() raise GitFlowError(
"No se puede hacer release de una rama en el ultimo entorno."
if not commits:
raise GitFlowError("No hay cambios a mergear.")
print("Cambios a pasar al proximo entorno:")
for commit in commits:
print("- " + commit)
grouping = group is not None
if not group:
if confirm("¿Desea agrupar este release con otra rama?", False):
grouping = True
group = choice(
"Grupo release: ", Git.get_branches("release/" + next_env + "/")
) )
has_remote = "flow.remote" in self.flowconfig
self.ensure_clean_worktree(has_remote)
next_env = envs[envs.index(env) + 1]
next_branch = (
branch.replace(f"/{env}/", f"/{next_env}/")
if branch.startswith("release/")
else f"release/{next_env}/{branch}"
)
base = Git.get_first_fork_point(branch, env)
commits = Git("log", base + ".." , format="%s").lines()
if not commits:
raise GitFlowError("No hay cambios a mergear.")
print("Cambios a pasar al proximo entorno:")
for commit in commits:
print("- " + commit)
group = args.group
grouping = group is not None
if not grouping:
if self.confirm("¿Desea agrupar este release con otra rama?", False):
grouping = True
group = self.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("switch", "-").exec(print="Volviendo a la rama original")
if len(commits) > 1 and self.confirm(
f"¿Desea reemplazar los {len(commits)} commits de la rama por uno solo?",
False
):
self.info("Debe ingresar el mensaje del commit a crear.")
commit_type = self.choice("Tipo de commit", COMMIT_TYPES[:-1])
commit_message = self.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"
)
Git("merge", branch, squash=True).exec(
print="Squasheando commits en uno solo"
)
Git("commit", m=message).exec(print="Creando commit único")
else: else:
group = next_env Git("switch", next_branch, create=True).exec(print="Creando rama release")
if has_remote and Git.get_tracking_branch(group): status = Git("rebase", base, next_branch, onto=group).code(
Git("switch", group).exec(print="Cambiando a la rama objetivo") print="Moviendo cambios hacia el siguiente ambiente"
Git("pull").exec(print="Sincronizando cambios la rama objetivo")
Git("switch", "-").exec(print="Volviendo a la rama original")
if len(commits) > 1 and confirm(
f"¿Desea reemplazar los {len(commits)} commits de la rama por uno solo?", False
):
info("Debe ingresar el mensaje del commit a crear.")
commit_type = choice("Tipo de commit", COMMIT_TYPES[:-1])
commit_message = prompt(
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
) )
message = commit_type + ": " + commit_message
Git("switch", next_branch, base, create=True).exec( if status:
print="Creando rama release en base" Git("rebase", abort=True).exec(
) print="Deshaciendo cambios por conflictos"
Git("merge", branch, squash=True).exec(print="Squasheando commits en uno solo") )
Git("commit", m=message).exec(print="Creando commit único") raise GitFlowError("Error al realizar pasaje de cambios a rama objetivo")
else: elif grouping:
Git("switch", next_branch, create=True).exec(print="Creando rama release") Git("switch", group).exec(print="Cambiando a rama objetivo")
Git("merge", next_branch, ff_only=True).exec(print="Mergeando cambios de la nueva rama")
status = Git("rebase", base, next_branch, onto=group).code(
print="Moviendo cambios hacia el siguiente ambiente"
)
if status:
Git("rebase", abort=True).exec(print="Deshaciendo cambios por conflictos")
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"
)
+72 -74
View File
@@ -1,3 +1,4 @@
from argparse import ArgumentParser, Namespace
from git_flow import ( from git_flow import (
COMMIT_TYPE_INCREMENT, COMMIT_TYPE_INCREMENT,
SEMVER_MAJOR, SEMVER_MAJOR,
@@ -7,103 +8,100 @@ 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.command.base import Command
from git_flow.git import Git from git_flow.git import Git
from typing import Optional
import typer
app = typer.Typer()
@app.command() class TagCommand(Command):
def tag(token: Optional[str] = None): def name(self) -> str:
"""Crea un nuevo tag para el último merge.""" return "tag"
ensure_initialized()
target = Git.get_current_branch() def description(self) -> str:
return """Crea un nuevo tag para el último merge."""
if target not in environments: def run(self, args: Namespace = Namespace()):
raise GitFlowError("Solo se pueden taggear commits en ramas principales.") self.ensure_initialized()
branch = Git( envs = self.flowconfig["flow.branches"].split(",")
"show", get_last_merge_commit() + "^2", patch=False, format="%h" target = Git.get_current_branch()
).firstline()
base = Git.get_first_fork_point(branch, target)
commits = Git("log", base + ".." + branch, format="%s").lines()
next_tag = get_next_tag_from_commits(commits, target)
if not next_tag: if target not in envs:
info( raise GitFlowError("Solo se pueden taggear commits en ramas principales.")
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
)
return
token, ci = get_token_and_ci(token) branch = Git("show", self.get_last_merge_commit() + "^2", patch=False, format="%h").firstline()
base = Git.get_first_fork_point(branch, target)
commits = Git("log", base + ".." + branch, format="%s").lines()
next_tag = self.get_next_tag_from_commits(commits, target)
if not token: if not next_tag:
raise GitFlowError("No hay token disponible") self.info(
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
)
return
changelog = Changelog() (token, ci) = self.get_token_and_ci(args)
changelog = Changelog()
changelog.update(next_tag, base, branch) changelog.update(next_tag, base, 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)) self.success(self.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(self, args: Namespace):
try:
return (self.ensure_repository_token(), False)
except GitFlowError:
return (args.token, True)
def get_token_and_ci(token: Optional[str]): def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
try: parser.add_argument("--token")
return (ensure_repository_token(), False)
except GitFlowError:
return (token, True)
return parser
def get_last_merge_commit(): def get_last_merge_commit(self):
commit = Git( commit = Git(
"log", first_parent=True, merges=True, max_count=1, format="%h" "log", first_parent=True, merges=True, max_count=1, format="%h"
).firstline() ).firstline()
if not commit: if not commit:
raise GitFlowError("No hay merges a taggear.") raise GitFlowError("No hay merges a taggear.")
return commit return commit
def check_not_tagged(self, commit: str):
tag = Git("describe", commit, tags=True, exact_match=True).firstline(check=False)
def check_not_tagged(commit: str): if tag:
tag = Git("describe", commit, tags=True, exact_match=True).firstline(check=False) raise GitFlowError("El commit a taggear ya tiene tag: " + tag)
if tag: def get_next_tag_from_commits(self, commits: list[str], branch: str) -> str | None:
raise GitFlowError("El commit a taggear ya tiene tag: " + tag) increment = SEMVER_SKIP
for commit in commits:
commit_type = commit[: commit.index(":")]
increment = max(
increment, COMMIT_TYPE_INCREMENT.get(commit_type, SEMVER_SKIP)
)
tag = Git.get_current_tag() or f"v0.0.0-{branch}"
def get_next_tag_from_commits(commits: list[str], branch: str) -> str | None: until_dash = tag.index("-") if "-" in tag else None
increment = SEMVER_SKIP suffix = tag[until_dash:] if until_dash is not None else ""
for commit in commits: [major, minor, patch] = map(int, tag[1:until_dash].split("."))
commit_type = commit[: commit.index(":")]
increment = max(increment, COMMIT_TYPE_INCREMENT.get(commit_type, SEMVER_SKIP))
tag = Git.get_current_tag() or f"v0.0.0-{branch}" if increment == SEMVER_MAJOR:
major += 1
minor = 0
patch = 0
elif increment == SEMVER_MINOR:
minor += 1
patch = 0
elif increment == SEMVER_PATCH:
patch += 1
until_dash = tag.index("-") if "-" in tag else None return None if increment == SEMVER_SKIP else f"v{major}.{minor}.{patch}{suffix}"
suffix = tag[until_dash:] if until_dash is not None else ""
[major, minor, patch] = map(int, tag[1:until_dash].split("."))
if increment == SEMVER_MAJOR:
major += 1
minor = 0
patch = 0
elif increment == SEMVER_MINOR:
minor += 1
patch = 0
elif increment == SEMVER_PATCH:
patch += 1
elif increment == SEMVER_SKIP:
suffix = "-r" + Git("rev-list", "HEAD", count=True).firstline()
return None if increment == SEMVER_SKIP else f"v{major}.{minor}.{patch}{suffix}"
+27 -13
View File
@@ -1,9 +1,16 @@
import subprocess import subprocess
from git_flow import FLOWCONFIG_FILENAME from git_flow import GitFlowError
from git_flow.io import * from git_flow.io import *
FLOWCONFIG_FILE = ".flowconfig"
CHANGELOG_FILE = "CHANGELOG.md"
BUMP_VERSION = "chore: bump version and update CHANGELOG.md [skip ci]"
class Git: class Git:
class Status: class Status:
worktree: str worktree: str
@@ -37,7 +44,7 @@ class Git:
@staticmethod @staticmethod
def flow_config(*args, **kwargs): def flow_config(*args, **kwargs):
return Git("config", *args, file=FLOWCONFIG_FILENAME, **kwargs) return Git("config", *args, file=FLOWCONFIG_FILE, **kwargs)
@staticmethod @staticmethod
def get_current_branch(): def get_current_branch():
@@ -81,9 +88,7 @@ class Git:
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return None return None
def __init__( def __init__(self, subcommand: str, *args: str, **kwargs: str | int | bool | list[str] | None) -> None:
self, subcommand: str, *args: str, **kwargs: str | int | bool | list[str] | None
) -> None:
self.command = ["git", subcommand] self.command = ["git", subcommand]
for option, value in kwargs.items(): for option, value in kwargs.items():
@@ -128,7 +133,7 @@ class Git:
def exec(self, **kwargs): def exec(self, **kwargs):
self._run(**kwargs) self._run(**kwargs)
def _get(self, print: str | None = None, check: bool = True): def _get(self, print: str|None = None, check: bool = True):
process = subprocess.run(self.command, capture_output=True, text=True) process = subprocess.run(self.command, capture_output=True, text=True)
if print is not None: if print is not None:
@@ -139,8 +144,17 @@ class Git:
return process return process
def _run(self, print: str | None = None, check: bool = True):
return self._get(print, check) def _run(self, print: str|None = None, check: bool = True):
process = subprocess.run(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 = []
@@ -154,12 +168,12 @@ class Git:
command.append(arg) command.append(arg)
text = f"[green]$ {" ".join(command)}[/green]" title = title or "Ejecutando"
print(COLOR_YELLOW + "| " + title + COLOR_RESET)
print(COLOR_YELLOW + "| $ " + " ".join(command) + COLOR_RESET)
for line in stdout.splitlines(): for line in stdout.splitlines():
text += "\n" + line print(COLOR_YELLOW + "| " + COLOR_RESET + "[out] " + line)
for line in stderr.splitlines(): for line in stderr.splitlines():
text += "\n[red]" + line + "[/red]" print(COLOR_YELLOW + "| " + COLOR_RED + "[err] " + line + COLOR_RESET)
panel(title or "Ejecutando", text)
+42 -41
View File
@@ -1,55 +1,56 @@
from rich.console import Console import sys
from rich.prompt import Prompt, Confirm
from rich.panel import Panel
console = Console(highlight=False) from git_flow import COLOR_BLACK_BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_RED, COLOR_RESET, COLOR_YELLOW
def confirm(prompt, default: bool = True):
user_input = input(prompt + (" [Y/n]: " if default else " [y/N]: "))
return default if len(user_input) == 0 else user_input.startswith("y")
def success(msg: str): def choose(prompt: str, options: list[str]):
console.print(f":white_check_mark: [green]{msg}[/green]") print(prompt)
for i, option in enumerate(options):
print(f"\t{i+1}. {option}")
def warning(msg: str): selection = None
console.print(f":warning: [yellow]{msg}[/yellow]")
while selection is None:
user_input = input(f"Seleccione una opción [1-{len(options)}] o escribala: ")
def error(msg: str): if user_input.isdigit():
console.print(f":x: [red]{msg}[/red]") user_input = int(user_input)
if 1 <= user_input and user_input <= len(options):
selection = options[user_input - 1]
def info(msg: str): if not confirm(
console.print(f":information: [blue]{msg}[/blue]") f"Seleccionó la opción {user_input} ({selection}), ¿es correcto?"
):
selection = None
def panel(title: str, text: str): else:
console.print(Panel(text, title=title, expand=False, title_align="left")) print_error(f"La opción {user_input} está fuera del rango permitido.")
elif user_input in options:
selection = user_input
def prompt(
prompt: str,
default: str | None = None,
persistent: bool = False,
strip: bool = True,
) -> str:
onetime = not persistent
while onetime or persistent:
result = Prompt.ask(prompt)
result = result.strip() if strip else result
if result:
return result
elif default is not None:
return default
elif persistent:
error("Debe ingresar un valor no vacío.")
else: else:
return result print_error(f"La opción '{user_input}' es inválida.")
return selection
def confirm(question: str, default: bool = True): def print_error(message: str):
return Confirm.ask(question, default=default) print(f"{COLOR_RED}[err] {message}{COLOR_RESET}", file=sys.stderr)
def choice(prompt: str, options: list[str]): def print_warning(message: str):
return Prompt.ask(prompt, choices=options) print(f"{COLOR_YELLOW}[wrn] {message}{COLOR_RESET}")
def print_success(message: str):
print(f"{COLOR_GREEN}[ok!] {message}{COLOR_RESET}")
def print_info(message: str):
print(f"{COLOR_BLUE}[inf] {message}{COLOR_RESET}")
def print_debug(message: str):
print(f"{COLOR_BLACK_BOLD}[dbg] {message}{COLOR_RESET}")
+74 -16
View File
@@ -1,32 +1,90 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import typer import locale
from argparse import Namespace, ArgumentParser
from git_flow import GitFlowError from git_flow import GitFlowError
import git_flow.command.base as base from git_flow.command.base import Command
from git_flow.command import init, new, commit, merge, tag, release, branch from git_flow.command.branch import BranchCommand
from git_flow.command.commit import CommitCommand
from git_flow.command.init import InitCommand
from git_flow.command.merge import MergeCommand
from git_flow.command.new import NewCommand
from git_flow.command.release import ReleaseCommand
from git_flow.command.tag import TagCommand
from git_flow.io import *
REPOSITORY_TOKEN_PATH = ".repository-token" REPOSITORY_TOKEN_PATH = ".repository-token"
LOCALE = "es_AR.UTF-8" LOCALE = "es_AR.UTF-8"
app = typer.Typer() class GitFlowCommand(Command):
app.add_typer(init.app) commands: tuple[Command, ...]
app.add_typer(new.app)
app.add_typer(commit.app) def __init__(self, *args: Command) -> None:
app.add_typer(merge.app) self.commands = args
app.add_typer(tag.app)
app.add_typer(release.app) def name(self) -> str:
app.add_typer(branch.app) return "git-flow"
def description(self) -> str:
return """Git flow es una herramienta para automatizar un workflow siguiendo conventional commits, conventional branch y semver."""
def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
subparsers = parser.add_subparsers(
title="comandos", dest="command", required=True
)
for command in self.commands:
subparser = subparsers.add_parser(
command.name(), description=command.description()
)
command.setup_parser(subparser)
return parser
def get_parser(self) -> ArgumentParser:
parser = ArgumentParser(self.name(), description=self.description())
return self.setup_parser(parser)
def run(self, args: Namespace = Namespace()):
args = self.get_parser().parse_args()
for command in self.commands:
if command.name() == args.command:
command.init()
return command.run(args)
def main(): def main():
try: try:
app() locale.setlocale(locale.LC_ALL, LOCALE)
print() except locale.Error as e:
print_warning(f"No se pudo configurar el locale '{LOCALE}': {e}")
command = GitFlowCommand(
InitCommand(),
NewCommand(),
CommitCommand(),
MergeCommand(),
TagCommand(),
ReleaseCommand(),
BranchCommand()
)
try:
command.run()
except GitFlowError as e: except GitFlowError as e:
base.io_error(str(e), title="Git Flow") command.error(str(e))
except Exception as e: except Exception as e:
base.io_error(str(e), title="Inesperado") command.error("Ocurrió un error inesperado: " + str(e))
except KeyboardInterrupt as e: except KeyboardInterrupt as e:
base.io_error("Ejecución abortada") print()
command.error("Ejecución abortada")
return
if __name__ == "__main__":
main()
+28 -11
View File
@@ -12,24 +12,42 @@ 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:]
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: else:
raise GitFlowError(f"La URL del remoto {remote} es inválida: {url}") # https://host/user/repo[.git]
host_start = url.index("://") + 3
uri_start = url.index("/", host_start)
host = url[host_start:uri_start]
repository = url[uri_start+1:]
# Esto permite configurar hosts "imaginarios" para permitir el uso de multiples claves ssh
if host.endswith(".bitbucket.org"):
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]
repository = repository.removesuffix(".git")
return [schema, host, repository]
@abstractmethod @abstractmethod
def create_pull_request( def create_pull_request(
@@ -38,7 +56,6 @@ 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
+1 -2
View File
@@ -12,7 +12,6 @@ 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"),
@@ -22,7 +21,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": close_source_branch, "close_source_branch": True,
}, },
) )
-55
View File
@@ -1,55 +0,0 @@
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,
}
+4 -29
View File
@@ -1,10 +1,9 @@
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://api.github.com/repos" API_ENDPOINT = "https://github.com"
def create_pull_request( def create_pull_request(
self, self,
@@ -12,35 +11,11 @@ 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:
response = requests.post( raise GitFlowError("not implemented yet!")
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:
response = requests.post( raise GitFlowError("not implemented yet!")
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 f"{GithubRemoteAPI.API_ENDPOINT}/{self.repository}{resource}" return GithubRemoteAPI.API_ENDPOINT
Generated
+67 -141
View File
@@ -1,15 +1,6 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.14" requires-python = ">=3.10"
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
@@ -26,6 +17,70 @@ version = "3.4.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
{ url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
{ url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
{ url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
{ url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
{ url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
{ url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
{ url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
{ url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
{ url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
{ url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
{ url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
{ url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
{ url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
{ url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
@@ -46,43 +101,14 @@ wheels = [
] ]
[[package]] [[package]]
name = "click" name = "git-flow"
version = "8.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "git-flow-envs"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "questionary" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" },
{ name = "typer" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [{ name = "requests" }]
{ name = "questionary" },
{ name = "requests" },
{ name = "rich" },
{ name = "typer" },
]
[[package]] [[package]]
name = "idna" name = "idna"
@@ -93,60 +119,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
] ]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
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" },
]
[[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]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
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" },
]
[[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"
@@ -162,43 +134,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
] ]
[[package]]
name = "rich"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "typer"
version = "0.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.6.3" version = "2.6.3"
@@ -207,12 +142,3 @@ 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" },
]