Compare commits

..
165 Commits
Author SHA1 Message Date
jt a884c28094 Merge pull request 'refactor: use questionary package' (#1) from refactor/use-questionary-package into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 12s
Reviewed-on: #1
2026-07-14 03:32:09 +00:00
jt 0782cfb041 refactor: migrar comando merge para que utilice questionary 2026-07-14 00:28:20 -03:00
jt 320d40a211 refactor: migrar comando commit para que utilice questionary 2026-07-14 00:23:36 -03:00
jt a5a753f429 refactor: migrar comando new para que utilice metodos usando questionary 2026-07-14 00:22:22 -03:00
gitea-actions 4968667dad chore: bump version and update CHANGELOG.md [skip ci] 2026-07-09 21:15:32 +00:00
jt 79ae88b012 Merge pull request 'bugfix: actualizo versiones de checkout, setup-python, clonar depth completo' (#10) from bugfix/publish-pypi-workflow into main
Reviewed-on: #10
2026-07-09 21:15:00 +00:00
jt 4666503f32 bugfix: actualizo versiones de checkout, setup-python, clonar depth completo 2026-07-09 18:14:38 -03:00
gitea-actions 4c089eb8da chore: bump version and update CHANGELOG.md [skip ci] 2026-07-09 21:05:19 +00:00
jt dc858d026f Merge pull request 'feature: add publish pypi package workflow' (#9) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #9
2026-07-09 21:05:03 +00:00
jt 5989699f08 bugfix: usar https por defecto como esquema si se detecta ssh 2026-07-09 18:04:40 -03:00
jt ecfe058f8d feature: usar esquema de remoto para host de api gitea 2026-07-09 18:00:39 -03:00
gitea-actions ffe813bf06 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-09 01:21:44 +00:00
jt 6cd6771241 Merge pull request 'bugfix: agregar setup previo a ejecucion de git tag' (#8) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #8
2026-07-09 01:21:35 +00:00
jt 1cc2de0b75 bugfix: agregar setup previo a ejecucion de git tag 2026-07-08 22:21:17 -03:00
jt 0e669019f4 Merge pull request 'bugfix: arreglo acceso a secretos' (#7) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #7
2026-07-09 01:11:06 +00:00
jt d456565b2f bugfix: arreglo acceso a secretos 2026-07-08 22:10:35 -03:00
jt 74a78d30a1 Merge pull request 'bugfix: agrego fetch-depth a checkout' (#6) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #6
2026-07-09 01:08:20 +00:00
jt ad8984cb28 bugfix: agrego fetch-depth a checkout 2026-07-08 22:08:02 -03:00
jt 53b02f1f8f Merge pull request 'bugfix: agrego log de git luego del checkout' (#5) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #5
2026-07-09 01:06:41 +00:00
jt bf6a0e8443 bugfix: agrego log de git luego del checkout 2026-07-08 22:06:24 -03:00
jt 12351e64ba Merge pull request 'bugfix: cambio version de action setup-uv' (#4) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #4
2026-07-09 00:59:09 +00:00
jt cb742425c1 bugfix: cambio version de action setup-uv 2026-07-08 21:58:52 -03:00
jt bd47b40aff Merge pull request 'feature: agrego workflow para subir paquete a pypi' (#3) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #3
2026-07-09 00:55:53 +00:00
jt e94ee2ccc7 feature: agrego workflow para subir paquete a pypi 2026-07-08 21:55:16 -03:00
jt 069c62b697 Merge pull request 'feature: agrego implementacion de github remote api' (#2) from feature/add-github-remote-api into main
Reviewed-on: #2
2026-07-09 00:28:52 +00:00
jt c18ca454c0 feature: agrego implementacion de github remote api 2026-07-08 21:27:42 -03:00
jt 0fcf53bcb5 Merge pull request 'feature: add gitea remote api' (#1) from feature/add-gitea-remote-api into main
Reviewed-on: #1
2026-07-09 00:22:34 +00:00
jt f9dddbc4d6 bugfix: agregar schema a host de gitea api 2026-07-08 21:20:43 -03:00
jt c7888c74f1 bugfix: corregir headers de gitea 2026-07-08 21:19:24 -03:00
jt 87c5ba81c4 feature: agrego soporte para apis gitea 2026-07-08 21:12:06 -03:00
jt a10e9fe4fe Add .gitea/workflows/demo.yaml 2026-07-08 02:56:08 +00:00
bitbucket-pipelines c93833769b chore: bump version and update CHANGELOG.md [skip ci] 2026-06-03 00:40:26 +00:00
JonathanGitFlowandjt c536deb54d Merged in bugfix/allow-commit-messages-non-conventional (pull request #77)
bugfix: allow commits not compliant with conventional commits in changelog generation

Approved-by: Jonathan Teran
2026-06-03 00:39:43 +00:00
jt 3ff9f76a6c bugfix: allow commits not compliant with conventional commits in changelog generation 2026-06-02 21:38:18 -03:00
bitbucket-pipelines 1c319beff4 chore: bump version and update CHANGELOG.md [skip ci] 2026-05-24 05:42:13 +00:00
JonathanGitFlowandjt f885c27e08 Merged in bugfix/pr-title-with-ticket (pull request #76)
bugfix: use rest of branch name instead of ticket for pr title generation

Approved-by: Jonathan Teran
2026-05-24 05:41:05 +00:00
jt 094bc1a698 bugfix: use rest of branch name instead of ticket for pr title generation 2026-05-24 02:40:33 -03:00
bitbucket-pipelines 9b0d7d6166 chore: bump version and update CHANGELOG.md [skip ci] 2026-05-22 19:20:02 +00:00
JonathanGitFlowandjt f52edf03e0 Merged in bugfix/prevent-close-branch-on-environment-merges (pull request #75)
bugfix: dont close branches on merges between environments

Approved-by: Jonathan Teran
2026-05-22 19:19:30 +00:00
Jonathan Teran 8ee01290ef bugfix: dont close branches on merges between environments 2026-05-22 16:18:56 -03:00
bitbucket-pipelines 9c89f4d638 chore: bump version and update CHANGELOG.md [skip ci] 2026-05-22 19:14:59 +00:00
JonathanGitFlowandjt 8d35610c00 Merged in bugfix/customize-pr-title-on-environment-merges (pull request #74)
bugfix: change pr title on merges between environments

Approved-by: Jonathan Teran
2026-05-22 19:14:19 +00:00
Jonathan Teran de124baf1a bugfix: change pr title on merges between environments 2026-05-22 16:13:39 -03:00
bitbucket-pipelines 2676f41def chore: bump version and update CHANGELOG.md [skip ci] 2026-05-20 20:14:50 +00:00
JonathanGitFlowandjt 22fd68c5b6 Merged in bugfix/dont-push-environment-branches-on-merge (pull request #73)
bugfix: dont push environment branches since they are already created on remote

Approved-by: Jonathan Teran
2026-05-20 20:14:08 +00:00
jt 04c9a4b449 bugfix: dont push environment branches since they are already created on remote 2026-05-20 17:13:34 -03:00
JonathanGitFlowandjt 417021b36e Merged in refactor/unify-flow-environments-usage (pull request #72)
refactor: unify flow branches usage

Approved-by: Jonathan Teran
2026-05-20 20:12:26 +00:00
jt be245114d5 refactor: unify flow branches usage 2026-05-20 17:10:58 -03:00
bitbucket-pipelines 184b135886 chore: bump version and update CHANGELOG.md [skip ci] 2026-05-20 20:01:41 +00:00
JonathanGitFlowandjt acc0e8666a Merged in feature/allow-environment-merges (pull request #71)
feature: allow merges between environments

Approved-by: Jonathan Teran
2026-05-20 20:00:57 +00:00
jt 5806aeab27 feature: allow merges between environments 2026-05-20 17:00:20 -03:00
bitbucket-pipelines 23eb9df482 chore: bump version and update CHANGELOG.md [skip ci] 2026-05-20 12:20:37 +00:00
JonathanGitFlowandjt 538c9130dc Merged in bugfix/pr-title-getter (pull request #70)
bugfix: arreglo split de branch para armar correctamente el titulo del PR

Approved-by: Jonathan Teran
2026-05-20 12:19:58 +00:00
Jonathan Teran Carballo a0d3028904 bugfix: arreglo split de branch para armar correctamente el titulo del PR 2026-05-20 09:19:24 -03:00
bitbucket-pipelines 59b9e1c419 chore: bump version and update CHANGELOG.md [skip ci] 2026-05-20 02:30:52 +00:00
JonathanGitFlowandjt 8793248dbe Merged in feature/add-ticket-support (pull request #69)
feature: agrego soporte para especificar tickets en nombre e rama

Approved-by: Jonathan Teran
2026-05-20 02:30:18 +00:00
jt f389118515 feature: agrego soporte para especificar tickets en nombre e rama 2026-05-19 23:29:22 -03:00
JonathanGitFlowandjt 5a7ed03801 Merged in refactor/move-constants-to-git_flow (pull request #68)
refactor: move constants to git_flow module

Approved-by: Jonathan Teran
2026-05-17 14:48:46 +00:00
Jonathan Teran Carballo 5f7db6cf5d refactor: move constants to git_flow module 2026-05-17 11:48:02 -03:00
bitbucket-pipelines 8929438fbf chore: bump version and update CHANGELOG.md [skip ci] 2026-05-17 14:38:22 +00:00
JonathanGitFlowandjt d35dab9046 Merged in feature/include-revlist-count-in-version (pull request #67)
feature: include revlist count in version tag when skipping bump

Approved-by: Jonathan Teran
2026-05-17 14:37:38 +00:00
jt d275ac08f3 feature: include revlist count in version tag when skipping bump 2026-05-17 11:37:04 -03:00
JonathanGitFlowandjt 9b5584bbfe Merged in refactor/migrate-from-class-to-scripts (pull request #66)
refactor: migrate from class to scripts

Approved-by: Jonathan Teran
2026-05-17 14:28:48 +00:00
jt 9ccffd502d refactor: remove unused constants, move io functions to module 2026-05-17 11:27:17 -03:00
jt 0ac04aee84 refactor: migrate from class commands to scripts commands 2026-05-17 11:19:30 -03:00
JonathanGitFlowandjt 9ffefafad0 Merged in refactor/migrate-to-typer-framework (pull request #65)
refactor: replace global command class to typer functions

Approved-by: Jonathan Teran
2026-05-16 22:01:32 +00:00
jt fec30cd1e8 refactor: replace global command class to typer functions 2026-05-16 18:59:45 -03:00
bitbucket-pipelines e87571af8b chore: bump version and update CHANGELOG.md [skip ci] 2026-02-11 10:47:35 +00:00
JonathanGitFlowandjt e504887f2f Merged in feature/update-project-name-in-lock (pull request #64)
feature: update project name in lock

Approved-by: Jonathan Teran
2026-02-11 10:47:04 +00:00
Jonathan Teran Carballo 9cd0b2514b feature: actualizar nombre de proyecto en uv.lock 2026-02-11 07:46:38 -03:00
bitbucket-pipelines 7d246fd4a2 chore: bump version and update CHANGELOG.md [skip ci] 2026-02-11 10:41:14 +00:00
JonathanGitFlowandjt e9ef77d9fc Merged in bugfix/test-pipeline-change (pull request #63)
bugfix: test pipeline change

Approved-by: Jonathan Teran
2026-02-11 10:40:39 +00:00
Jonathan Teran Carballo f2eb5af5f2 bugfix: test pipeline change 2026-02-11 07:38:24 -03:00
bitbucket-pipelines 7c938f207f chore: bump version and update CHANGELOG.md [skip ci] 2026-02-11 00:06:32 +00:00
JonathanGitFlowandjt d571267966 Merged in feature/update-project-name (pull request #62)
feature: actualizo nombre y descripcion del proyecto

Approved-by: Jonathan Teran
2026-02-11 00:06:09 +00:00
jt 41d6957901 feature: actualizo nombre y descripcion del proyecto 2026-02-10 21:05:41 -03:00
bitbucket-pipelines 81a0578830 chore: bump version and update CHANGELOG.md [skip ci] 2026-02-11 00:02:13 +00:00
JonathanGitFlowandjt d035c02cf7 Merged in bugfix/tag-all-commits-locally (pull request #61)
bugfix: taggeo todos los commits localmente, incluso en ci

Approved-by: Jonathan Teran
2026-02-11 00:01:47 +00:00
jt 5d23bb6e8c bugfix: taggeo todos los commits localmente, incluso en ci 2026-02-10 21:00:18 -03:00
bitbucket-pipelines 04c1b47551 chore: bump version and update CHANGELOG.md [skip ci] 2026-02-10 23:57:03 +00:00
JonathanGitFlowandjt 5b91c44c41 Merged in bugfix/test-pipeline-change (pull request #60)
bugfix: test pipeline change

Approved-by: Jonathan Teran
2026-02-10 23:56:38 +00:00
jt 98bf7e7f9c bugfix: test pipeline change 2026-02-10 20:54:06 -03:00
JonathanGitFlowandjt c7d25203c7 Merged in chore/test-git-status-after-tag (pull request #59)
test: muestro git status y log despues de taggear el commit en pipeline

Approved-by: Jonathan Teran
2026-02-10 23:49:02 +00:00
jt 7ac8376247 test: muestro git status y log despues de taggear el commit en pipeline 2026-02-10 20:48:24 -03:00
bitbucket-pipelines 8b8b9337ac chore: bump version and update CHANGELOG.md [skip ci] 2026-02-10 23:39:27 +00:00
JonathanGitFlowandjt da8b27ce88 Merged in bugfix/add-build-step-to-pipeline (pull request #58)
bugfix: agrego paso de build al pipeline

Approved-by: Jonathan Teran
2026-02-10 23:38:32 +00:00
jt ade86f1350 bugfix: agrego paso de build al pipeline 2026-02-10 20:38:02 -03:00
bitbucket-pipelines 1eee08f60d chore: bump version and update CHANGELOG.md [skip ci] 2026-02-10 23:36:17 +00:00
JonathanGitFlowandjt f20f8c811c Merged in feature/use-uv-package-manager (pull request #57)
feature: cambio pipelines para usar uv package manager

Approved-by: Jonathan Teran
2026-02-10 23:35:35 +00:00
jt 0f700b026a feature: cambio pipelines para usar uv package manager 2026-02-10 20:34:04 -03:00
JonathanGitFlowandjt 9564339218 Merged in docs/create-access-token (pull request #56)
docs: arreglo pasos para crear Access Token

Approved-by: Jonathan Teran
2026-01-14 12:28:46 +00:00
Jonathan Teran Carballo dfd96e62bb docs: arreglo pasos para crear Access Token 2026-01-14 09:27:03 -03:00
bitbucket-pipelines 65c901e5b8 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-22 02:33:28 +00:00
JonathanGitFlowandjt 5759f67aa2 Merged in feature/2025-12-21 (pull request #55)
feature: 2025 12 21

Approved-by: Jonathan Teran
2025-12-22 02:32:42 +00:00
jt 726c73af39 feature: al agrupar release, mergear cambios en rama group 2025-12-21 23:30:46 -03:00
jt 057b32a2f9 feature: usar commit message cuando el pr solo tiene un commit 2025-12-21 23:10:21 -03:00
jt c0e2e596aa feature: eliminar prompt de ticket en creacion de rama 2025-12-21 22:32:54 -03:00
jt 77e338dcc8 feature: deshacer rebase cuando este falla en el pasaje de cambios 2025-12-21 22:31:16 -03:00
jt 1aa2263a38 bugfix: uso titulo personalizado de PR 2025-12-21 22:27:14 -03:00
bitbucket-pipelines 8c4037f702 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-15 02:03:36 +00:00
JonathanGitFlowandjt f3980d6c78 Merged in feature/merge-command-improvements (pull request #54)
feature: merge command improvements

Approved-by: Jonathan Teran
2025-12-15 02:03:11 +00:00
Jonathan Teran Carballo df8998a092 feature: agrego opcion para cambiar a rama objetivo y obtener cambios 2025-12-14 23:01:49 -03:00
Jonathan Teran Carballo c87b14121c feature: agrego opcion para personalizar nombre de PR 2025-12-14 22:53:26 -03:00
bitbucket-pipelines 7fb0ba0c39 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-15 01:40:00 +00:00
JonathanGitFlowandjt 5b6beb7c08 Merged in bugfix/list-branchs-on-environment (pull request #53)
bugfix: list branchs on environment

Approved-by: Jonathan Teran
2025-12-15 01:39:35 +00:00
Jonathan Teran Carballo c54cb95ed7 bugfix: uso rama actual cuando esta es un entorno 2025-12-14 22:38:25 -03:00
bitbucket-pipelines a2731e3400 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-15 01:11:57 +00:00
JonathanGitFlowandjt b20787d69d Merged in feature/branch-command-improvements (pull request #52)
feature: branch command improvements

Approved-by: Jonathan Teran
2025-12-15 01:11:32 +00:00
Jonathan Teran Carballo 5d33b29abd bugfix: uso env actual en vez de rama actual para listar ramas 2025-12-14 22:10:00 -03:00
Jonathan Teran Carballo 104b3214d4 feature: listar ramas de todos los entornos con flag --all, y del entorno actual por defecto 2025-12-14 22:00:14 -03:00
Jonathan Teran Carballo 774cf9ade6 feature: agrego opcion para listar ramas wip 2025-12-14 21:56:18 -03:00
Jonathan Teran Carballo 74c333aa1a refactor: cambio opcion para listar ramas trash 2025-12-14 21:48:44 -03:00
Jonathan Teran Carballo 499babcaec bugfix: elimino : extra de comando new 2025-12-14 21:40:54 -03:00
bitbucket-pipelines 379e0f3257 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-15 00:32:39 +00:00
JonathanGitFlowandjt 4a57dfd121 Merged in feature/new-command-improvements (pull request #51)
feature: new command improvements

Approved-by: Jonathan Teran
2025-12-15 00:32:13 +00:00
Jonathan Teran Carballo 01e938ebd9 feature: agrego opcion para incluir ticket en nombre de rama 2025-12-14 21:29:48 -03:00
Jonathan Teran Carballo 97b4a56ff7 feature: mostrar warning si la rama actual no es un entorno configurado 2025-12-14 13:46:09 -03:00
Jonathan Teran Carballo 24be50111d feature: hacer pull en rama actual antes de crear nueva rama 2025-12-14 13:43:23 -03:00
bitbucket-pipelines 897e9aa53b chore: bump version and update CHANGELOG.md [skip ci] 2025-12-14 16:39:20 +00:00
JonathanGitFlowandjt e4800d1054 Merged in bugfix/arreglos-pendientes (pull request #50)
bugfix: arreglos pendientes

Approved-by: Jonathan Teran
2025-12-14 16:38:55 +00:00
Jonathan Teran Carballo 0d95a978bf bugfix: verificar si la rama grupo trackea una rama remota antes de hacer pull 2025-12-14 13:32:23 -03:00
Jonathan Teran Carballo 0af5102fae bugfix: capturo interrupion por teclado para no mostrar stack trace 2025-12-14 13:25:31 -03:00
bitbucket-pipelines 2a21922a63 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-14 15:22:19 +00:00
JonathanGitFlowandjt 12b2bc9a39 Merged in bugfix/merge-pr-title-generation (pull request #49)
bugfix: merge pr title generation

Approved-by: Jonathan Teran
2025-12-14 15:21:54 +00:00
Jonathan Teran Carballo b24b22a178 bugfix: generación de PR de ramas release 2025-12-14 12:20:32 -03:00
bitbucket-pipelines c5f1be346c chore: bump version and update CHANGELOG.md [skip ci] 2025-12-14 15:05:04 +00:00
JonathanGitFlowandjt 6c32b3e751 Merged in bugfix/release-not-listing-branches (pull request #48)
bugfix: release not listing branches

Approved-by: Jonathan Teran
2025-12-14 15:04:39 +00:00
Jonathan Teran Carballo 5600e5ec83 bugfix: listado de ramas para elegir grupo release no devolvia resultados 2025-12-14 11:59:42 -03:00
bitbucket-pipelines 57142afa40 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-11 20:08:25 +00:00
JonathanGitFlowandjt 4375af35d9 Merged in bugfix/release-switch-to-group (pull request #47)
bugfix: release switch to group

Approved-by: Jonathan Teran
2025-12-11 20:07:56 +00:00
Jonathan Teran Carballo 44b8efd953 bugfix: al hacer un release, cambio a la rama grupo si esta disponible 2025-12-11 17:07:13 -03:00
bitbucket-pipelines f4b47000a6 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-11 18:18:23 +00:00
JonathanGitFlowandjt b12d2a1930 Merged in bugfix/first-fork-point-getter (pull request #46)
bugfix: first fork point getter

Approved-by: Jonathan Teran
2025-12-11 18:17:53 +00:00
Jonathan Teran Carballo 7b4fb34ef3 bugfix: cambio la forma en la que se obtiene el first fork point entre 2 ramas 2025-12-11 15:15:45 -03:00
bitbucket-pipelines 6a758cdb94 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-11 12:55:31 +00:00
JonathanGitFlowandjt b148a9f71d Merged in feature/show-commit-list-on-release (pull request #45)
feature: show commit list on release

Approved-by: Jonathan Teran
2025-12-11 12:55:01 +00:00
Jonathan Teran 2e65cb7a3e feature: muestro el listado de commits a pasar al siguiente entorno en release 2025-12-11 09:53:41 -03:00
bitbucket-pipelines 9c8df34a09 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-10 18:29:40 +00:00
JonathanGitFlowandjt 89f8bc58ba Merged in bugfix/commit-list-for-merge (pull request #44)
bugfix: commit list for merge

Approved-by: Jonathan Teran
2025-12-10 18:29:07 +00:00
Jonathan Teran 4e089dc4d6 bugfix: cambio la lista de commits a mostrar en el merge 2025-12-10 15:06:21 -03:00
Jonathan Teran c8e1a7cc88 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-04 10:19:54 -03:00
JonathanGitFlowandjt 65be7a01f0 Merged in bugfix/tag-name-scheme (pull request #43)
bugfix: tag name scheme

Approved-by: Jonathan Teran
2025-12-04 13:17:52 +00:00
Jonathan Teran 7ce6addf07 bugfix: revertir guion entre version y entorno, agregar guion a sufijo 2025-12-04 10:17:26 -03:00
bitbucket-pipelines 824b5f06de chore: bump version and update CHANGELOG.md [skip ci] 2025-12-04 13:16:27 +00:00
JonathanGitFlowandjt 9937f4b6bc Merged in bugfix/tag-name-scheme (pull request #42)
bugfix: tag name scheme

Approved-by: Jonathan Teran
2025-12-04 13:15:56 +00:00
Jonathan Teran 053b8b102b bugfix: agrego guion entre version y nombre de entorno 2025-12-04 10:15:32 -03:00
bitbucket-pipelines a2475195b2 chore: bump version and update CHANGELOG.md [skip ci] 2025-12-04 13:13:42 +00:00
JonathanGitFlowandjt 603519bf39 Merged in bugfix/create-changelog-when-missing (pull request #41)
bugfix: create changelog when missing

Approved-by: Jonathan Teran
2025-12-04 13:13:14 +00:00
Jonathan Teran 0c8e7bb37a bugfix: crear changelog cuando no existe 2025-12-04 10:11:10 -03:00
bitbucket-pipelines 6643271747 chore: bump version and update CHANGELOG.md [skip ci] 2025-11-29 16:41:43 +00:00
JonathanGitFlowandjt 68e11c66d1 Merged in bugfix/group-release-confirmation-and-commits-squash-count (pull request #40)
bugfix: group release confirmation and commits squash count

Approved-by: Jonathan Teran
2025-11-29 16:41:19 +00:00
jt f1f6e6724c bugfix: arreglo mensaje de confirmacion de squash, muevo prompt de commit message antes del squash 2025-11-29 13:40:34 -03:00
jt 83df7e9553 bugfix: cambio default en confirmacion para agrupar release 2025-11-29 13:35:25 -03:00
bitbucket-pipelines f5e683a1da chore: bump version and update CHANGELOG.md [skip ci] 2025-11-29 16:22:03 +00:00
JonathanGitFlowandjt fb9b8b57f6 Merged in feature/release-branch-groups (pull request #39)
feature: release branch groups

Approved-by: Jonathan Teran
2025-11-29 16:21:39 +00:00
jt c47afffa14 feature: agrego opcion para agrupar ramas release 2025-11-29 13:20:33 -03:00
bitbucket-pipelines 341e6239b2 chore: bump version and update CHANGELOG.md [skip ci] 2025-11-28 18:48:40 +00:00
JonathanGitFlowandjt 4fb00e786f Merged in bugfix/command-init-merge-errors (pull request #38)
bugfix: command init merge errors

Approved-by: Jonathan Teran
2025-11-28 18:48:15 +00:00
Jonathan Teran Carballo 2ad747111f bugfix: arreglo split de ramas de flow.branches 2025-11-28 15:44:05 -03:00
Jonathan Teran Carballo 5c3a3171fd bugfix: arreglo iteracion de items de flowconfig en set_config 2025-11-28 15:43:46 -03:00
Jonathan Teran Carballo 50979e2c3d docs: agrego venv a lista de carpetas ignoradas 2025-11-28 15:42:54 -03:00
bitbucket-pipelines a670b90899 chore: bump version and update CHANGELOG.md [skip ci] 2025-11-27 02:35:54 +00:00
JonathanGitFlowandjt 532a93e95e Merged in refactor/move-constants (pull request #37)
refactor: move constants

Approved-by: Jonathan Teran
2025-11-27 02:35:28 +00:00
jt 29757ccfb5 refactor: muevo constantes a modulo git_flow para centralizar uso 2025-11-26 23:35:00 -03:00
jt 7f857fdd68 bugfix: ignorar carpetas generadas en build de lsp 2025-11-26 23:34:39 -03:00
27 changed files with 1507 additions and 859 deletions
+2
View File
@@ -2,3 +2,5 @@
remote = origin remote = origin
branches = main branches = main
initialized = true initialized = true
version = 2
remote-type = gitea
+37
View File
@@ -0,0 +1,37 @@
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 }}"
+11 -3
View File
@@ -1,6 +1,14 @@
# Bitbucket token
.repository-token .repository-token
**/__pycache__/ # Virtual environment
build/ venv/
**/*.egg-info/
# Build folders and artifacts
dist/ dist/
build/
**/__pycache__/
**/*.egg-info/
# MacOS
.DS_Store
+1
View File
@@ -0,0 +1 @@
3.14
+303
View File
@@ -1,3 +1,306 @@
## v1.17.6 - 2026-07-09
- **bugfix**: actualizo versiones de checkout, setup-python, clonar depth completo [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (4666503)
---
## v0.1.0-main - 2026-07-09
- **bugfix**: usar https por defecto como esquema si se detecta ssh [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (5989699)
- **feature**: usar esquema de remoto para host de api gitea [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ecfe058)
- **bugfix**: agregar setup previo a ejecucion de git tag [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1cc2de0)
- **bugfix**: arreglo acceso a secretos [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d456565)
- **bugfix**: agrego fetch-depth a checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ad8984c)
- **bugfix**: agrego log de git luego del checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (bf6a0e8)
- **bugfix**: cambio version de action setup-uv [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (cb74242)
- **feature**: agrego workflow para subir paquete a pypi [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e94ee2c)
---
## v0.1.0-main - 2026-07-09
- **bugfix**: agregar setup previo a ejecucion de git tag [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1cc2de0)
- **bugfix**: arreglo acceso a secretos [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d456565)
- **bugfix**: agrego fetch-depth a checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ad8984c)
- **bugfix**: agrego log de git luego del checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (bf6a0e8)
- **bugfix**: cambio version de action setup-uv [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (cb74242)
- **feature**: agrego workflow para subir paquete a pypi [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e94ee2c)
---
## v1.17.5 - 2026-06-03
- **bugfix**: allow commits not compliant with conventional commits in changelog generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (3ff9f76)
---
## v1.17.4 - 2026-05-24
- **bugfix**: use rest of branch name instead of ticket for pr title generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (094bc1a)
---
## v1.17.3 - 2026-05-22
- **bugfix**: dont close branches on merges between environments [[jteran](mailto:jteran@renatre.org.ar)] (8ee0129)
---
## v1.17.2 - 2026-05-22
- **bugfix**: change pr title on merges between environments [[jteran](mailto:jteran@renatre.org.ar)] (de124ba)
---
## v1.17.1 - 2026-05-20
- **bugfix**: dont push environment branches since they are already created on remote [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (04c9a4b)
---
## v1.17.0 - 2026-05-20
- **feature**: allow merges between environments [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (5806aea)
---
## v1.16.1 - 2026-05-20
- **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)
---
## v1.13.1 - 2026-02-11
- **bugfix**: test pipeline change [[jteran](mailto:jteran@renatre.org.ar)] (f2eb5af)
---
## v1.13.0 - 2026-02-11
- **feature**: actualizo nombre y descripcion del proyecto [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (41d6957)
---
## v1.12.3 - 2026-02-11
- **bugfix**: taggeo todos los commits localmente, incluso en ci [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (5d23bb6)
---
## v1.12.2 - 2026-02-10
- **bugfix**: test pipeline change [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (98bf7e7)
---
## v1.12.1 - 2026-02-10
- **bugfix**: agrego paso de build al pipeline [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ade86f1)
---
## v1.12.0 - 2026-02-10
- **feature**: cambio pipelines para usar uv package manager [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (0f700b0)
---
## v1.11.0 - 2025-12-22
- **feature**: al agrupar release, mergear cambios en rama group [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (726c73a)
- **feature**: usar commit message cuando el pr solo tiene un commit [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (057b32a)
- **feature**: eliminar prompt de ticket en creacion de rama [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (c0e2e59)
- **feature**: deshacer rebase cuando este falla en el pasaje de cambios [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (77e338d)
- **bugfix**: uso titulo personalizado de PR [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1aa2263)
---
## v1.10.0 - 2025-12-15
- **feature**: agrego opcion para cambiar a rama objetivo y obtener cambios [[jteran](mailto:jteran@renatre.org.ar)] (df8998a)
- **feature**: agrego opcion para personalizar nombre de PR [[jteran](mailto:jteran@renatre.org.ar)] (c87b141)
---
## v1.9.1 - 2025-12-15
- **bugfix**: uso rama actual cuando esta es un entorno [[jteran](mailto:jteran@renatre.org.ar)] (c54cb95)
---
## v1.9.0 - 2025-12-15
- **bugfix**: uso env actual en vez de rama actual para listar ramas [[jteran](mailto:jteran@renatre.org.ar)] (5d33b29)
- **feature**: listar ramas de todos los entornos con flag --all, y del entorno actual por defecto [[jteran](mailto:jteran@renatre.org.ar)] (104b321)
- **feature**: agrego opcion para listar ramas wip [[jteran](mailto:jteran@renatre.org.ar)] (774cf9a)
- **refactor**: cambio opcion para listar ramas trash [[jteran](mailto:jteran@renatre.org.ar)] (74c333a)
- **bugfix**: elimino : extra de comando new [[jteran](mailto:jteran@renatre.org.ar)] (499babc)
---
## v1.8.0 - 2025-12-15
- **feature**: agrego opcion para incluir ticket en nombre de rama [[jteran](mailto:jteran@renatre.org.ar)] (01e938e)
- **feature**: mostrar warning si la rama actual no es un entorno configurado [[jteran](mailto:jteran@renatre.org.ar)] (97b4a56)
- **feature**: hacer pull en rama actual antes de crear nueva rama [[jteran](mailto:jteran@renatre.org.ar)] (24be501)
---
## v1.7.5 - 2025-12-14
- **bugfix**: verificar si la rama grupo trackea una rama remota antes de hacer pull [[jteran](mailto:jteran@renatre.org.ar)] (0d95a97)
- **bugfix**: capturo interrupion por teclado para no mostrar stack trace [[jteran](mailto:jteran@renatre.org.ar)] (0af5102)
---
## v1.7.4 - 2025-12-14
- **bugfix**: generación de PR de ramas release [[jteran](mailto:jteran@renatre.org.ar)] (b24b22a)
---
## v1.7.3 - 2025-12-14
- **bugfix**: listado de ramas para elegir grupo release no devolvia resultados [[jteran](mailto:jteran@renatre.org.ar)] (5600e5e)
---
## v1.7.2 - 2025-12-11
- **bugfix**: al hacer un release, cambio a la rama grupo si esta disponible [[jteran](mailto:jteran@renatre.org.ar)] (44b8efd)
---
## v1.7.1 - 2025-12-11
- **bugfix**: cambio la forma en la que se obtiene el first fork point entre 2 ramas [[jteran](mailto:jteran@renatre.org.ar)] (7b4fb34)
---
## v1.7.0 - 2025-12-11
- **feature**: muestro el listado de commits a pasar al siguiente entorno en release [[jteran](mailto:jteran@renatre.org.ar)] (2e65cb7)
---
## v1.6.4 - 2025-12-10
- **bugfix**: cambio la lista de commits a mostrar en el merge [[jteran](mailto:jteran@renatre.org.ar)] (4e089dc)
---
## v1.6.3 - 2025-12-04
- **bugfix**: revertir guion entre version y entorno, agregar guion a sufijo [[jteran](mailto:jteran@renatre.org.ar)] (7ce6add)
- **bugfix**: agrego guion entre version y nombre de entorno [[jteran](mailto:jteran@renatre.org.ar)] (053b8b1)
---
## v1.6.3- - 2025-12-04
- **bugfix**: agrego guion entre version y nombre de entorno [[jteran](mailto:jteran@renatre.org.ar)] (053b8b1)
---
## v1.6.2 - 2025-12-04
- **bugfix**: crear changelog cuando no existe [[jteran](mailto:jteran@renatre.org.ar)] (0c8e7bb)
---
## v1.6.1 - 2025-11-29
### Commits
- **bugfix**: arreglo mensaje de confirmacion de squash, muevo prompt de commit message antes del squash [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (f1f6e67)
- **bugfix**: cambio default en confirmacion para agrupar release [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (83df7e9)
---
## v1.6.0 - 2025-11-29
### Commits
- **feature**: agrego opcion para agrupar ramas release [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (c47afff)
---
## v1.5.2 - 2025-11-28
### Commits
- **bugfix**: arreglo split de ramas de flow.branches [[jteran](mailto:jteran@renatre.org.ar)] (2ad7471)
- **bugfix**: arreglo iteracion de items de flowconfig en set_config [[jteran](mailto:jteran@renatre.org.ar)] (5c3a317)
- **docs**: agrego venv a lista de carpetas ignoradas [[jteran](mailto:jteran@renatre.org.ar)] (50979e2)
---
## v1.5.1 - 2025-11-27
### Commits
- **refactor**: muevo constantes a modulo git_flow para centralizar uso [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (29757cc)
- **bugfix**: ignorar carpetas generadas en build de lsp [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (7f857fd)
---
## v1.5.0 - 2025-11-27 ## v1.5.0 - 2025-11-27
### Commits ### Commits
+5 -4
View File
@@ -47,11 +47,12 @@ El comando solicitará 2 opciones:
automáticamente. Por el momento, se soportan los siguientes remotos: automáticamente. Por el momento, se soportan los siguientes remotos:
- [x] Bitbucket - [x] Bitbucket
- [ ] Github: planeado - [x] Github
- [x] Gitea
La configuración del remoto requiere tener un API 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: *"Atlassian Account Settings" > "Security" > "Create and Manage API El mismo se puede crear desde el repositorio al que se quiere dar acceso: *"Repository settings" >
Tokens" > "Create API Tokens with scopes"*. El token necesita el scope `write:pullrequest:bitbucket` "Access Tokens" > "Create access token"*. El token necesita permiso para *Write* en *Pull Requests*.
Luego se debe guardar el token dentro del repositorio a configurar, en el archivo Luego se debe guardar el token dentro del repositorio a configurar, en el archivo
`.repository-token`. De no especificarse, los merges y tags se harán de forma local. `.repository-token`. De no especificarse, los merges y tags se harán de forma local.
+6 -5
View File
@@ -1,13 +1,14 @@
image: python:3.12 image: python:3.14
pipelines: pipelines:
branches: branches:
'main': 'main':
- step: - step:
name: "Tag main" name: "Tag and publish package"
caches: caches:
- pip - pip
script: script:
- cd $BITBUCKET_CLONE_DIR - pip install uv
- pip install . - uv run git-flow tag --token=$BEARER
- git-flow tag --token=$BEARER - uv build
- uv publish --token=$PYPI_TOKEN
+4 -4
View File
@@ -3,12 +3,12 @@ requires = ["setuptools >= 80", "setuptools-scm[simple] >= 8" ]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "git-flow" name = "git-flow-envs"
description = "Git workflow automation to follow best practices" description = "Git workflow automation to follow best practices when working with multiple environments"
dynamic = ["version"] dynamic = ["version"]
requires-python = ">= 3.12" requires-python = ">= 3.14"
dependencies = [ dependencies = [
"requests" "requests", "typer", "rich", "questionary"
] ]
authors = [ authors = [
{name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"}, {name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"},
+6
View File
@@ -0,0 +1,6 @@
{
"exclude": [
"build",
"**/*.egg-info"
]
}
+8 -1
View File
@@ -18,9 +18,12 @@ COMMIT_TYPES = [
"docs", "docs",
"style", "style",
"test", "test",
"wip" "wip",
] ]
WIP_BRANCH_PREFIX = "wip/"
TRASH_BRANCH_PREFIX = "trash/"
# Commits con estos valores incrementan el componente correspondiente # Commits con estos valores incrementan el componente correspondiente
SEMVER_MAJOR = 3 SEMVER_MAJOR = 3
SEMVER_MINOR = 2 SEMVER_MINOR = 2
@@ -46,3 +49,7 @@ SUPPORTED_REMOTE_APIS = [
] ]
REPOSITORY_TOKEN_FILENAME = ".repository-token" REPOSITORY_TOKEN_FILENAME = ".repository-token"
FLOWCONFIG_FILENAME = ".flowconfig"
FLOWCONFIG_VERSION = 2
+11 -8
View File
@@ -16,8 +16,6 @@ class Changelog:
return f"""## {title} - {now.strftime("%F")} return f"""## {title} - {now.strftime("%F")}
### Commits
""" """
def generate_content(self, base: str, branch: str): def generate_content(self, base: str, branch: str):
@@ -30,11 +28,15 @@ class Changelog:
message = Git("show", commit, patch=False, format="%s").firstline() message = Git("show", commit, patch=False, format="%s").firstline()
email = Git("show", commit, patch=False, format="%ae").firstline() email = Git("show", commit, patch=False, format="%ae").firstline()
username = email[: email.index("@")] username = email[: email.index("@")]
index = message.index(":") description = message
commit_type = message[:index]
commit_message = message[index + 1 :]
content += f"- **{commit_type}**: {commit_message} [[{username}](mailto:{email})] ({commit})\n" if ":" in message:
index = message.index(":")
commit_type = message[:index]
commit_message = message[index + 1 :]
description = f"**{commit_type}**: {commit_message}"
content += f"- {description} [[{username}](mailto:{email})] ({commit})\n"
return content return content
@@ -56,5 +58,6 @@ class Changelog:
if isfile(self.FILENAME): if isfile(self.FILENAME):
with open(self.FILENAME, "r") as f: with open(self.FILENAME, "r") as f:
content += f.read() content += f.read()
with open(self.FILENAME, "w") as f:
f.write(content) with open(self.FILENAME, "w") as f:
f.write(content)
+127 -198
View File
@@ -1,243 +1,172 @@
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,
REPOSITORY_TOKEN_FILENAME, REPOSITORY_TOKEN_FILENAME,
FLOWCONFIG_FILENAME,
FLOWCONFIG_VERSION,
GitFlowError, GitFlowError,
) )
from git_flow.git import FLOWCONFIG_FILE, Git from git_flow.git import Git
from git_flow.remote.base import RemoteAPI from git_flow.remote.base import RemoteAPI
from git_flow.remote.bitbucket import BitbucketRemoteAPI from git_flow.remote.bitbucket import BitbucketRemoteAPI
from git_flow.remote.github import GithubRemoteAPI from git_flow.remote.github import GithubRemoteAPI
from git_flow.remote.gitea import GiteaRemoteAPI
from git_flow.io import *
from os.path import isfile
COLOR_BLACK = "\033[30m" from typing import Optional
COLOR_RED = "\033[31m" import rich
COLOR_GREEN = "\033[32m" import rich.panel
COLOR_YELLOW = "\033[33m" import questionary
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"
TYPE_SUCCESS = 0
TYPE_WARNING = 1
TYPE_ERROR = 2
TYPE_INFO = 3
class Command(ABC): flowconfig = Git.get_config(FLOWCONFIG_FILENAME) if isfile(FLOWCONFIG_FILENAME) else {}
flowconfig: dict[str, str] environments = flowconfig["flow.branches"].split(",") if "flow.branches" in flowconfig else []
@abstractmethod def ensure_initialized():
def name(self) -> str: initialized = flowconfig["flow.initialized"] if flowconfig else None
pass
@abstractmethod if not initialized:
def description(self) -> str: raise GitFlowError(
pass "El repositorio no fue inicializado, debe ejecutar el comando `init`."
)
@abstractmethod elif initialized != "true":
def run(self, args: Namespace = Namespace()): raise GitFlowError(
pass f"El valor de `flow.initialized` ({initialized}) es inválido."
def init(self):
self.flowconfig = (
Git.get_config(FLOWCONFIG_FILE) if isfile(FLOWCONFIG_FILE) else {}
) )
def success(self, msg: str): version = int(flowconfig.get("flow.version", "1"))
self._print(TYPE_SUCCESS, msg)
def warning(self, msg: str): if version != FLOWCONFIG_VERSION:
self._print(TYPE_WARNING, msg) raise GitFlowError(
f"La versión del flowconfig ({version}) no es compatible con esta versión de git-flow ({FLOWCONFIG_VERSION}). "
f"Debe reinicializar el repositorio."
)
def error(self, msg: str):
self._print(TYPE_ERROR, msg)
def info(self, msg: str): def ensure_right_branch():
self._print(TYPE_INFO, msg) branch = Git.get_current_branch()
def prompt( if branch == "HEAD":
self, raise GitFlowError("No se encuentra parado sobre una rama.")
prompt: str, elif io_confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"):
default: str | None = None, return branch
persistent: bool = False, else:
strip: bool = True, raise GitFlowError("Ejecución cancelada.")
) -> str:
onetime = not persistent
while onetime or persistent:
if default:
prompt += f" [{default}]"
prompt += ": " def get_branch_env_and_type(branch: str) -> tuple[str, str]:
result = input(prompt) components = branch.split("/")
result = result.strip() if strip else result
if result: if len(components) == 2:
return result if components[0] not in BRANCH_TYPES:
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(
"El repositorio no fue inicializado, debe ejecutar el comando `init`." f"Branch inválida, el tipo '{components[0]}' no es válido."
)
elif initialized != "true":
raise GitFlowError(
f"El valor de `flow.initialized` ({initialized}) es inválido."
) )
def ensure_right_branch(self): return (environments[0], components[0])
branch = Git.get_current_branch() elif len(components) == 4:
valid_environments = environments[1:]
if branch == "HEAD": if (
raise GitFlowError("No se encuentra parado sobre una rama.") components[0] != "release"
elif self.confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"): or components[1] not in valid_environments
return branch or components[2] not in BRANCH_TYPES
else: ):
raise GitFlowError("Ejecución cancelada.")
def ensure_repository_token(self):
if not isfile(REPOSITORY_TOKEN_FILENAME):
raise GitFlowError( raise GitFlowError(
"No existe un token para el repositorio en " + REPOSITORY_TOKEN_FILENAME "Branch release inválido, debe tener el siguiente formato: "
"release/<env>/<type>/<name>, pero es: " + branch
) )
with open(REPOSITORY_TOKEN_FILENAME) as f: return (components[1], components[2])
return f.readline().strip() else:
raise GitFlowError("Branch inválido: " + branch)
def ensure_clean_worktree(self, has_remote: bool):
status = Git.status()
if not status: def ensure_repository_token():
return if not isfile(REPOSITORY_TOKEN_FILENAME):
raise GitFlowError(
"No existe un token para el repositorio en " + REPOSITORY_TOKEN_FILENAME
)
not_empty = any(map(lambda s: s.index != " " or s.worktree != " ", status)) with open(REPOSITORY_TOKEN_FILENAME) as f:
return f.readline().strip()
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.")
if not self.confirm("¿Desea continuar?", False): def ensure_clean_worktree(has_remote: bool):
raise GitFlowError("Ejecución abortada") status = Git.status()
def get_branch_env_and_type(self, branch: str) -> tuple[str, str]: if not status:
components = branch.split("/") return
target_branches = self.flowconfig["flow.branches"].split(" ")
if len(components) == 2: not_empty = any(map(lambda s: s.index != " " or s.worktree != " ", status))
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]) if not_empty:
elif len(components) == 4: if not has_remote:
target_branches = target_branches[1:] io_warning("Existen cambios en tu entorno de trabajo sin commitear.")
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: else:
raise GitFlowError("Branch inválido: " + branch) raise GitFlowError("No se puede continuar con cambios pendientes.")
def get_remote_api(self, token: str) -> RemoteAPI: if not io_confirm("¿Desea continuar?", False):
if "flow.remote" not in self.flowconfig: raise GitFlowError("Ejecución abortada")
raise GitFlowError("El repositorio no tiene configurado un remoto.")
[host, repository] = RemoteAPI.parse(self.flowconfig["flow.remote"])
if host == "bitbucket.org": def get_remote_api(token: str) -> RemoteAPI:
return BitbucketRemoteAPI(repository, token) if "flow.remote" not in flowconfig:
elif host == "github.com": raise GitFlowError("El repositorio no tiene configurado un remoto.")
return GithubRemoteAPI(repository, token)
else:
raise GitFlowError("El host del repositorio remoto es inválido")
def _print(self, type: int, msg: str): remote_type = flowconfig.get("flow.remote-type", "").lower()
print(self._get_color(type) + self._get_tag(type) + " " + msg + COLOR_RESET) [schema, host, repository] = RemoteAPI.parse(flowconfig["flow.remote"])
def _get_color(self, type: int) -> str: if remote_type == "gitea":
if type == TYPE_SUCCESS: return GiteaRemoteAPI(("https://" if schema == "ssh://" else schema) + host, repository, token)
return COLOR_GREEN elif remote_type == "bitbucket":
elif type == TYPE_WARNING: return BitbucketRemoteAPI(repository, token)
return COLOR_YELLOW elif remote_type == "github":
elif type == TYPE_ERROR: return GithubRemoteAPI(repository, token)
return COLOR_RED else:
elif type == TYPE_INFO: raise GitFlowError("El tipo del remoto es inválido")
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 "" 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}]")
def io_confirm(question: str, default: bool = True) -> bool:
return questionary.confirm(question, default=default, auto_enter=False).ask()
def io_choice(prompt: str, options: list[str]) -> str:
return questionary.select(prompt, options, instruction="Usar flechas").ask()
def io_prompt(message: str, default: str = "", persistent: bool = False, strip: bool = False, validator = None) -> str:
if persistent and not validator:
validator = lambda s: len(s.strip()) > 0
value = questionary.text(message, default, validate=validator).ask()
return value.strip() if strip else value
+125 -74
View File
@@ -1,89 +1,140 @@
from argparse import ArgumentParser, Namespace from git_flow import (
from git_flow import GitFlowError TRASH_BRANCH_PREFIX,
from git_flow.command.base import COLOR_GREEN, COLOR_RED, COLOR_RESET, COLOR_WHITE_BOLD, Command WIP_BRANCH_PREFIX,
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"
def description(self) -> str: @app.command()
return """Lista ramas del repositorio, agrupandolas por entorno objetivo""" def branch(
environment: Annotated[
str | None,
typer.Argument(
help="Entorno de las ramas a listar. Por defecto, es el entorno actual."
),
] = None,
trash: Annotated[
bool,
typer.Option(
help=f"Listar ramas en la 'papelera de reciclaje' (ramas que empiezan con '{TRASH_BRANCH_PREFIX}')"
),
] = False,
wip: Annotated[
bool,
typer.Option(
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()
def setup_parser(self, parser: ArgumentParser) -> ArgumentParser: current_branch = Git.get_current_branch()
parser.add_argument(
"environment", if current_branch in environments:
nargs="?", current_environment = current_branch
help="Entorno de las ramas a listar. Puede ser vacio para listar ramas de todos los entornos, un entorno válido, o 'trash' para listar las ramas eliminadas.", 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
) )
return parser trash = len(get_branches(TRASH_BRANCH_PREFIX))
def run(self, args: Namespace = Namespace()): if trash:
self.ensure_initialized()
self.current_branch = Git.get_current_branch()
self.environments = self.flowconfig["flow.branches"].split(",")
if args.environment is None:
self.show_envs()
self.show_all_branches()
elif args.environment in self.environments:
self.show_envs()
self.show_env_branches(args.environment)
elif args.environment == "trash":
self.show_trash_branches()
else:
raise GitFlowError(f"'{args.environment}' no es un entorno válido.")
def show_trash_branches(self):
branches = self.get_trash_branches()
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_trash_branches(self):
return Git("for-each-ref", "refs/heads/trash/", 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_trash_branches())
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/", "refs/heads/release/"] if is_first_env else []
branches = Git("for-each-ref", pattern, exclude=exclude, format=BRANCH_FORMAT).lines()
print(COLOR_WHITE_BOLD + (environment if show_env else "Ramas") + COLOR_RESET) def show_envs(environments: list[str], current_branch: str):
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
for branch in branches: def show_env_branches(
prefix = (COLOR_GREEN + "*") if branch == self.current_branch else " " environment: str,
print(prefix + " " + branch + COLOR_RESET) environments: list[str],
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)
+52 -50
View File
@@ -1,62 +1,64 @@
from argparse import Namespace import typer
from datetime import datetime from datetime import datetime
from git_flow import COMMIT_TYPES, 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()
class CommitCommand(Command): @app.command()
def name(self) -> str: def commit():
return "commit" """Crea un commit siguiendo el formato de Conventional Commits"""
base.ensure_initialized()
branch = base.ensure_right_branch()
def description(self) -> str: if not _has_files_staged():
return """Crea un commit siguiendo el formato de Conventional Commits""" base.io_warning("No hay cambios en el indice para commitear.")
if base.io_confirm("¿Desea agregar la carpeta actual?"):
def run(self, args: Namespace = Namespace()): Git("add", ".").exec(print="Agregando cambios")
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/"):
original_branch = branch.removeprefix("wip/")
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 + "/" + suffix, move=True).exec(print="Backup de rama WIP")
else: else:
if commit_type == "wip": raise GitFlowError("Debe agregar algún cambio al indice para continuar.")
Git("switch", "wip/" + branch, create=True).exec(print="Creando nueva rama WIP")
Git("commit", m=message).exec(print="Creando commit") commit_type = base.io_choice("Tipo de commit", COMMIT_TYPES)
commit_message = base.io_prompt(
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
)
message = commit_type + ": " + commit_message
def _has_files_staged(self): if branch.startswith(WIP_BRANCH_PREFIX):
status = Git.status() original_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
Git("commit", m=message).exec(print="Creando commit en rama WIP")
if not status: if commit_type != "wip":
raise GitFlowError("No hay cambios para commitear.") 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:
if commit_type == "wip":
Git("switch", WIP_BRANCH_PREFIX + branch, create=True).exec(
print="Creando nueva rama WIP"
)
files_in_index = [] Git("commit", m=message).exec(print="Creando commit")
for s in status:
if s.worktree != "?" and s.worktree != " ":
files_in_index.append(s.file)
return len(files_in_index) > 0 def _has_files_staged():
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
+98 -92
View File
@@ -1,116 +1,122 @@
from argparse import Namespace import typer
import os
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()
class InitCommand(Command): @app.command()
def name(self) -> str: def init():
return "init" """Inicializa el repositorio para utilizar git-flow"""
def description(self) -> str: _ensure_is_repository()
return """Inicializa el repositorio para utilizar git-flow""" _ensure_not_already_initialized()
def run(self, args: Namespace = Namespace()): branches = _setup_flow_branches()
self._ensure_is_repository() (remote, remote_type) = _setup_flow_remote()
self._ensure_not_already_initialized()
branches = self._setup_flow_branches() flowconfig = {
remote = self._setup_flow_remote() "flow.version": str(FLOWCONFIG_VERSION),
"flow.initialized": "true",
"flow.branches": ",".join(branches),
}
flowconfig = { if remote and remote_type:
"flow.initialized": "true", flowconfig["flow.remote"] = remote
"flow.branches": ",".join(branches), flowconfig["flow.remote-type"] = remote_type
}
if remote: Git.set_config(flowconfig, FLOWCONFIG_FILENAME)
flowconfig["flow.remote"] = remote Git("add", FLOWCONFIG_FILENAME).exec()
Git("commit", message="feature: initialize git-flow").exec()
Git.set_config(flowconfig, FLOWCONFIG_FILE) _ensure_all_flow_branches_exist(branches, remote)
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(self): def _ensure_is_repository():
if not Git.is_repository(): if not Git.is_repository():
self.warning("El directorio actual no es un repositorio.") warning("El directorio actual no es un repositorio.")
if not self.confirm("¿Desea inicializarlo?"): if not confirm("¿Desea inicializarlo?"):
raise GitFlowError( raise GitFlowError("No se puede continuar sin inicializar el repositorio")
"No se puede continuar sin inicializar el repositorio"
)
Git("init").exec() Git("init").exec()
def _ensure_not_already_initialized(self):
if not self.flowconfig: def _ensure_not_already_initialized():
return if not flowconfig:
elif self.flowconfig.get("flow.initialized") == "true": return
raise GitFlowError("El repositorio ya fue inicializado para usar git-flow") 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: else:
raise GitFlowError("Valor de 'flow.initialized' es inválido") info("Tiene más de un remoto, seleccione el que va a utilizar.")
remote = choice("Remoto", remotes)
def _setup_flow_branches(self): remote_type = choice("Tipo de remoto:", ["bitbucket", "github", "gitea"])
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( return (remote, remote_type)
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 _ensure_all_flow_branches_exist(branches: list[str], remote: str | None):
existing_branches = Git.get_branches()
def _setup_flow_remote(self): if not all(map(lambda b: b in existing_branches, branches)):
self.info( warning("Algunas de las ramas de entornos expecificadas no existen.")
"""Puede elegir o crear un repositorio remoto para generar automáticamente PRs""" info("Debe indicar sobre que rama se crearán las ramas de entorno.")
)
remote = None target_branch = choice("Rama", existing_branches)
if self.confirm("¿Configurar repositorio remoto?"): for branch in branches:
remotes = Git("remote").lines(print="Listando remotos disponibles") if branch not in existing_branches:
Git("branch", branch, target_branch).exec(
if not remotes: print="Creando rama inexistente"
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 if remote:
Git("push", remote, branch, set_upstream=True).exec(
def _ensure_all_flow_branches_exist(self, branches: list[str], remote: str | None): print="Creando rama en remoto"
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"
)
+117 -63
View File
@@ -1,92 +1,146 @@
from argparse import Namespace import typer
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.changelog import Changelog from git_flow.changelog import Changelog
from git_flow.command.base import Command import git_flow.command.base as base
from git_flow.git import Git from git_flow.git import Git
app = typer.Typer()
class MergeCommand(Command):
def name(self) -> str:
return "merge"
def description(self) -> str: @app.command()
return """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado""" def merge():
"""Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
base.ensure_initialized()
def run(self, args: Namespace = Namespace()): branch = base.ensure_right_branch()
self.ensure_initialized()
branch = self.ensure_right_branch() if branch in base.environments:
(target, _) = self.get_branch_env_and_type(branch) if branch == base.environments[-1]:
base = Git.get_first_fork_point(branch, target) raise GitFlowError("No se puede hacer un merge del último entorno.")
if "flow.remote" in self.flowconfig: base.io_info(
remote = self.flowconfig["flow.remote"] """Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""",
token = self.ensure_repository_token() "Merge de entornos",
self.run_remote(remote, token, base, branch, target) )
else:
self.run_local(base, branch, target)
def run_remote(self, remote: str, token: str, base: str, branch: str, target: str): if not base.io_confirm("¿Desea continuar?"):
self.ensure_clean_worktree(True) raise GitFlowError("Ejecución abortada")
Git("switch", target).exec(print="Cambiando a rama destino") target = base.environments[base.environments.index(branch) + 1]
Git("pull").exec(print="Obteniendo ultimos cambios del remoto") else:
Git("switch", "-").exec(print="Volviendo a rama a mergear") target, _ = base.get_branch_env_and_type(branch)
self.check_merge_conflicts(target) if "flow.remote" in base.flowconfig:
self.show_commits_to_merge(base) remote = base.flowconfig["flow.remote"]
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"
) )
self.create_pull_request(token, base, branch, target) create_pull_request(token, branch, target)
def run_local(self, base: str, branch: str, target: str):
self.ensure_clean_worktree(False)
self.check_merge_conflicts(target)
self.show_commits_to_merge(base)
if self.confirm(f"¿Mergear rama '{branch}' a '{target}'?"): def run_local(branch: str, target: str):
Git("switch", target).exec(print="Cambiando a rama destino") base.ensure_clean_worktree(False)
Git("merge", branch, ff=False).exec(print="Mergeando") check_merge_conflicts(target)
show_commits_to_merge(target)
def show_commits_to_merge(self, base): if base.io_confirm(f"¿Mergear rama '{branch}' a '{target}'?"):
commits = Git("log", base + "..", format="%s").lines() Git("switch", target).exec(print="Cambiando a rama destino")
Git("merge", branch, ff=False).exec(print="Mergeando")
if not commits:
raise GitFlowError("No hay cambios a mergear.")
print("Cambios a mergear:") def show_commits_to_merge(target):
commits = Git("log", target + "..", format="%s").lines()
for commit in commits: if not commits:
print("- " + commit) raise GitFlowError("No hay cambios a mergear.")
def check_merge_conflicts(self, target: str): print("Cambios a mergear:")
merge_conflicts = Git("merge", target, ff=False, commit=False).code(
print="Realizando merge de prueba para verificar conflictos"
)
Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False) for commit in commits:
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 create_pull_request(self, token: str, base: str, branch: str, target: str): def check_merge_conflicts(target: str):
changelog = Changelog() merge_conflicts = Git("merge", target, ff=False, commit=False).code(
print="Realizando merge de prueba para verificar conflictos"
)
message = self.get_remote_api(token).create_pull_request( Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False)
branch,
target,
branch.replace("/", ": ").replace("-", " "),
changelog.generate_content(base, branch),
)
self.success(message) if merge_conflicts:
base.io_error(f"La rama actual tiene conflictos con {target}.\n"
f"Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente, y ejecutar nuevamente este comando", "Conflictos de merge")
raise GitFlowError("Ejecución abortada.")
else:
base.io_success("No se detectaron merge conflicts.")
def create_pull_request(token: str, branch: str, target: str):
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 base.io_confirm("¿Desea cambiar el título del PR?", False):
title = base.io_prompt("Título del PR", persistent=True)
message = base.get_remote_api(token).create_pull_request(
branch,
target,
title,
changelog.generate_content(target, branch),
branch not in base.environments
)
base.io_success(message, "Rama creada")
if base.io_confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"):
Git("switch", target).exec(print="Cambiando a rama objetivo")
Git("pull").exec(print="Obteniendo cambios")
def _get_pr_title_from_branch(branch: str) -> str:
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("-", " ")
]))
+44 -36
View File
@@ -1,49 +1,57 @@
from argparse import Namespace import typer
from git_flow import BRANCH_TYPES from git_flow import BRANCH_TYPES
from git_flow.command.base import Command import git_flow.command.base as base
from git_flow.git import Git from git_flow.git import Git
from typing import Optional
app = typer.Typer()
class NewCommand(Command): @app.command()
def name(self) -> str: def new():
return "new" """Crea una nueva rama siguiendo Conventional Branches"""
def description(self) -> str: base.ensure_initialized()
return """Crea una nueva rama siguiendo conventional branches""" branch = base.ensure_right_branch()
def run(self, args: Namespace = Namespace()): if branch not in base.environments:
self.ensure_initialized() base.io_warning("La rama actual no corresponde a un entorno configurado.")
self.ensure_right_branch()
self.info("Se creará una nueva rama de trabajo sobre la rama actual.") base.io_info("Se creará una nueva rama de trabajo sobre la rama actual.\n"
self.info("Debe seleccionar el tipo de cambio a realizar.") "Debe seleccionar el tipo de cambio a realizar.", title="Tipo de cambio")
branch_type = self.choice("Tipos de cambio", BRANCH_TYPES) branch_type = base.io_choice("Tipos de cambio", BRANCH_TYPES)
new_branch = self._get_unique_branch_name(branch_type) ticket = _get_optional_ticket()
new_branch = _get_unique_branch_name(branch_type, ticket)
if self.confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"): if "flow.remote" in base.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_unique_branch_name(self, branch_type: str) -> str: if base.io_confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"):
self.info( Git("switch", new_branch, create=True).exec(print="Creando nueva rama")
f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'."
)
branch = None
branches = Git.get_branches()
while branch is None: def _get_optional_ticket() -> Optional[str]:
keywords = self.prompt( return base.io_prompt(
"Palabras clave del cambio (por ejemplo: 'create worker form')", "Ticket que respalda el cambio (en formato ABC-123)",
persistent=True, 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:
base.io_info(f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.\n"
"Las mismas se utilizaran para generar el nombre de la rama.", title="Nombre de rama")
branch = None
branches = Git.get_branches()
while branch is None:
keywords = base.io_prompt("Palabras clave", persistent=True)
keywords = filter(lambda k: len(k) > 0, keywords.split(" "))
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
keywords = filter(lambda k: len(k) > 0, keywords.split(" ")) return branch
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
+76 -52
View File
@@ -1,67 +1,91 @@
from argparse import Namespace
from git_flow import COMMIT_TYPES, GitFlowError from git_flow import COMMIT_TYPES, GitFlowError
from git_flow.command.base import Command from git_flow.command.base import *
from git_flow.git import Git from git_flow.git import Git
from typing import Optional
import typer
app = typer.Typer()
class ReleaseCommand(Command): @app.command()
def name(self) -> str: def release(group: Optional[str] = None):
return "release" """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
ensure_initialized()
def description(self) -> str: branch = ensure_right_branch()
return """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado""" env, _ = get_branch_env_and_type(branch)
def run(self, args: Namespace = Namespace()): if env == environments[-1]:
self.ensure_initialized() raise GitFlowError(
"No se puede hacer release de una rama en el ultimo entorno."
branch = self.ensure_right_branch()
envs = self.flowconfig["flow.branches"].split(",")
(env, _) = self.get_branch_env_and_type(branch)
if env == envs[-1]:
raise GitFlowError(
"No se puede hacer release de una rama en el ultimo entorno."
)
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}"
) )
if has_remote: has_remote = "flow.remote" in flowconfig
Git("pull").exec(print="Sincronizando cambios de la rama")
Git("switch", next_env).exec(print="Cambiando al siguiente entorno")
Git("pull").exec(print="Sincronizando cambios del entorno")
Git("switch", "-").exec(print="Volviendo a la rama original")
base = Git.get_first_fork_point(branch, env) ensure_clean_worktree(has_remote)
commits = int(Git("rev-list", base + "..", count=True).firstline())
if commits > 1 and self.confirm( next_env = environments[environments.index(env) + 1]
"Desea reemplazar los {commits} commits de la rama por uno solo?" next_branch = (
): branch.replace(f"/{env}/", f"/{next_env}/")
Git("switch", next_branch, base, create=True).exec( if branch.startswith("release/")
print="Creando rama release en base" else f"release/{next_env}/{branch}"
) )
Git("merge", branch, squash=True).exec(
print="Squasheando commits en uno solo"
)
commit_type = self.choice("Tipo de commit", COMMIT_TYPES[:-1]) base = Git.get_first_fork_point(branch, env)
commit_message = self.prompt( commits = Git("log", base + "..", format="%s").lines()
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
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 + "/")
) )
message = commit_type + ": " + commit_message
Git("commit", m=message).exec(print="Creando commit único")
else: else:
Git("switch", next_branch, create=True).exec(print="Creando rama release") group = next_env
Git("rebase", base, next_branch, onto=next_env).exec( if has_remote and Git.get_tracking_branch(group):
print="Moviendo cambios hacia el siguiente ambiente" 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 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(
print="Creando rama release en base"
)
Git("merge", branch, squash=True).exec(print="Squasheando commits en uno solo")
Git("commit", m=message).exec(print="Creando commit único")
else:
Git("switch", next_branch, create=True).exec(print="Creando rama release")
status = Git("rebase", 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"
) )
+75 -73
View File
@@ -1,4 +1,3 @@
from argparse import ArgumentParser, Namespace
from git_flow import ( from git_flow import (
COMMIT_TYPE_INCREMENT, COMMIT_TYPE_INCREMENT,
SEMVER_MAJOR, SEMVER_MAJOR,
@@ -8,100 +7,103 @@ from git_flow import (
GitFlowError, GitFlowError,
) )
from git_flow.changelog import Changelog from git_flow.changelog import Changelog
from git_flow.command.base import Command from git_flow.command.base import *
from git_flow.git import Git from git_flow.git import Git
from typing import Optional
import typer
app = typer.Typer()
class TagCommand(Command): @app.command()
def name(self) -> str: def tag(token: Optional[str] = None):
return "tag" """Crea un nuevo tag para el último merge."""
ensure_initialized()
def description(self) -> str: target = Git.get_current_branch()
return """Crea un nuevo tag para el último merge."""
def run(self, args: Namespace = Namespace()): if target not in environments:
self.ensure_initialized() raise GitFlowError("Solo se pueden taggear commits en ramas principales.")
envs = self.flowconfig["flow.branches"].split(",") branch = Git(
target = Git.get_current_branch() "show", 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 = get_next_tag_from_commits(commits, target)
if target not in envs: if not next_tag:
raise GitFlowError("Solo se pueden taggear commits en ramas principales.") info(
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
)
return
branch = Git("show", self.get_last_merge_commit() + "^2", patch=False, format="%h").firstline() token, ci = get_token_and_ci(token)
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 next_tag: if not token:
self.info( raise GitFlowError("No hay token disponible")
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
)
return
(token, ci) = self.get_token_and_ci(args) changelog = Changelog()
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")
self.success(self.get_remote_api(token).create_tag(next_tag, tag_commit)) success(get_remote_api(token).create_tag(next_tag, tag_commit))
else:
Git("tag", next_tag, tag_commit).exec(print="Creando tag localmente")
def get_token_and_ci(self, args: Namespace): Git("tag", next_tag, tag_commit).exec(print="Creando tag localmente")
try:
return (self.ensure_repository_token(), False)
except GitFlowError as e:
return (args.token, True)
def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
parser.add_argument("--token")
return parser def get_token_and_ci(token: Optional[str]):
try:
return (ensure_repository_token(), False)
except GitFlowError:
return (token, True)
def get_last_merge_commit(self):
commit = Git(
"log", first_parent=True, merges=True, max_count=1, format="%h"
).firstline()
if not commit: def get_last_merge_commit():
raise GitFlowError("No hay merges a taggear.") commit = Git(
"log", first_parent=True, merges=True, max_count=1, format="%h"
).firstline()
return commit if not commit:
raise GitFlowError("No hay merges a taggear.")
def check_not_tagged(self, commit: str): return commit
tag = Git("describe", commit, tags=True, exact_match=True).firstline(check=False)
if tag:
raise GitFlowError("El commit a taggear ya tiene tag: " + tag)
def get_next_tag_from_commits(self, commits: list[str], branch: str) -> str | None: def check_not_tagged(commit: str):
increment = SEMVER_SKIP tag = Git("describe", commit, tags=True, exact_match=True).firstline(check=False)
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}" if tag:
raise GitFlowError("El commit a taggear ya tiene tag: " + tag)
until_dash = tag.index("-") if "-" in tag else None
suffix = tag[until_dash + 1 :] if until_dash is not None else ""
[major, minor, patch] = map(int, tag[1:until_dash].split("."))
if increment == SEMVER_MAJOR: def get_next_tag_from_commits(commits: list[str], branch: str) -> str | None:
major += 1 increment = SEMVER_SKIP
minor = 0 for commit in commits:
patch = 0 commit_type = commit[: commit.index(":")]
elif increment == SEMVER_MINOR: increment = max(increment, COMMIT_TYPE_INCREMENT.get(commit_type, SEMVER_SKIP))
minor += 1
patch = 0
elif increment == SEMVER_PATCH:
patch += 1
return None if increment == SEMVER_SKIP else f"v{major}.{minor}.{patch}{suffix}" tag = Git.get_current_tag() or f"v0.0.0-{branch}"
until_dash = tag.index("-") if "-" in tag else None
suffix = tag[until_dash:] if until_dash is not None else ""
[major, minor, patch] = map(int, tag[1:until_dash].split("."))
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}"
+29 -44
View File
@@ -1,15 +1,9 @@
import subprocess import subprocess
from git_flow import FLOWCONFIG_FILENAME
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
@@ -38,20 +32,20 @@ class Git:
@staticmethod @staticmethod
def set_config(config: dict[str, str], file: str | None = None): def set_config(config: dict[str, str], file: str | None = None):
for key, value in config: for key, value in config.items():
Git("config", key, value, file=file).exec() Git("config", key, value, file=file).exec()
@staticmethod @staticmethod
def flow_config(*args, **kwargs): def flow_config(*args, **kwargs):
return Git("config", *args, file=FLOWCONFIG_FILE, **kwargs) return Git("config", *args, file=FLOWCONFIG_FILENAME, **kwargs)
@staticmethod @staticmethod
def get_current_branch(): def get_current_branch():
return Git("rev-parse", "HEAD", abbrev_ref=True).firstline() return Git("rev-parse", "HEAD", abbrev_ref=True).firstline()
@staticmethod @staticmethod
def get_branches(): def get_branches(prefix: str = ""):
return Git._get_references("heads") return Git._get_references("heads/" + prefix)
@staticmethod @staticmethod
def status(): def status():
@@ -63,33 +57,33 @@ class Git:
@staticmethod @staticmethod
def get_first_fork_point(branch: str, env: str): def get_first_fork_point(branch: str, env: str):
boundary_commits = list( # Lista de commits alcanzables por el entorno (por ejemplo, dev),
filter( env_commits = Git("rev-list", env + "..." + branch, first_parent=True).lines()
lambda c: c.startswith("-"), # boundary commits
Git(
"rev-list", env + "..." + branch, topo_order=True, boundary=True
).lines(),
)
)
if not boundary_commits: if not env_commits:
raise RuntimeError("No se encontro un commit base para realizar el rebase.") raise RuntimeError("No se encontro un commit base para realizar el rebase.")
return boundary_commits[-1].removeprefix("-") return Git("show", env_commits[-1] + "^", patch=False, format="%h").firstline()
@staticmethod @staticmethod
def _get_references(kind: str): def _get_references(kind: str):
prefix = "refs/" + kind + "/" prefix = "refs/" + kind
return list(map( return Git("for-each-ref", prefix + "**", format="%(refname:short)").lines()
lambda r: r.removeprefix(prefix),
Git("for-each-ref", prefix + "*", format="%(refname)").lines(),
))
@staticmethod @staticmethod
def is_repository() -> bool: def is_repository() -> bool:
return Git("rev-parse", is_inside_work_tree=True).code() == 0 return Git("rev-parse", is_inside_work_tree=True).code() == 0
def __init__(self, subcommand: str, *args: str, **kwargs: str | int | bool | list[str] | None) -> None: @staticmethod
def get_tracking_branch(branch: str) -> str | None:
try:
return Git("rev-parse", branch + "@{upstream}", abbrev_ref=True).firstline()
except subprocess.CalledProcessError:
return None
def __init__(
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():
@@ -134,7 +128,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:
@@ -145,17 +139,8 @@ class Git:
return process return process
def _run(self, print: str | None = None, check: bool = True):
def _run(self, print: str|None = None, check: bool = True): return self._get(print, check)
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 = []
@@ -169,12 +154,12 @@ class Git:
command.append(arg) command.append(arg)
title = title or "Ejecutando" text = f"[green]$ {" ".join(command)}[/green]"
print(COLOR_YELLOW + "| " + title + COLOR_RESET)
print(COLOR_YELLOW + "| $ " + " ".join(command) + COLOR_RESET)
for line in stdout.splitlines(): for line in stdout.splitlines():
print(COLOR_YELLOW + "| " + COLOR_RESET + "[out] " + line) text += "\n" + line
for line in stderr.splitlines(): for line in stderr.splitlines():
print(COLOR_YELLOW + "| " + COLOR_RED + "[err] " + line + COLOR_RESET) text += "\n[red]" + line + "[/red]"
panel(title or "Ejecutando", text)
+38 -47
View File
@@ -1,64 +1,55 @@
import sys from rich.console import Console
from rich.prompt import Prompt, Confirm
from rich.panel import Panel
console = Console(highlight=False)
COLOR_BLACK = '\033[30m' def success(msg: str):
COLOR_RED = '\033[31m' console.print(f":white_check_mark: [green]{msg}[/green]")
COLOR_GREEN = '\033[32m'
COLOR_YELLOW = '\033[33m'
COLOR_BLUE = '\033[34m'
COLOR_BLACK_BOLD = '\033[1;30m'
COLOR_RESET = '\033[0m'
def confirm(prompt, default: bool = True): def warning(msg: str):
user_input = input(prompt + (" [Y/n]: " if default else " [y/N]: ")) console.print(f":warning: [yellow]{msg}[/yellow]")
return default if len(user_input) == 0 else user_input.startswith("y")
def choose(prompt: str, options: list[str]): def error(msg: str):
print(prompt) console.print(f":x: [red]{msg}[/red]")
for i, option in enumerate(options):
print(f"\t{i+1}. {option}")
selection = None def info(msg: str):
console.print(f":information: [blue]{msg}[/blue]")
while selection is None:
user_input = input(f"Seleccione una opción [1-{len(options)}] o escribala: ")
if user_input.isdigit(): def panel(title: str, text: str):
user_input = int(user_input) console.print(Panel(text, title=title, expand=False, title_align="left"))
if 1 <= user_input and user_input <= len(options):
selection = options[user_input - 1]
if not confirm( def prompt(
f"Seleccionó la opción {user_input} ({selection}), ¿es correcto?" prompt: str,
): default: str | None = None,
selection = None persistent: bool = False,
else: strip: bool = True,
print_error(f"La opción {user_input} está fuera del rango permitido.") ) -> str:
elif user_input in options: onetime = not persistent
selection = user_input
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:
print_error(f"La opción '{user_input}' es inválida.") return result
return selection
def print_error(message: str): def confirm(question: str, default: bool = True):
print(f"{COLOR_RED}[err] {message}{COLOR_RESET}", file=sys.stderr) return Confirm.ask(question, default=default)
def print_warning(message: str): def choice(prompt: str, options: list[str]):
print(f"{COLOR_YELLOW}[wrn] {message}{COLOR_RESET}") return Prompt.ask(prompt, choices=options)
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}")
+17 -72
View File
@@ -1,87 +1,32 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import locale import typer
from argparse import Namespace, ArgumentParser
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.command.base import Command import git_flow.command.base as base
from git_flow.command.branch import BranchCommand from git_flow.command import init, new, commit, merge, tag, release, branch
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"
class GitFlowCommand(Command): app = typer.Typer()
commands: tuple[Command, ...] app.add_typer(init.app)
app.add_typer(new.app)
def __init__(self, *args: Command) -> None: app.add_typer(commit.app)
self.commands = args app.add_typer(merge.app)
app.add_typer(tag.app)
def name(self) -> str: app.add_typer(release.app)
return "git-flow" app.add_typer(branch.app)
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:
locale.setlocale(locale.LC_ALL, LOCALE) app()
except locale.Error as e: print()
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:
command.error(str(e)) base.io_error(str(e), title="Git Flow")
except Exception as e: except Exception as e:
command.error("Ocurrió un error inesperado: " + str(e)) base.io_error(str(e), title="Inesperado")
except KeyboardInterrupt as e:
return base.io_error("Ejecución abortada")
if __name__ == "__main__":
main()
+11 -28
View File
@@ -12,42 +12,24 @@ class RemoteAPI(ABC):
@staticmethod @staticmethod
def parse(remote: str): def parse(remote: str):
url = Git("remote", "get-url", remote).firstline() url = Git("remote", "get-url", remote).firstline()
invalid_url_error = GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
url_type = None
if url.startswith("git@"): if url.startswith("git@"):
url_type = "ssh"
elif url.startswith("http://") or url.startswith("https://"):
url_type = "https"
else:
raise invalid_url_error
if url_type == "ssh":
# git@host:user/repo[.git]
at_pos = url.index("@") at_pos = url.index("@")
colon_pos = url.index(":") colon_pos = url.index(":")
schema = "ssh://"
host = url[at_pos+1:colon_pos] host = url[at_pos+1:colon_pos]
repository = url[colon_pos+1:] repository = url[colon_pos+1:]
else: elif url.startswith("http://") or url.startswith("https://"):
# https://host/user/repo[.git] start_pos = url.index("://") + 3
host_start = url.index("://") + 3 uri_start = url.index("/", start_pos)
uri_start = url.index("/", host_start) schema = url[:start_pos]
host = url[host_start:uri_start] host = url[start_pos:uri_start]
repository = url[uri_start+1:] repository = url[uri_start+1:]
else:
raise GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
# Esto permite configurar hosts "imaginarios" para permitir el uso de multiples claves ssh repository = repository.removesuffix(".git")
if host.endswith(".bitbucket.org"): return [schema, host, repository]
host = "bitbucket.org"
elif host.endswith(".github.com"):
host = "github.com"
if host not in SUPPORTED_REMOTE_APIS:
raise GitFlowError("El host del repositorio remoto es inválido")
repository.removesuffix(".git")
return [host, repository]
@abstractmethod @abstractmethod
def create_pull_request( def create_pull_request(
@@ -56,6 +38,7 @@ class RemoteAPI(ABC):
destination: str, destination: str,
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
close_source_branch: bool = True
) -> str: ) -> str:
pass pass
+2 -1
View File
@@ -12,6 +12,7 @@ class BitbucketRemoteAPI(RemoteAPI):
destination: str, destination: str,
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
close_source_branch: bool = True
): ):
request = requests.post( request = requests.post(
self.get_endpoint("/pullrequests"), self.get_endpoint("/pullrequests"),
@@ -21,7 +22,7 @@ class BitbucketRemoteAPI(RemoteAPI):
"description": description, "description": description,
"source": {"branch": {"name": source}}, "source": {"branch": {"name": source}},
"destination": {"branch": {"name": destination}}, "destination": {"branch": {"name": destination}},
"close_source_branch": True, "close_source_branch": close_source_branch,
}, },
) )
+55
View File
@@ -0,0 +1,55 @@
import requests
from git_flow import GitFlowError
from git_flow.remote.base import RemoteAPI
class GiteaRemoteAPI(RemoteAPI):
def __init__(self, base_url: str, repository: str, token: str) -> None:
super().__init__(repository, token)
self.base_url = base_url
def create_pull_request(
self,
source: str,
destination: str,
title: str | None = None,
description: str | None = None,
close_source_branch: bool = True
) -> str:
response = requests.post(
self.get_endpoint("/pulls"),
headers=self.get_headers(),
json={
"title": title,
"body": description,
"head": source,
"base": destination,
},
)
if response.ok:
return "PR creado exitosamente: " + response.json()["html_url"]
else:
raise GitFlowError("Ocurrió un error al crear el PR: " + response.text)
def create_tag(self, tag: str, commit: str) -> str:
response = requests.post(
self.get_endpoint("/tags"),
headers=self.get_headers(),
json={"tag_name": tag, "target": commit},
)
if response.ok:
return "Tag creado exitosamente: " + tag
else:
raise GitFlowError("Ocurrió un error al crear el tag: " + response.text)
def get_endpoint(self, resource: str) -> str:
return f"{self.base_url}/api/v1/repos/{self.repository}{resource}"
def get_headers(self):
return {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "token " + self.token,
}
+29 -4
View File
@@ -1,9 +1,10 @@
import requests
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.remote.base import RemoteAPI from git_flow.remote.base import RemoteAPI
class GithubRemoteAPI(RemoteAPI): class GithubRemoteAPI(RemoteAPI):
API_ENDPOINT = "https://github.com" API_ENDPOINT = "https://api.github.com/repos"
def create_pull_request( def create_pull_request(
self, self,
@@ -11,11 +12,35 @@ class GithubRemoteAPI(RemoteAPI):
destination: str, destination: str,
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
close_source_branch: bool = True
) -> str: ) -> str:
raise GitFlowError("not implemented yet!") response = requests.post(
self.get_endpoint("/pulls"),
headers=self.get_headers(),
json={
"title": title,
"body": description,
"head": source,
"base": destination,
},
)
if response.ok:
return "PR creado exitosamente: " + response.json()["html_url"]
else:
raise GitFlowError("Ocurrió un error al crear el PR: " + response.text)
def create_tag(self, tag: str, commit: str) -> str: def create_tag(self, tag: str, commit: str) -> str:
raise GitFlowError("not implemented yet!") response = requests.post(
self.get_endpoint("/git/refs"),
headers=self.get_headers(),
json={"ref": f"refs/tags/{tag}", "sha": commit},
)
if response.ok:
return "Tag creado exitosamente: " + tag
else:
raise GitFlowError("Ocurrió un error al crear el tag: " + response.text)
def get_endpoint(self, resource: str) -> str: def get_endpoint(self, resource: str) -> str:
return GithubRemoteAPI.API_ENDPOINT return f"{GithubRemoteAPI.API_ENDPOINT}/{self.repository}{resource}"
Generated
+218
View File
@@ -0,0 +1,218 @@
version = 1
revision = 3
requires-python = ">=3.14"
[[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]]
name = "certifi"
version = "2026.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
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" }
wheels = [
{ 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/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/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "click"
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 = "." }
dependencies = [
{ name = "questionary" },
{ name = "requests" },
{ name = "rich" },
{ name = "typer" },
]
[package.metadata]
requires-dist = [
{ name = "questionary" },
{ name = "requests" },
{ name = "rich" },
{ name = "typer" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
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" },
]
[[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]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
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" },
]
[[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]]
name = "urllib3"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
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" },
]
[[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" },
]