Compare commits

...
120 Commits
Author SHA1 Message Date
Jonathan Teran Carballo 6df7206eec bugfix: print errors in stderr 2026-09-21 10:45:16 -03:00
Jonathan Teran Carballo e793feaa1e bugfix: only return tag on remote tag command, empty if error 2026-09-21 10:44:48 -03:00
jt d03cf964e1 Merge pull request 'docs: fix workflow yaml' (#27) from chore/update-pipeline into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 12s
Reviewed-on: #27
2026-09-21 12:53:52 +00:00
Jonathan Teran Carballo 9d17cf0320 docs: fix workflow yaml 2026-09-21 09:53:28 -03:00
jt 8c95835207 Merge pull request 'docs: update pipeline workflow' (#26) from chore/update-pipeline into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 0s
Reviewed-on: #26
2026-09-21 12:49:27 +00:00
Jonathan Teran Carballo 7c69936360 docs: update pipeline workflow 2026-09-21 09:48:01 -03:00
gitea-actions 4cf6321b80 chore: bump version and update CHANGELOG.md [skip ci] 2026-09-21 12:45:33 +00:00
jt ae61fa4fa4 Merge pull request 'refactor: update remote tag fetching behaviour so it uses api instead of shallow clone' (#25) from refactor/remote-tag-fetching-behaviour into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 37s
Reviewed-on: #25
2026-09-21 12:45:04 +00:00
Jonathan Teran Carballo 22ba8f5e5f refactor: update remote tag fetching behaviour so it uses api instead of shallow clone 2026-09-21 09:15:48 -03:00
gitea-actions eabef5eb33 chore: bump version and update CHANGELOG.md [skip ci] 2026-09-21 00:12:35 +00:00
jt ff61238bfe Merge pull request 'feature: tag remote fetch commits from api' (#24) from feature/tag-remote-fetch-commits-from-api into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 41s
Reviewed-on: #24
2026-09-21 00:11:58 +00:00
Jonathan Teran Carballo c50688bb1a bugfix: fix parameters for changelog generate_content 2026-09-20 21:08:12 -03:00
Jonathan Teran Carballo 26ab81b175 feature: tag from ci fetches commits from remote api 2026-09-20 21:05:21 -03:00
gitea-actions 889109bf77 chore: bump version and update CHANGELOG.md [skip ci] 2026-09-18 14:27:33 +00:00
jt 164eded8f8 Merge pull request 'bugfix: muevo titulo de error al panel, agrego repositorio a gitlab error' (#23) from bugfix/add-debug-error-info-2 into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 18s
Reviewed-on: #23
2026-09-18 14:27:19 +00:00
Jonathan Teran ce1ab99ddc bugfix: muevo titulo de error al panel, agrego repositorio a gitlab error 2026-09-18 11:26:55 -03:00
gitea-actions 8f96cb21f6 chore: bump version and update CHANGELOG.md [skip ci] 2026-09-18 13:27:25 +00:00
jt 9fa7fa41bd Merge pull request 'bugfix: agrego informacion de traceback para errores de GitFlow' (#22) from bugfix/add-debug-error-info into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 38s
Reviewed-on: #22
2026-09-18 13:26:51 +00:00
Jonathan Teran 8a4cf6e667 bugfix: agrego informacion de traceback para errores de GitFlow 2026-09-18 10:25:52 -03:00
gitea-actions a502541e35 chore: bump version and update CHANGELOG.md [skip ci] 2026-09-17 14:52:53 +00:00
jt b6370648f9 Merge pull request 'bugfix: apply release prefix and ticket detection to single-commit PR titles' (#21) from bugfix/pr-title-single-commit into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 19s
Reviewed-on: #21
2026-09-17 14:52:37 +00:00
Jonathan Teran e0200e3695 bugfix: apply release prefix and ticket detection to single-commit PR titles 2026-09-17 11:36:36 -03:00
gitea-actions 40f3e6ba49 chore: bump version and update CHANGELOG.md [skip ci] 2026-09-17 13:58:06 +00:00
jt 77d9a224fa Merge pull request 'feature: downgrade python version for better compatibility' (#18) from feature/downgrade-python-version into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 1m2s
Reviewed-on: #18
2026-09-17 13:57:07 +00:00
Jonathan Teran 55172bda74 feature: downgrade python version for better compatibility 2026-09-17 10:56:34 -03:00
gitea-actions b8fff9e77a chore: bump version and update CHANGELOG.md [skip ci] 2026-09-15 19:53:13 +00:00
jt f6a7f1a987 Merge pull request 'bugfix: include group path in repo for gitlab support' (#17) from bugfix/gitlab-include-group-path into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 44s
Reviewed-on: #17
2026-09-15 19:52:35 +00:00
Jonathan Teran 3e448544f0 bugfix: include group path in repo for gitlab support 2026-09-15 16:50:45 -03:00
jt 75a77201d4 Merge pull request 'docs: actualizar README con secciones por remoto y ejemplos de pipeline' (#16) from worktree-kind-watching-hearth into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 29s
Reviewed-on: #16
2026-09-14 04:03:06 +00:00
jtandClaude Sonnet 5 e8c0c0b777 docs: actualizar README con secciones por remoto y ejemplos de pipeline
Documenta como generar tokens de acceso para Github, Gitlab, Bitbucket y
Gitea, y agrega ejemplos de pipeline para taggeo automatico en cada uno.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 23:18:47 -03:00
gitea-actions d4dc694db6 chore: bump version and update CHANGELOG.md [skip ci] 2026-09-14 00:23:39 +00:00
jt 9120698900 Merge pull request 'refactor: remove release prompts' (#15) from refactor/remove-release-prompts into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 17s
Reviewed-on: #15
2026-09-14 00:23:28 +00:00
jt c035a8867e refactor: cambiar incremento de commits refactor y perf 2026-09-13 21:22:41 -03:00
jt 0c31c747ff refactor: eliminar preguntas innecesarias de release y merge 2026-09-13 21:22:14 -03:00
gitea-actions 12ce68d954 chore: bump version and update CHANGELOG.md [skip ci] 2026-09-13 23:56:52 +00:00
jt 29eacfdc4d Merge pull request 'feature: add gitlab remote support' (#14) from feature/gitlab-remote-support into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 2m40s
Reviewed-on: #14
2026-09-13 23:54:15 +00:00
jt 748d4201cc feature: add gitlab remote support 2026-09-13 20:48:49 -03:00
gitea-actions 94a219ed82 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-31 15:25:28 +00:00
jt 3f3650360a Merge pull request 'Feature/parse git remote url with library' (#12) from feature/parse-git-remote-url-with-library into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 55s
Reviewed-on: #12
2026-07-31 15:24:35 +00:00
Jonathan Teran 11a48a922c style: format with ruff 2026-07-31 12:22:39 -03:00
Jonathan Teran f4adb431db feature: parse git remote url with giturlparse 2026-07-31 12:22:14 -03:00
gitea-actions 8f1a60cc06 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-29 14:56:49 +00:00
jt fcc8114c8e Merge pull request 'bugfix: change checkout version to v7' (#11) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Successful in 16s
Reviewed-on: #11
2026-07-29 14:56:38 +00:00
Jonathan Teran Carballo c58c4cea96 bugfix: change checkout version to v7 2026-07-29 11:55:54 -03:00
jt 7fbf499278 Merge pull request 'bugfix: move git fetch to separate line' (#10) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 8s
Reviewed-on: #10
2026-07-29 14:49:33 +00:00
Jonathan Teran Carballo 64fc890cb7 bugfix: move git fetch to separate line 2026-07-29 11:49:25 -03:00
gitea-actions 7199249f75 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-29 14:47:56 +00:00
jt 7efaf367e5 Merge pull request 'bugfix: change actions/checkout to v4 and add step to fetch tags' (#9) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 17s
Reviewed-on: #9
2026-07-29 14:47:38 +00:00
Jonathan Teran Carballo dc79ffa399 bugfix: change actions/checkout to v4 and add step to fetch tags 2026-07-29 11:47:00 -03:00
jt 31513a3e3a Merge pull request 'bugfix: change actions/checkout version' (#8) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 19s
Reviewed-on: #8
2026-07-29 14:45:20 +00:00
Jonathan Teran Carballo c45f065e77 bugfix: change actions/checkout version 2026-07-29 11:45:12 -03:00
jt 7f7dd7b538 Merge pull request 'bugfix: add fetch-tags: true' (#7) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 14s
Reviewed-on: #7
2026-07-29 14:41:06 +00:00
Jonathan Teran Carballo adcb53156a bugfix: add fetch-tags: true 2026-07-29 11:40:54 -03:00
jt 5ca817c796 Merge pull request 'bugfix: remove checkout parameters' (#6) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 14s
Reviewed-on: #6
2026-07-29 14:35:04 +00:00
Jonathan Teran Carballo 2be9b50ac4 bugfix: remove checkout parameters 2026-07-29 11:34:43 -03:00
gitea-actions 347b8b9556 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-29 13:57:37 +00:00
jt 9b547bd6a9 Merge pull request 'feature: change flow settings in local config' (#5) from feature/change-flow-settings-in-local-config into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 29s
Reviewed-on: #5
2026-07-29 13:57:06 +00:00
Jonathan Teran Carballo f05582b50f feature: show commit count and list tags on workflow 2026-07-29 10:53:56 -03:00
gitea-actions a1a9a7acac chore: bump version and update CHANGELOG.md [skip ci] 2026-07-18 03:16:04 +00:00
jt 190b4b8408 Merge pull request 'bugfix: add fetch tags to checkout step' (#4) from bugfix/fetch-tags-on-checkout into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 11s
Reviewed-on: #4
2026-07-18 03:15:50 +00:00
jt f9fb5a62d1 bugfix: add fetch tags to checkout step 2026-07-18 00:15:19 -03:00
gitea-actions 09d0de2860 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-18 01:35:18 +00:00
jt f233adfd1d Merge pull request 'feature: print current tag command output' (#3) from feature/log-current-tag-command into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 10s
Reviewed-on: #3
2026-07-18 01:35:03 +00:00
jt 0cdc01291c feature: print current tag command output 2026-07-17 22:34:39 -03:00
gitea-actions 098a4724ca chore: bump version and update CHANGELOG.md [skip ci] 2026-07-18 01:13:08 +00:00
jt 9167390098 Merge pull request 'feature: update I/O methods to use questionary where applicable' (#2) from feature/refactor-use-questionary-package into main
Publish to PyPI / Publish-to-PyPI (push) Failing after 43s
Reviewed-on: #2
2026-07-18 01:12:22 +00:00
jt a08666cac3 style: organize imports 2026-07-17 22:07:37 -03:00
jt 726df217ec style: format and organize imports 2026-07-17 22:04:56 -03:00
jt 07d4ff960e refactor: use base.io_ methods 2026-07-17 22:01:42 -03:00
jt 919e680e75 refactor: use base.io_ methods 2026-07-16 22:26:48 -03:00
jt add4cd2747 refactor: use base.io_ methods 2026-07-16 22:25:20 -03:00
jt e4833c9ed9 style: run black formatter on merge.py 2026-07-16 22:12:30 -03:00
jt 7315681b23 style: run black formatter on commit.py 2026-07-16 22:11:35 -03:00
jt ba1c9bd68d style: run black formatter on new.py 2026-07-16 22:11:07 -03:00
jt 1a7ae338ff style: run black formatter 2026-07-16 22:09:28 -03:00
jt 0120faac9e refactor: use new base.io_ methods 2026-07-16 22:08:46 -03:00
jt febd18c3b3 refactor: use unsafe_ask in favor of global error catch 2026-07-16 22:02:47 -03:00
jt 069c33bc17 style: run black formatter 2026-07-16 21:48:28 -03:00
jt 1d186e117c refactor: reword current branch check, move if around so it's better placed 2026-07-16 21:47:27 -03:00
jt c0f7642687 feature: change unicode status icons 2026-07-16 21:35:41 -03:00
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
25 changed files with 1315 additions and 351 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
+33
View File
@@ -0,0 +1,33 @@
name: Publish to PyPI
on:
push:
branches:
- main
jobs:
Publish-to-PyPI:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- 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 git-flow and bump version
run: |
pip install git-flow-envs=="${{ vars.GITFLOWENVS_VERSION }}"
git-flow tag --token="${{ secrets.GITEA_TOKEN }}"
- name: Build and publish
run: |
uv build
uv publish --token="${{ secrets.PYPI_TOKEN }}"
+1 -1
View File
@@ -1 +1 @@
3.14 3.10
+192
View File
@@ -1,3 +1,195 @@
## v1.22.1 - 2026-09-21
- **refactor**: update remote tag fetching behaviour so it uses api instead of shallow clone [[jteran](mailto:jteran@renatre.org.ar)] (22ba8f5)
---
## v1.22.0 - 2026-09-21
- **feature**: tag from ci fetches commits from remote api [[jteran](mailto:jteran@renatre.org.ar)] (26ab81b)
- **bugfix**: fix parameters for changelog generate_content [[jteran](mailto:jteran@renatre.org.ar)] (c50688b)
---
## v1.21.3 - 2026-09-18
- **bugfix**: muevo titulo de error al panel, agrego repositorio a gitlab error [[jteran](mailto:jteran@renatre.org.ar)] (ce1ab99)
---
## v1.21.2 - 2026-09-18
- **bugfix**: agrego informacion de traceback para errores de GitFlow [[jteran](mailto:jteran@renatre.org.ar)] (8a4cf6e)
---
## v1.21.1 - 2026-09-17
- **bugfix**: apply release prefix and ticket detection to single-commit PR titles [[jteran](mailto:jteran@renatre.org.ar)] (e0200e3)
---
## v1.21.0 - 2026-09-17
- **feature**: downgrade python version for better compatibility [[jteran](mailto:jteran@renatre.org.ar)] (55172bd)
---
## v1.20.2 - 2026-09-15
- **bugfix**: include group path in repo for gitlab support [[jteran](mailto:jteran@renatre.org.ar)] (3e44854)
---
## v1.20.1 - 2026-09-14
- **refactor**: cambiar incremento de commits refactor y perf [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (c035a88)
- **refactor**: eliminar preguntas innecesarias de release y merge [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (0c31c74)
---
## v1.20.0 - 2026-09-13
- **feature**: add gitlab remote support [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (748d420)
---
## v1.19.0 - 2026-07-31
- **style**: format with ruff [[jteran](mailto:jteran@renatre.org.ar)] (11a48a9)
- **feature**: parse git remote url with giturlparse [[jteran](mailto:jteran@renatre.org.ar)] (f4adb43)
---
## v1.18.0 - 2026-07-29
- **bugfix**: change checkout version to v7 [[jteran](mailto:jteran@renatre.org.ar)] (c58c4ce)
- **bugfix**: move git fetch to separate line [[jteran](mailto:jteran@renatre.org.ar)] (64fc890)
- **bugfix**: change actions/checkout to v4 and add step to fetch tags [[jteran](mailto:jteran@renatre.org.ar)] (dc79ffa)
- **bugfix**: change actions/checkout version [[jteran](mailto:jteran@renatre.org.ar)] (c45f065)
- **bugfix**: add fetch-tags: true [[jteran](mailto:jteran@renatre.org.ar)] (adcb531)
- **bugfix**: remove checkout parameters [[jteran](mailto:jteran@renatre.org.ar)] (2be9b50)
- **feature**: show commit count and list tags on workflow [[jteran](mailto:jteran@renatre.org.ar)] (f05582b)
---
## v0.1.0-main - 2026-07-29
- **bugfix**: change actions/checkout to v4 and add step to fetch tags [[jteran](mailto:jteran@renatre.org.ar)] (dc79ffa)
- **bugfix**: change actions/checkout version [[jteran](mailto:jteran@renatre.org.ar)] (c45f065)
- **bugfix**: add fetch-tags: true [[jteran](mailto:jteran@renatre.org.ar)] (adcb531)
- **bugfix**: remove checkout parameters [[jteran](mailto:jteran@renatre.org.ar)] (2be9b50)
- **feature**: show commit count and list tags on workflow [[jteran](mailto:jteran@renatre.org.ar)] (f05582b)
---
## v0.1.0-main - 2026-07-29
- **feature**: show commit count and list tags on workflow [[jteran](mailto:jteran@renatre.org.ar)] (f05582b)
---
## v0.0.1-main - 2026-07-18
- **bugfix**: add fetch tags to checkout step [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (f9fb5a6)
---
## v0.1.0-main - 2026-07-18
- **feature**: print current tag command output [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (0cdc012)
---
## v0.1.0-main - 2026-07-18
- **style**: organize imports [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (a08666c)
- **style**: format and organize imports [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (726df21)
- **refactor**: use base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (07d4ff9)
- **refactor**: use base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (919e680)
- **refactor**: use base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (add4cd2)
- **style**: run black formatter on merge.py [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e4833c9)
- **style**: run black formatter on commit.py [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (7315681)
- **style**: run black formatter on new.py [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ba1c9bd)
- **style**: run black formatter [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1a7ae33)
- **refactor**: use new base.io_ methods [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (0120faa)
- **refactor**: use unsafe_ask in favor of global error catch [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (febd18c)
- **style**: run black formatter [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (069c33b)
- **refactor**: reword current branch check, move if around so it's better placed [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1d186e1)
- **feature**: change unicode status icons [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (c0f7642)
---
## v1.17.6 - 2026-07-09
- **bugfix**: actualizo versiones de checkout, setup-python, clonar depth completo [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (4666503)
---
## v0.1.0-main - 2026-07-09
- **bugfix**: usar https por defecto como esquema si se detecta ssh [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (5989699)
- **feature**: usar esquema de remoto para host de api gitea [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ecfe058)
- **bugfix**: agregar setup previo a ejecucion de git tag [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1cc2de0)
- **bugfix**: arreglo acceso a secretos [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d456565)
- **bugfix**: agrego fetch-depth a checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ad8984c)
- **bugfix**: agrego log de git luego del checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (bf6a0e8)
- **bugfix**: cambio version de action setup-uv [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (cb74242)
- **feature**: agrego workflow para subir paquete a pypi [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e94ee2c)
---
## v0.1.0-main - 2026-07-09
- **bugfix**: agregar setup previo a ejecucion de git tag [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1cc2de0)
- **bugfix**: arreglo acceso a secretos [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d456565)
- **bugfix**: agrego fetch-depth a checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ad8984c)
- **bugfix**: agrego log de git luego del checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (bf6a0e8)
- **bugfix**: cambio version de action setup-uv [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (cb74242)
- **feature**: agrego workflow para subir paquete a pypi [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e94ee2c)
---
## v1.17.5 - 2026-06-03
- **bugfix**: allow commits not compliant with conventional commits in changelog generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (3ff9f76)
---
## v1.17.4 - 2026-05-24
- **bugfix**: use rest of branch name instead of ticket for pr title generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (094bc1a)
---
## v1.17.3 - 2026-05-22
- **bugfix**: dont close branches on merges between environments [[jteran](mailto:jteran@renatre.org.ar)] (8ee0129)
---
## v1.17.2 - 2026-05-22 ## v1.17.2 - 2026-05-22
- **bugfix**: change pr title on merges between environments [[jteran](mailto:jteran@renatre.org.ar)] (de124ba) - **bugfix**: change pr title on merges between environments [[jteran](mailto:jteran@renatre.org.ar)] (de124ba)
+154 -12
View File
@@ -15,7 +15,7 @@ carpeta local, y lo instalamos:
```sh ```sh
cd ~/Downloads cd ~/Downloads
git clone git@bitbucket.org:sushi2717/git-flow.git git clone ssh://git@git.jonathanteran.dev:59169/jt/git-flow.git
cd git-flow cd git-flow
pipx install . pipx install .
``` ```
@@ -47,25 +47,29 @@ 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
- [x] Gitlab
La configuración del remoto requiere tener un Access Token para poder crear los PRs automáticamente. La configuración del remoto requiere tener un Access Token para poder crear los PRs y tags
El mismo se puede crear desde el repositorio al que se quiere dar acceso: *"Repository settings" > automáticamente. Ver la sección [Remotos soportados](#remotos-soportados) para las instrucciones
"Access Tokens" > "Create access token"*. El token necesita permiso para *Write* en *Pull Requests*. de cómo generar el token en cada proveedor.
Luego se debe guardar el token dentro del repositorio a configurar, en el archivo Una vez generado, el token se debe guardar 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.
## Utilización ## Utilización
Git Flow cuenta con 5 comandos principales para guiar el workflow (ver `git-flow <command> -h` para Git Flow cuenta con los siguientes comandos para guiar el workflow (ver `git-flow <command> -h` para
más información): más información):
- `init`: Inicializa el repositorio para utilizar git-flow (ramas principales y remoto).
- `new`: Crea una nueva rama siguiendo CB. - `new`: Crea una nueva rama siguiendo CB.
- `commit`: Crea un commit siguiendo CC. - `commit`: Crea un commit siguiendo CC.
- `merge`: Mergea la rama actual a una de las ramas principales. - `merge`: Mergea la rama actual a una de las ramas principales.
- `tag`: Crea un tag sobre el ultimo merge siguiendo SemVer. - `tag`: Crea un tag sobre el ultimo merge siguiendo SemVer.
- `release`: Prepara la rama actual para pasarse al siguiente entorno configurado - `release`: Prepara la rama actual para pasarse al siguiente entorno configurado.
- `branch`: Lista las ramas del repositorio, agrupadas por entorno objetivo.
El workflow para el cual se penso la herramienta es el siguiente: El workflow para el cual se penso la herramienta es el siguiente:
@@ -84,9 +88,147 @@ El workflow para el cual se penso la herramienta es el siguiente:
`feature/my-new-feature`, se crea la rama `release/test/feature/my-new-feature` sobre `test` que `feature/my-new-feature`, se crea la rama `release/test/feature/my-new-feature` sobre `test` que
tiene los cambios de la rama original. tiene los cambios de la rama original.
En cualquier momento se puede usar `git-flow branch` para ver en qué entorno está cada rama, y cuáles
están todavía en progreso (`wip/`) o descartadas (`trash/`).
## Remotos soportados
Cada remoto necesita un Access Token con permisos para crear Pull/Merge Requests y tags. El token se
guarda en el archivo `.repository-token` en la raíz del repositorio (una única línea con el valor del
token), o se pasa por parámetro con `--token=<TOKEN>` al comando `git-flow tag` (útil para no
commitear el token y usarlo solo desde un pipeline mediante una variable secreta).
### Github
1. Ir a *Settings* del repositorio (o de la organización) > *Developer settings* > *Personal access
tokens* > *Fine-grained tokens* > *Generate new token*.
2. Seleccionar el repositorio sobre el que se va a usar `git-flow`.
3. En *Repository permissions*, otorgar:
- **Contents**: Read and write (necesario para crear tags).
- **Pull requests**: Read and write (necesario para crear PRs).
4. Generar el token y guardarlo en `.repository-token`.
Ejemplo de pipeline (GitHub Actions) para taggear automáticamente en cada push a una rama principal:
```yaml
# .github/workflows/tag.yml
name: Tag and publish
on:
push:
branches: [main]
jobs:
tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v3
- run: uv run git-flow tag --token=${{ secrets.GIT_FLOW_TOKEN }}
- run: uv build
- run: uv publish --token=${{ secrets.PYPI_TOKEN }}
```
### Gitlab
1. Ir a *Settings* > *Access Tokens* del proyecto (o *Group access tokens* si se quiere usar a nivel
grupo).
2. Crear un token con rol **Developer** o superior, y los scopes:
- **api** (o al menos `write_repository` para tags y `api` para Merge Requests).
3. Generar el token y guardarlo en `.repository-token`.
Ejemplo de pipeline (GitLab CI) para taggear automáticamente en cada push a una rama principal:
```yaml
# .gitlab-ci.yml
tag:
image: python:3.14
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- pip install uv
- uv run git-flow tag --token=$GIT_FLOW_TOKEN
- uv build
- uv publish --token=$PYPI_TOKEN
```
El token debe guardarse como variable protegida y enmascarada (`GIT_FLOW_TOKEN`) en *Settings* >
*CI/CD* > *Variables*.
### Bitbucket
1. Ir a *Repository settings* > *Access Tokens* > *Create access token* (o *Workspace settings* >
*Access Tokens* para un token a nivel workspace).
2. Otorgar permiso de **Write** en *Pull requests* y *Repositories* (necesario para crear tags).
3. Generar el token y guardarlo en `.repository-token`.
Este mismo repositorio usa Git Flow para el taggeo automático usando Bitbucket Pipelines, se puede
ver el archivo `bitbucket-pipelines.yml` como ejemplo:
```yaml
# bitbucket-pipelines.yml
image: python:3.14
pipelines:
branches:
'main':
- step:
name: "Tag and publish package"
caches:
- pip
script:
- pip install uv
- uv run git-flow tag --token=$BEARER
- uv build
- uv publish --token=$PYPI_TOKEN
```
El token se configura como variable segura del repositorio (`BEARER`) en *Repository settings* >
*Repository variables*.
### Gitea
1. Ir a *Settings* del usuario (o de la organización) > *Applications* > *Manage Access Tokens* >
*Generate New Token*.
2. Otorgar permiso de escritura sobre:
- **repository** (necesario para crear tags).
- **issue** (necesario para crear PRs, ya que Gitea expone los PRs bajo el mismo scope).
3. Generar el token y guardarlo en `.repository-token`.
Ejemplo de pipeline (Gitea Actions) para taggear automáticamente en cada push a una rama principal:
```yaml
# .gitea/workflows/tag.yml
name: Tag and publish
on:
push:
branches: [main]
jobs:
tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v3
- run: uv run git-flow tag --token=${{ secrets.GIT_FLOW_TOKEN }}
- run: uv build
- run: uv publish --token=${{ secrets.PYPI_TOKEN }}
```
## Taggeo automático por pipeline ## Taggeo automático por pipeline
Éste repositorio utiliza Git Flow para el taggeo automático usando pipelines, se puede ver el En todos los casos, `git-flow tag` detecta que se está ejecutando desde un pipeline (no encuentra
archivo `bitbucket-pipelines.yml` como ejemplo. El comando `git-flow tag` soporta el pasaje del `.repository-token` en el repositorio) y usa el token pasado por `--token=<TOKEN>` tanto para crear
token mediante la opción `--token=<TOKEN>`. Ver la sección [Configuración](#configuracion) para las el tag en el remoto como para pushear el commit del `CHANGELOG.md` generado, en vez de crear el tag
instrucciones de como generar este token. El mismo necesita scope `write:repository:bitbucket`. únicamente en forma local como sucede al ejecutarlo manualmente sin remoto configurado.
+1 -1
View File
@@ -1,4 +1,4 @@
image: python:3.14 image: python:3.10
pipelines: pipelines:
branches: branches:
+2 -2
View File
@@ -6,9 +6,9 @@ build-backend = "setuptools.build_meta"
name = "git-flow-envs" name = "git-flow-envs"
description = "Git workflow automation to follow best practices when working with multiple environments" description = "Git workflow automation to follow best practices when working with multiple environments"
dynamic = ["version"] dynamic = ["version"]
requires-python = ">= 3.14" requires-python = ">= 3.10"
dependencies = [ dependencies = [
"requests", "typer" "requests", "typer", "rich", "questionary", "giturlparse"
] ]
authors = [ authors = [
{name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"}, {name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"},
+8 -5
View File
@@ -36,18 +36,21 @@ COMMIT_TYPE_INCREMENT = {
"feature": SEMVER_MINOR, "feature": SEMVER_MINOR,
"bugfix": SEMVER_PATCH, "bugfix": SEMVER_PATCH,
"hotfix": SEMVER_PATCH, "hotfix": SEMVER_PATCH,
"refactor": SEMVER_SKIP, "refactor": SEMVER_PATCH,
"perf": SEMVER_SKIP, "perf": SEMVER_PATCH,
"docs": SEMVER_SKIP, "docs": SEMVER_SKIP,
"style": SEMVER_SKIP, "style": SEMVER_SKIP,
"test": SEMVER_SKIP, "test": SEMVER_SKIP,
} }
SUPPORTED_REMOTE_APIS = [ SUPPORTED_REMOTE_URL_SCHEMAS = [
"bitbucket.org", "ssh",
"github.com", "http",
"https"
] ]
REPOSITORY_TOKEN_FILENAME = ".repository-token" REPOSITORY_TOKEN_FILENAME = ".repository-token"
FLOWCONFIG_FILENAME = ".flowconfig" FLOWCONFIG_FILENAME = ".flowconfig"
FLOWCONFIG_VERSION = 2
+17 -16
View File
@@ -1,15 +1,15 @@
from datetime import datetime from datetime import datetime
from os.path import isfile from os.path import isfile
from git_flow.git import Git from git_flow.git import Commit
class Changelog: class Changelog:
FILENAME = "CHANGELOG.md" FILENAME = "CHANGELOG.md"
COMMIT_MESSAGE = "chore: bump version and update CHANGELOG.md [skip ci]" COMMIT_MESSAGE = "chore: bump version and update CHANGELOG.md [skip ci]"
def update(self, title: str, base: str, branch: str): def update(self, title: str, commits: list[Commit]):
self._prepend(self.generate_entry(title, base, branch)) self._prepend(self.generate_entry(title, commits))
def generate_header(self, title): def generate_header(self, title):
now = datetime.now() now = datetime.now()
@@ -18,21 +18,22 @@ class Changelog:
""" """
def generate_content(self, base: str, branch: str): def generate_content(self, commits: list[Commit]):
commits = Git(
"log", base + ".." + branch, first_parent=True, format="%h", merges=False
).lines()
content = "" content = ""
for commit in commits: for commit in commits:
message = Git("show", commit, patch=False, format="%s").firstline() message = commit.subject
email = Git("show", commit, patch=False, format="%ae").firstline() email = commit.email
username = email[: email.index("@")] username = email[: email.index("@")] if "@" in email else email
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.sha})\n"
return content return content
@@ -43,10 +44,10 @@ class Changelog:
""" """
def generate_entry(self, title: str, base: str, branch: str): def generate_entry(self, title: str, commits: list[Commit]):
return ( return (
self.generate_header(title) self.generate_header(title)
+ self.generate_content(base, branch) + self.generate_content(commits)
+ self.generate_footer() + self.generate_footer()
) )
+119 -17
View File
@@ -1,24 +1,32 @@
import sys
from git_flow import ( from git_flow import (
BRANCH_TYPES, BRANCH_TYPES,
REPOSITORY_TOKEN_FILENAME, REPOSITORY_TOKEN_FILENAME,
FLOWCONFIG_FILENAME, FLOWCONFIG_FILENAME,
FLOWCONFIG_VERSION,
COMMIT_TYPES,
GitFlowError, GitFlowError,
) )
from git_flow.git import Git from git_flow.git import Git
from git_flow.remote.base import RemoteAPI from git_flow.remote.base import RemoteAPI
from git_flow.remote.bitbucket import BitbucketRemoteAPI from git_flow.remote.bitbucket import BitbucketRemoteAPI
from git_flow.remote.github import GithubRemoteAPI from git_flow.remote.github import GithubRemoteAPI
from git_flow.remote.gitea import GiteaRemoteAPI
from git_flow.remote.gitlab import GitlabRemoteAPI
from git_flow.io import * from git_flow.io import *
from os.path import isfile from os.path import isfile
TYPE_SUCCESS = 0 from typing import Optional
TYPE_WARNING = 1 import rich
TYPE_ERROR = 2 import rich.panel
TYPE_INFO = 3 import questionary
flowconfig = Git.get_config(FLOWCONFIG_FILENAME) if isfile(FLOWCONFIG_FILENAME) else {} flowconfig = Git.get_config(FLOWCONFIG_FILENAME) if isfile(FLOWCONFIG_FILENAME) else {}
environments = flowconfig["flow.branches"].split(",") if "flow.branches" in flowconfig else [] environments = (
flowconfig["flow.branches"].split(",") if "flow.branches" in flowconfig else []
)
def ensure_initialized(): def ensure_initialized():
initialized = flowconfig["flow.initialized"] if flowconfig else None initialized = flowconfig["flow.initialized"] if flowconfig else None
@@ -32,13 +40,21 @@ def ensure_initialized():
f"El valor de `flow.initialized` ({initialized}) es inválido." f"El valor de `flow.initialized` ({initialized}) es inválido."
) )
version = int(flowconfig.get("flow.version", "1"))
if version != FLOWCONFIG_VERSION:
raise GitFlowError(
f"La versión del flowconfig ({version}) no es compatible con esta versión de git-flow ({FLOWCONFIG_VERSION}). "
f"Debe reinicializar el repositorio."
)
def ensure_right_branch(): def ensure_right_branch():
branch = Git.get_current_branch() branch = Git.get_current_branch()
if branch == "HEAD": if branch == "HEAD":
raise GitFlowError("No se encuentra parado sobre una rama.") raise GitFlowError("No se encuentra parado sobre una rama.")
elif confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"): elif io_confirm(f"Rama actual: {branch}. ¿Es correcto?"):
return branch return branch
else: else:
raise GitFlowError("Ejecución cancelada.") raise GitFlowError("Ejecución cancelada.")
@@ -92,26 +108,34 @@ def ensure_clean_worktree(has_remote: bool):
if not_empty: if not_empty:
if not has_remote: if not has_remote:
warning("Existen cambios en tu entorno de trabajo sin commitear.") io_warning("Existen cambios en tu entorno de trabajo sin commitear.")
if not io_confirm("¿Desea continuar?", False):
raise GitFlowError("Ejecución abortada")
else: else:
raise GitFlowError("No se puede continuar con cambios pendientes.") raise GitFlowError("No se puede continuar con cambios pendientes.")
if not confirm("¿Desea continuar?", False):
raise GitFlowError("Ejecución abortada")
def get_remote_api(token: str) -> RemoteAPI: def get_remote_api(token: str) -> RemoteAPI:
if "flow.remote" not in flowconfig: if "flow.remote" not in flowconfig:
raise GitFlowError("El repositorio no tiene configurado un remoto.") raise GitFlowError("El repositorio no tiene configurado un remoto.")
[host, repository] = RemoteAPI.parse(flowconfig["flow.remote"]) remote_type = flowconfig.get("flow.remote-type", "").lower()
[schema, host, repository] = RemoteAPI.parse(flowconfig["flow.remote"])
if host == "bitbucket.org": if remote_type == "gitea":
remote_schema = "https://" if schema == "ssh://" else schema
return GiteaRemoteAPI(remote_schema + host, repository, token)
elif remote_type == "gitlab":
remote_schema = "https://" if schema == "ssh://" else schema
return GitlabRemoteAPI(remote_schema + host, repository, token)
elif remote_type == "bitbucket":
return BitbucketRemoteAPI(repository, token) return BitbucketRemoteAPI(repository, token)
elif host == "github.com": elif remote_type == "github":
return GithubRemoteAPI(repository, token) return GithubRemoteAPI(repository, token)
else: else:
raise GitFlowError("El host del repositorio remoto es inválido") raise GitFlowError("El tipo del remoto es inválido")
def is_valid_ticket(ticket: str) -> bool: def is_valid_ticket(ticket: str) -> bool:
"""Check if the specified string is a valid ticket number (ABC-123)""" """Check if the specified string is a valid ticket number (ABC-123)"""
@@ -119,8 +143,86 @@ def is_valid_ticket(ticket: str) -> bool:
if len(components) != 2: if len(components) != 2:
return False return False
ticket_project = components[0] ticket_project = components[0]
ticket_number = components[1] ticket_number = components[1]
return ticket_project.isalpha() and ticket_project.isupper() and ticket_number.isnumeric() return (
ticket_project.isalpha()
and ticket_project.isupper()
and ticket_number.isnumeric()
)
def get_commit_message(commit_types: list[str] = COMMIT_TYPES) -> str:
commit_type = io_choice("Tipo de commit", commit_types)
commit_message = io_prompt(
"Mensaje de commit",
validator=lambda s: 0 < len(s) and len(s) < 100,
instruction="100 caracteres máximo",
)
return commit_type + ": " + commit_message
def io_error(message: str, title: Optional[str] = None):
_io_status(message, "✖ Error: ", "red", title, file=sys.stderr)
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, file=None):
if title:
rich.print(
rich.panel.Panel(
message,
title=prefix + title,
style=style,
expand=False,
title_align="left",
),
file=file
)
else:
rich.print(f"[{style}]{prefix}{message}[/{style}]")
def io_confirm(question: str, default: bool = True) -> bool:
return questionary.confirm(question, default=default, auto_enter=False).unsafe_ask()
def io_choice(prompt: str, options: list[str]) -> str:
return questionary.select(
prompt,
options,
use_search_filter=True,
use_jk_keys=False,
instruction="Tipear o usar flechas",
).unsafe_ask()
def io_prompt(
message: str,
default: str = "",
persistent: bool = False,
strip: bool = False,
validator=None,
instruction: str | None = None,
) -> str:
if persistent and not validator:
validator = lambda s: len(s.strip()) > 0
value = questionary.text(
message, default, validate=validator, instruction=instruction
).unsafe_ask()
return value.strip() if strip else value
+20 -14
View File
@@ -1,16 +1,20 @@
from typing import Annotated
import typer
from rich.console import Console
import git_flow.command.base as base
from git_flow import ( from git_flow import (
TRASH_BRANCH_PREFIX, TRASH_BRANCH_PREFIX,
WIP_BRANCH_PREFIX, WIP_BRANCH_PREFIX,
GitFlowError, GitFlowError,
) )
from git_flow.command.base import *
from git_flow.git import Git from git_flow.git import Git
from typing import Annotated
import typer
BRANCH_FORMAT = "%(refname:short)" BRANCH_FORMAT = "%(refname:short)"
app = typer.Typer() app = typer.Typer()
console = Console()
@app.command() @app.command()
@@ -38,28 +42,28 @@ def branch(
] = False, ] = False,
): ):
"""Lista ramas del repositorio, agrupandolas por entorno objetivo""" """Lista ramas del repositorio, agrupandolas por entorno objetivo"""
ensure_initialized() base.ensure_initialized()
current_branch = Git.get_current_branch() current_branch = Git.get_current_branch()
if current_branch in environments: if current_branch in base.environments:
current_environment = current_branch current_environment = current_branch
else: else:
current_environment, _ = get_branch_env_and_type(current_branch) current_environment, _ = base.get_branch_env_and_type(current_branch)
if trash: if trash:
show_branches(TRASH_BRANCH_PREFIX, current_branch) show_branches(TRASH_BRANCH_PREFIX, current_branch)
elif wip: elif wip:
show_branches(WIP_BRANCH_PREFIX, current_branch) show_branches(WIP_BRANCH_PREFIX, current_branch)
elif all: elif all:
show_envs(environments, current_branch) show_envs(base.environments, current_branch)
show_all_branches(environments, current_branch) show_all_branches(base.environments, current_branch)
elif environment is None: elif environment is None:
show_envs(environments, current_branch) show_envs(base.environments, current_branch)
show_env_branches(current_environment, environments, current_branch) show_env_branches(current_environment, base.environments, current_branch)
elif environment in environments: elif environment in base.environments:
show_envs(environments, current_branch) show_envs(base.environments, current_branch)
show_env_branches(environment, environments, current_branch) show_env_branches(environment, base.environments, current_branch)
else: else:
raise GitFlowError(f"'{environment}' no es un entorno válido.") raise GitFlowError(f"'{environment}' no es un entorno válido.")
@@ -78,7 +82,9 @@ def show_branches(branch_prefix: str, current_branch: str):
def get_branches(branch_prefix: str): def get_branches(branch_prefix: str):
return Git("for-each-ref", "refs/heads/" + branch_prefix, format=BRANCH_FORMAT).lines() return Git(
"for-each-ref", "refs/heads/" + branch_prefix, format=BRANCH_FORMAT
).lines()
def show_all_branches(environments: list[str], current_branch: str): def show_all_branches(environments: list[str], current_branch: str):
+11 -12
View File
@@ -1,8 +1,10 @@
import typer
from datetime import datetime from datetime import datetime
from git_flow import COMMIT_TYPES, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
import typer
import git_flow.command.base as base
from git_flow import TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
from git_flow.git import Git from git_flow.git import Git
from git_flow.command.base import *
app = typer.Typer() app = typer.Typer()
@@ -10,21 +12,18 @@ app = typer.Typer()
@app.command() @app.command()
def commit(): def commit():
"""Crea un commit siguiendo el formato de Conventional Commits""" """Crea un commit siguiendo el formato de Conventional Commits"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
if not _has_files_staged(): if not _has_files_staged():
warning("No hay cambios en el indice para commitear.") base.io_warning("No hay cambios en el indice para commitear.")
if confirm("¿Desea agregar la carpeta actual?"): if base.io_confirm("¿Desea agregar la carpeta actual?"):
Git("add", ".").exec(print="Agregando cambios") Git("add", ".").exec(print="Agregando cambios")
else: else:
raise GitFlowError("Debe agregar algún cambio al indice para continuar.") raise GitFlowError("Debe agregar algún cambio al indice para continuar.")
commit_type = choice("Tipo de commit", COMMIT_TYPES) message = base.get_commit_message()
commit_message = prompt( commit_type = message[: message.index(":")]
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
)
message = commit_type + ": " + commit_message
if branch.startswith(WIP_BRANCH_PREFIX): if branch.startswith(WIP_BRANCH_PREFIX):
original_branch = branch.removeprefix(WIP_BRANCH_PREFIX) original_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
+34 -23
View File
@@ -1,8 +1,8 @@
import typer import typer
from git_flow import FLOWCONFIG_FILENAME, GitFlowError import git_flow.command.base as base
from git_flow import FLOWCONFIG_FILENAME, FLOWCONFIG_VERSION, GitFlowError
from git_flow.git import Git from git_flow.git import Git
from git_flow.command.base import *
app = typer.Typer() app = typer.Typer()
@@ -15,15 +15,17 @@ def init():
_ensure_not_already_initialized() _ensure_not_already_initialized()
branches = _setup_flow_branches() branches = _setup_flow_branches()
remote = _setup_flow_remote() remote, remote_type = _setup_flow_remote()
flowconfig = { flowconfig = {
"flow.version": str(FLOWCONFIG_VERSION),
"flow.initialized": "true", "flow.initialized": "true",
"flow.branches": ",".join(branches), "flow.branches": ",".join(branches),
} }
if remote: if remote and remote_type:
flowconfig["flow.remote"] = remote flowconfig["flow.remote"] = remote
flowconfig["flow.remote-type"] = remote_type
Git.set_config(flowconfig, FLOWCONFIG_FILENAME) Git.set_config(flowconfig, FLOWCONFIG_FILENAME)
Git("add", FLOWCONFIG_FILENAME).exec() Git("add", FLOWCONFIG_FILENAME).exec()
@@ -34,32 +36,34 @@ def init():
def _ensure_is_repository(): def _ensure_is_repository():
if not Git.is_repository(): if not Git.is_repository():
warning("El directorio actual no es un repositorio.") base.io_warning("El directorio actual no es un repositorio.")
if not confirm("¿Desea inicializarlo?"): if not base.io_confirm("¿Desea inicializarlo?"):
raise GitFlowError("No se puede continuar sin inicializar el repositorio") raise GitFlowError("No se puede continuar sin inicializar el repositorio")
Git("init").exec() Git("init").exec()
def _ensure_not_already_initialized(): def _ensure_not_already_initialized():
if not flowconfig: if not base.flowconfig:
return return
elif flowconfig.get("flow.initialized") == "true": elif base.flowconfig.get("flow.initialized") == "true":
raise GitFlowError("El repositorio ya fue inicializado para usar git-flow") raise GitFlowError("El repositorio ya fue inicializado para usar git-flow")
else: else:
raise GitFlowError("Valor de 'flow.initialized' es inválido") raise GitFlowError("Valor de 'flow.initialized' es inválido")
def _setup_flow_branches(): def _setup_flow_branches():
panel( base.io_info(
"Configuración de entornos",
"""Ingrese las ramas que representan los entornos de deploy del proyecto """Ingrese las ramas que representan los entornos de deploy del proyecto
en orden creciente de cercanía al entorno productivo, y separados por en orden creciente de cercanía al entorno productivo, y separados por
coma. Por ejemplo: "dev, test, prod".""", coma. Por ejemplo: "dev, test, prod".""",
title="Configuración de entornos",
) )
branches = list(map(lambda b: b.strip(), prompt("Ramas", "main").split(","))) branches = list(
map(lambda b: b.strip(), base.io_prompt("Ramas", "main").split(","))
)
if not all(branches): if not all(branches):
raise GitFlowError("No puede ingresar una rama vacia") raise GitFlowError("No puede ingresar una rama vacia")
@@ -68,20 +72,23 @@ coma. Por ejemplo: "dev, test, prod".""",
def _setup_flow_remote(): def _setup_flow_remote():
panel( base.io_info(
"Configuración de remoto", """Configurar un repositorio remoto le permite generar PRs automáticamente para el
"Puede configurar un repositorio remoto para generar PRs automáticamente", entorno correspondiente. Deberá seleccionar uno de los remotos actuales, o crear
uno nuevo.""",
title="Configuración de remoto",
) )
remote = None remote = None
remote_type = None
if confirm("¿Configurar repositorio remoto?"): if base.io_confirm("¿Configurar repositorio remoto?"):
remotes = Git("remote").lines(print="Listando remotos disponibles") remotes = Git("remote").lines(print="Listando remotos disponibles")
if not remotes: if not remotes:
info("No tiene ningún repositorio remoto, se creará uno.") base.io_info("No tiene ningún repositorio remoto, se creará uno.")
url = prompt( url = base.io_prompt(
"Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)", "Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)",
persistent=True, persistent=True,
) )
@@ -90,20 +97,24 @@ def _setup_flow_remote():
elif len(remotes) == 1: elif len(remotes) == 1:
remote = remotes[0] remote = remotes[0]
else: else:
info("Tiene más de un remoto, seleccione el que va a utilizar.") base.io_info("Tiene más de un remoto, seleccione el que va a utilizar.")
remote = choice("Remoto", remotes) remote = base.io_choice("Remoto", remotes)
return remote remote_type = base.io_choice(
"Tipo de remoto:", ["bitbucket", "github", "gitea", "gitlab"]
)
return (remote, remote_type)
def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None): def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None):
existing_branches = Git.get_branches() existing_branches = Git.get_branches()
if not all(map(lambda b: b in existing_branches, branches)): if not all(map(lambda b: b in existing_branches, branches)):
warning("Algunas de las ramas de entornos expecificadas no existen.") base.io_warning("Algunas de las ramas de entornos expecificadas no existen.")
info("Debe indicar sobre que rama se crearán las ramas de entorno.") base.io_info("Debe indicar sobre que rama se crearán las ramas de entorno.")
target_branch = choice("Rama", existing_branches) target_branch = base.io_choice("Rama", existing_branches)
for branch in branches: for branch in branches:
if branch not in existing_branches: if branch not in existing_branches:
+65 -48
View File
@@ -1,7 +1,10 @@
from typing import Optional
import typer import typer
import git_flow.command.base as base
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.changelog import Changelog from git_flow.changelog import Changelog
from git_flow.command.base import *
from git_flow.git import Git from git_flow.git import Git
app = typer.Typer() app = typer.Typer()
@@ -10,36 +13,36 @@ app = typer.Typer()
@app.command() @app.command()
def merge(): def merge():
"""Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado""" """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
if branch in environments: if branch in base.environments:
if branch == environments[-1]: if branch == base.environments[-1]:
raise GitFlowError("No se puede hacer un merge del último entorno.") raise GitFlowError("No se puede hacer un merge del último entorno.")
panel( base.io_info(
"Merge de entornos",
"""Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""", """Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""",
"Merge de entornos",
) )
if not confirm("¿Desea continuar?"): if not base.io_confirm("¿Desea continuar?"):
raise GitFlowError("Ejecución abortada") raise GitFlowError("Ejecución abortada")
target = environments[environments.index(branch) + 1] target = base.environments[base.environments.index(branch) + 1]
else: else:
target, _ = get_branch_env_and_type(branch) target, _ = base.get_branch_env_and_type(branch)
if "flow.remote" in flowconfig: if "flow.remote" in base.flowconfig:
remote = flowconfig["flow.remote"] remote = base.flowconfig["flow.remote"]
token = ensure_repository_token() token = base.ensure_repository_token()
run_remote(remote, token, branch, target) run_remote(remote, token, branch, target)
else: else:
run_local(branch, target) run_local(branch, target)
def run_remote(remote: str, token: str, branch: str, target: str): def run_remote(remote: str, token: str, branch: str, target: str):
ensure_clean_worktree(True) base.ensure_clean_worktree(True)
Git("switch", target).exec(print="Cambiando a rama destino") Git("switch", target).exec(print="Cambiando a rama destino")
Git("pull").exec(print="Obteniendo ultimos cambios del remoto") Git("pull").exec(print="Obteniendo ultimos cambios del remoto")
@@ -48,8 +51,8 @@ def run_remote(remote: str, token: str, branch: str, target: str):
check_merge_conflicts(target) check_merge_conflicts(target)
show_commits_to_merge(target) show_commits_to_merge(target)
if confirm(f"¿Crear PR de '{branch}' a '{target}'?"): if base.io_confirm(f"¿Crear PR de '{branch}' a '{target}'?"):
if branch not in environments: 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"
) )
@@ -58,11 +61,11 @@ def run_remote(remote: str, token: str, branch: str, target: str):
def run_local(branch: str, target: str): def run_local(branch: str, target: str):
ensure_clean_worktree(False) base.ensure_clean_worktree(False)
check_merge_conflicts(target) check_merge_conflicts(target)
show_commits_to_merge(target) show_commits_to_merge(target)
if confirm(f"¿Mergear rama '{branch}' a '{target}'?"): if base.io_confirm(f"¿Mergear rama '{branch}' a '{target}'?"):
Git("switch", target).exec(print="Cambiando a rama destino") Git("switch", target).exec(print="Cambiando a rama destino")
Git("merge", branch, ff=False).exec(print="Mergeando") Git("merge", branch, ff=False).exec(print="Mergeando")
@@ -87,61 +90,75 @@ def check_merge_conflicts(target: str):
Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False) Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False)
if merge_conflicts: if merge_conflicts:
error(f"La rama actual tiene conflictos con {target}") base.io_error(
info( f"La rama actual tiene conflictos con {target}.\n"
f"""Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente, y ejecutar nuevamente este comando""" f"Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente,\n"
"y ejecutar nuevamente este comando",
title="Conflictos de merge",
) )
raise GitFlowError("Ejecución abortada.") raise GitFlowError("Ejecución abortada.")
else: else:
success("No se detectaron merge conflicts.") base.io_success("No se detectaron merge conflicts.")
def create_pull_request(token: str, branch: str, target: str): def create_pull_request(token: str, branch: str, target: str):
changelog = Changelog() changelog = Changelog()
commits = Git("log", target + "..", format="%s").lines() commits = Git("log", target + "..", format="%s").lines()
title = commits[0] if len(commits) == 1 else _get_pr_title_from_branch(branch) title = _get_pr_title(branch, commits)
panel("Título del PR", title) base.panel("Título del PR", title)
if confirm("¿Desea cambiar el título del PR?", False): if base.io_confirm("¿Desea cambiar el título del PR?", False):
title = prompt("Título del PR") title = base.io_prompt("Título del PR", persistent=True)
message = get_remote_api(token).create_pull_request( commits = Git.get_commits(target, branch)
message = base.get_remote_api(token).create_pull_request(
branch, branch,
target, target,
title, title,
changelog.generate_content(target, branch), changelog.generate_content(commits),
branch not in base.environments,
) )
success(message) base.io_success(message, "Rama creada")
if confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"):
Git("switch", target).exec(print="Cambiando a rama objetivo")
Git("pull").exec(print="Obteniendo cambios")
def _get_pr_title_from_branch(branch: str) -> str: def _get_pr_title(branch: str, commits: list[str]) -> str:
if branch in environments: if branch in base.environments:
target = environments[environments.index(branch) + 1] target = base.environments[base.environments.index(branch) + 1]
return f"Sincronización de entorno {branch} a {target}" return f"Sincronización de entorno {branch} a {target}"
components = branch.split("/") # <type>/<desc> or release/<env>/<type>/<desc> components = branch.split("/") # <type>/<desc> or release/<env>/<type>/<desc>
release_prefix = f"[Pasaje a {components[1]}]" if len(components) == 4 else None
branch_type = components[-2] branch_type = components[-2]
branch_desc = components[-1] branch_ticket, branch_desc = _extract_ticket_from_branch(components[-1])
# detect if branch description has ticket as prefix: <desc> = ABC-123-my-branch-name # when there's a single commit, use it as the PR description instead of
branch_ticket = None # deriving one from the branch name, but keep the release prefix / ticket
if len(commits) == 1:
return " ".join(filter(None, [release_prefix, branch_ticket, commits[0]]))
return " ".join(
filter(
None,
[
release_prefix,
branch_type + ":",
branch_ticket,
branch_desc.replace("-", " "),
],
)
)
def _extract_ticket_from_branch(branch_desc: str) -> tuple[Optional[str], str]:
"""detect if branch description has ticket as prefix: <desc> = ABC-123-my-branch-name"""
first_dash = branch_desc.find("-") first_dash = branch_desc.find("-")
second_dash = branch_desc.find("-", first_dash + 1) second_dash = branch_desc.find("-", first_dash + 1)
if first_dash >= 0 and second_dash >= 0: if first_dash >= 0 and second_dash >= 0:
maybe_ticket = branch_desc[:second_dash] maybe_ticket = branch_desc[:second_dash]
if is_valid_ticket(maybe_ticket): if base.is_valid_ticket(maybe_ticket):
branch_ticket = maybe_ticket return maybe_ticket, branch_desc[second_dash + 1 :]
branch_desc = branch_desc[:second_dash+1]
return " ".join(filter(None, [ return None, branch_desc
f"[Pasaje a {components[1]}]" if len(components) == 4 else None,
branch_type + ":",
branch_ticket,
branch_desc.replace("-", " ")
]))
+34 -29
View File
@@ -1,9 +1,11 @@
import typer
from git_flow import BRANCH_TYPES
from git_flow.command.base import *
from git_flow.git import Git
from typing import Optional from typing import Optional
import typer
import git_flow.command.base as base
from git_flow import BRANCH_TYPES
from git_flow.git import Git
app = typer.Typer() app = typer.Typer()
@@ -11,52 +13,55 @@ app = typer.Typer()
def new(): def new():
"""Crea una nueva rama siguiendo Conventional Branches""" """Crea una nueva rama siguiendo Conventional Branches"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
if branch not in environments: if branch not in base.environments:
warning("La rama actual no corresponde a un entorno configurado.") base.io_warning("La rama actual no corresponde a un entorno configurado.")
info("Se creará una nueva rama de trabajo sobre la rama actual.") base.io_info(
info("Debe seleccionar el tipo de cambio a realizar.") "Se creará una nueva rama de trabajo sobre la rama actual.\n"
"Debe seleccionar el tipo de cambio a realizar.",
title="Tipo de cambio",
)
branch_type = choice("Tipos de cambio", BRANCH_TYPES) branch_type = base.io_choice("Tipos de cambio", BRANCH_TYPES)
ticket = _get_optional_ticket() ticket = _get_optional_ticket()
new_branch = _get_unique_branch_name(branch_type, ticket) new_branch = _get_unique_branch_name(branch_type, ticket)
if "flow.remote" in flowconfig and Git.get_tracking_branch(branch): if "flow.remote" in base.flowconfig and Git.get_tracking_branch(branch):
Git("pull").exec(print="Actualizando rama actual") Git("pull").exec(print="Actualizando rama actual")
if confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"): if base.io_confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"):
Git("switch", new_branch, create=True).exec(print="Creando nueva rama") Git("switch", new_branch, create=True).exec(print="Creando nueva rama")
def _get_optional_ticket() -> Optional[str]:
ticket = prompt("[Opcional] Ticket que respalda el cambio (en formato ABC-123)")
if not ticket: def _get_optional_ticket() -> Optional[str]:
return None return base.io_prompt(
if not is_valid_ticket(ticket): "Ticket que respalda el cambio (en formato ABC-123)",
warning("Formato de ticket inválido, ignorando.") validator=lambda x: len(x.strip()) == 0 or base.is_valid_ticket(x),
return None )
return ticket
def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str: def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str:
info(f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.") base.io_info(
f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.\n"
"Las mismas se utilizaran para generar el nombre de la rama.",
title="Nombre de rama",
)
branch = None branch = None
branches = Git.get_branches() branches = Git.get_branches()
while branch is None: while branch is None:
keywords = prompt( keywords = base.io_prompt("Palabras clave", persistent=True)
"Palabras clave del cambio (por ejemplo: 'create worker form')",
persistent=True,
)
keywords = filter(lambda k: len(k) > 0, keywords.split(" ")) keywords = filter(lambda k: len(k) > 0, keywords.split(" "))
branch = branch_type + "/" + (ticket + "-" if ticket else "") + "-".join(keywords) branch = (
branch_type + "/" + (ticket + "-" if ticket else "") + "-".join(keywords)
)
if branch in branches: if branch in branches:
error( base.io_error(
f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra" f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra"
) )
branch = None branch = None
+18 -51
View File
@@ -1,38 +1,38 @@
from git_flow import COMMIT_TYPES, GitFlowError
from git_flow.command.base import *
from git_flow.git import Git
from typing import Optional
import typer import typer
import git_flow.command.base as base
from git_flow import GitFlowError
from git_flow.git import Git
app = typer.Typer() app = typer.Typer()
@app.command() @app.command()
def release(group: Optional[str] = None): def release():
"""Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado""" """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
env, _ = get_branch_env_and_type(branch) env, _ = base.get_branch_env_and_type(branch)
if env == environments[-1]: if env == base.environments[-1]:
raise GitFlowError( raise GitFlowError(
"No se puede hacer release de una rama en el ultimo entorno." "No se puede hacer release de una rama en el ultimo entorno."
) )
has_remote = "flow.remote" in flowconfig has_remote = "flow.remote" in base.flowconfig
ensure_clean_worktree(has_remote) base.ensure_clean_worktree(has_remote)
next_env = environments[environments.index(env) + 1] next_env = base.environments[base.environments.index(env) + 1]
next_branch = ( next_branch = (
branch.replace(f"/{env}/", f"/{next_env}/") branch.replace(f"/{env}/", f"/{next_env}/")
if branch.startswith("release/") if branch.startswith("release/")
else f"release/{next_env}/{branch}" else f"release/{next_env}/{branch}"
) )
base = Git.get_first_fork_point(branch, env) fork_point = Git.get_first_fork_point(branch, env)
commits = Git("log", base + "..", format="%s").lines() commits = Git("log", fork_point + "..", format="%s").lines()
if not commits: if not commits:
raise GitFlowError("No hay cambios a mergear.") raise GitFlowError("No hay cambios a mergear.")
@@ -42,50 +42,17 @@ def release(group: Optional[str] = None):
for commit in commits: for commit in commits:
print("- " + commit) print("- " + commit)
grouping = group is not None if has_remote and Git.get_tracking_branch(next_env):
Git("switch", next_env).exec(print="Cambiando a la rama objetivo")
if not group:
if confirm("¿Desea agrupar este release con otra rama?", False):
grouping = True
group = choice(
"Grupo release: ", Git.get_branches("release/" + next_env + "/")
)
else:
group = next_env
if has_remote and Git.get_tracking_branch(group):
Git("switch", group).exec(print="Cambiando a la rama objetivo")
Git("pull").exec(print="Sincronizando cambios la rama objetivo") Git("pull").exec(print="Sincronizando cambios la rama objetivo")
Git("switch", "-").exec(print="Volviendo a la rama original") Git("switch", "-").exec(print="Volviendo a la rama original")
if len(commits) > 1 and confirm( Git("switch", next_branch, create=True).exec(print="Creando rama release")
f"¿Desea reemplazar los {len(commits)} commits de la rama por uno solo?", False
):
info("Debe ingresar el mensaje del commit a crear.")
commit_type = choice("Tipo de commit", COMMIT_TYPES[:-1]) status = Git("rebase", fork_point, next_branch, onto=next_env).code(
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" print="Moviendo cambios hacia el siguiente ambiente"
) )
if status: if status:
Git("rebase", abort=True).exec(print="Deshaciendo cambios por conflictos") Git("rebase", abort=True).exec(print="Deshaciendo cambios por conflictos")
raise GitFlowError("Error al realizar pasaje de cambios a rama objetivo") raise GitFlowError("Error al realizar pasaje de cambios a rama objetivo")
elif grouping:
Git("switch", group).exec(print="Cambiando a rama objetivo")
Git("merge", next_branch, ff_only=True).exec(
print="Mergeando cambios de la nueva rama"
)
+65 -56
View File
@@ -1,3 +1,8 @@
from typing import Optional
import typer
import git_flow.command.base as base
from git_flow import ( from git_flow import (
COMMIT_TYPE_INCREMENT, COMMIT_TYPE_INCREMENT,
SEMVER_MAJOR, SEMVER_MAJOR,
@@ -7,10 +12,7 @@ from git_flow import (
GitFlowError, GitFlowError,
) )
from git_flow.changelog import Changelog from git_flow.changelog import Changelog
from git_flow.command.base import *
from git_flow.git import Git from git_flow.git import Git
from typing import Optional
import typer
app = typer.Typer() app = typer.Typer()
@@ -18,81 +20,90 @@ app = typer.Typer()
@app.command() @app.command()
def tag(token: Optional[str] = None): def tag(token: Optional[str] = None):
"""Crea un nuevo tag para el último merge.""" """Crea un nuevo tag para el último merge."""
ensure_initialized() base.ensure_initialized()
target = Git.get_current_branch() target = Git.get_current_branch()
if target not in environments: if target not in base.environments:
raise GitFlowError("Solo se pueden taggear commits en ramas principales.") raise GitFlowError("Solo se pueden taggear commits en ramas principales.")
branch = Git(
"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 not next_tag:
info(
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
)
return
token, ci = get_token_and_ci(token) token, ci = get_token_and_ci(token)
if not token: if ci and not token:
raise GitFlowError("No hay token disponible") raise GitFlowError("No hay token disponible")
merge_commit = Git(
"log", first_parent=True, merges=True, max_count=1, format="%H"
).firstline()
if not merge_commit:
if not ci:
raise GitFlowError("No hay merges a taggear.")
return
# Con varios entornos cada uno versiona por separado (v1.0.0-prod), con uno solo no hace falta
suffix = f"-{target}" if len(base.environments) > 1 else ""
if ci:
# Commits se obtienen por API del ultimo PR / MR mergeado
commits = base.get_remote_api(token).get_merge_commits(merge_commit)
commits = [c for c in commits if not c.is_merge]
# Último tag se obtiene por API filtrando por sufijo
current_tag = base.get_remote_api(token).get_latest_tag(suffix)
else:
# Commits se obtienen del último merge, listando hasta el primer forkpoint
branch = Git("show", merge_commit + "^2", patch=False, format="%h").firstline()
fork_point = Git.get_first_fork_point(branch, target)
commits = Git.get_commits(fork_point, branch)
# Último tag se obtiene localmente
current_tag = Git.get_current_tag(suffix)
next_tag = get_next_tag_from_commits(
[c.subject for c in commits], suffix, current_tag
)
if not next_tag:
if not ci:
base.io_info(
"Los cambios realizados no implican un salto de versión, se mantiene la anterior."
)
return
changelog = Changelog() changelog = Changelog()
changelog.update(next_tag, base, branch) changelog.update(next_tag, commits)
Git("add", Changelog.FILENAME).exec(print="Agregando nuevo CHANGELOG.md") Git("add", Changelog.FILENAME).exec(print=None if ci else "Agregando nuevo CHANGELOG.md")
Git("commit", message=Changelog.COMMIT_MESSAGE).exec(print="Creando commit") Git("commit", message=Changelog.COMMIT_MESSAGE).exec(print=None if ci else "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()
success(get_remote_api(token).create_tag(next_tag, tag_commit)) base.get_remote_api(token).create_tag(next_tag, tag_commit)
print(next_tag)
Git("tag", next_tag, tag_commit).exec(print="Creando tag localmente") else:
Git("tag", next_tag, tag_commit).exec(print="Creando tag localmente")
def get_token_and_ci(token: Optional[str]): def get_token_and_ci(token: Optional[str]):
try: if token:
return (ensure_repository_token(), False)
except GitFlowError:
return (token, True) return (token, True)
else:
return (base.ensure_repository_token(), False)
def get_last_merge_commit(): def get_next_tag_from_commits(
commit = Git( commits: list[str], suffix: str, current_tag: str | None
"log", first_parent=True, merges=True, max_count=1, format="%h" ) -> str | None:
).firstline()
if not commit:
raise GitFlowError("No hay merges a taggear.")
return commit
def check_not_tagged(commit: str):
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(commits: list[str], branch: str) -> str | None:
increment = SEMVER_SKIP increment = SEMVER_SKIP
for commit in commits: for commit in commits:
commit_type = commit[: commit.index(":")] commit_type = commit.partition(":")[0]
increment = max(increment, COMMIT_TYPE_INCREMENT.get(commit_type, SEMVER_SKIP)) increment = max(increment, COMMIT_TYPE_INCREMENT.get(commit_type, SEMVER_SKIP))
tag = Git.get_current_tag() or f"v0.0.0-{branch}" if increment == SEMVER_SKIP:
return None
until_dash = tag.index("-") if "-" in tag else None version = (current_tag or "v0.0.0" + suffix).removeprefix("v").removesuffix(suffix)
suffix = tag[until_dash:] if until_dash is not None else "" [major, minor, patch] = map(int, version.split("."))
[major, minor, patch] = map(int, tag[1:until_dash].split("."))
if increment == SEMVER_MAJOR: if increment == SEMVER_MAJOR:
major += 1 major += 1
@@ -103,7 +114,5 @@ def get_next_tag_from_commits(commits: list[str], branch: str) -> str | None:
patch = 0 patch = 0
elif increment == SEMVER_PATCH: elif increment == SEMVER_PATCH:
patch += 1 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}" return f"v{major}.{minor}.{patch}{suffix}"
+39 -17
View File
@@ -1,9 +1,20 @@
import subprocess import subprocess
from dataclasses import dataclass
from git_flow import FLOWCONFIG_FILENAME from git_flow import FLOWCONFIG_FILENAME
from git_flow.io import * from git_flow.io import *
@dataclass
class Commit:
"""Datos de un commit necesarios para versionar y generar el changelog."""
sha: str
subject: str
email: str
is_merge: bool = False
class Git: class Git:
class Status: class Status:
worktree: str worktree: str
@@ -52,8 +63,15 @@ class Git:
return list(map(Git.Status, Git("status", porcelain=True).lines(False))) return list(map(Git.Status, Git("status", porcelain=True).lines(False)))
@staticmethod @staticmethod
def get_current_tag(): def get_current_tag(suffix: str = ""):
return Git("describe", abbrev="0", tags=True).firstline(check=False) """Último tag SemVer alcanzable desde HEAD, con el sufijo exacto indicado."""
return Git(
"describe",
abbrev="0",
tags=True,
match="v[0-9]*.[0-9]*.[0-9]*" + suffix,
exclude=None if suffix else "*-*",
).firstline(check=False, print="Obteniendo tag actual")
@staticmethod @staticmethod
def get_first_fork_point(branch: str, env: str): def get_first_fork_point(branch: str, env: str):
@@ -65,6 +83,20 @@ class Git:
return Git("show", env_commits[-1] + "^", patch=False, format="%h").firstline() return Git("show", env_commits[-1] + "^", patch=False, format="%h").firstline()
@staticmethod
def get_commits(base: str, branch: str) -> list[Commit]:
"""Commits propios de la rama (sin merges), del más nuevo al más viejo."""
lines = Git(
"log", base + ".." + branch, first_parent=True, merges=False, format="%h%x09%ae%x09%s"
).lines()
commits = []
for line in lines:
[sha, email, subject] = line.split("\t", 2)
commits.append(Commit(sha, subject, email))
return commits
@staticmethod @staticmethod
def _get_references(kind: str): def _get_references(kind: str):
prefix = "refs/" + kind prefix = "refs/" + kind
@@ -117,7 +149,7 @@ class Git:
return lines if not strip else list(map(lambda l: l.strip(), lines)) return lines if not strip else list(map(lambda l: l.strip(), lines))
def firstline(self, **kwargs: bool): def firstline(self, **kwargs):
lines = self.lines(**kwargs) lines = self.lines(**kwargs)
return lines[0] if lines else "" return lines[0] if lines else ""
@@ -140,17 +172,7 @@ class Git:
return process return process
def _run(self, print: str | None = None, check: bool = True): def _run(self, print: str | None = None, check: bool = True):
process = subprocess.run( return self._get(print, check)
self.command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
if print is not None:
self._print_process("", "", title=print)
if check:
process.check_returncode()
return process
def _print_process(self, stdout: str, stderr: str, title: str): def _print_process(self, stdout: str, stderr: str, title: str):
command = [] command = []
@@ -164,12 +186,12 @@ class Git:
command.append(arg) command.append(arg)
text = f"[yellow]$ {" ".join(command)}[/yellow]" text = f"[green]$ {' '.join(command)}[/green]"
for line in stdout.splitlines(): for line in stdout.splitlines():
text += "\n\\[out] " + line text += "\n" + line
for line in stderr.splitlines(): for line in stderr.splitlines():
text += "\n[red]\\[err][/red] " + line text += "\n[red]" + line + "[/red]"
panel(title or "Ejecutando", text) panel(title or "Ejecutando", text)
+12 -8
View File
@@ -1,10 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import traceback
from subprocess import SubprocessError
import typer import typer
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.command.base import * from git_flow.command import base, branch, commit, init, merge, new, release, tag
from git_flow.command import init, new, commit, merge, tag, release, branch
REPOSITORY_TOKEN_PATH = ".repository-token" REPOSITORY_TOKEN_PATH = ".repository-token"
LOCALE = "es_AR.UTF-8" LOCALE = "es_AR.UTF-8"
@@ -23,10 +25,12 @@ app.add_typer(branch.app)
def main(): def main():
try: try:
app() app()
except GitFlowError as e:
error(str(e))
except Exception as e:
error("Ocurrió un error inesperado: " + str(e))
except KeyboardInterrupt as e:
print() print()
error("Ejecución abortada") except GitFlowError as e:
lines = [str(e), ""]
lines += traceback.format_exception(e)
base.io_error("".join(lines), title="Git Flow")
except SubprocessError as e:
base.io_error(str(e), title="Comando")
except KeyboardInterrupt:
base.io_error("Ejecución abortada")
+92 -33
View File
@@ -1,7 +1,12 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from git_flow import SUPPORTED_REMOTE_APIS, GitFlowError import re
from git_flow.git import Git
import giturlparse
import requests
from git_flow import SUPPORTED_REMOTE_URL_SCHEMAS, GitFlowError
from git_flow.git import Commit, Git
class RemoteAPI(ABC): class RemoteAPI(ABC):
@@ -11,43 +16,42 @@ class RemoteAPI(ABC):
@staticmethod @staticmethod
def parse(remote: str): def parse(remote: str):
"""Soportamos 3 tipos de URL: http, https y ssh. Si no se especifica el esquema, asumimos ssh:
Ejemplos:
- http://gitea/user/repo
- https://gitea.com/user/repo.git
- git@bitbucket.org:user/repo
- git.mygiteainstance.com:user/repo.git
- ssh://git@git.mygiteainstance.com:1234/user/repo.git
"""
url = Git("remote", "get-url", remote).firstline() url = Git("remote", "get-url", remote).firstline()
invalid_url_error = GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
url_type = None
if url.startswith("git@"): parsed_url = giturlparse.parse(url) # Validamos que la URL sea válida
url_type = "ssh"
elif url.startswith("http://") or url.startswith("https://"):
url_type = "https"
else:
raise invalid_url_error
if url_type == "ssh": if not parsed_url.valid:
# git@host:user/repo[.git] raise GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
at_pos = url.index("@")
colon_pos = url.index(":")
host = url[at_pos+1:colon_pos]
repository = url[colon_pos+1:]
else:
# https://host/user/repo[.git]
host_start = url.index("://") + 3
uri_start = url.index("/", host_start)
host = url[host_start:uri_start]
repository = url[uri_start+1:]
# Esto permite configurar hosts "imaginarios" para permitir el uso de multiples claves ssh if not parsed_url.protocol in SUPPORTED_REMOTE_URL_SCHEMAS:
if host.endswith(".bitbucket.org"): raise GitFlowError(
host = "bitbucket.org" f"La URL del remoto {remote} no tiene un esquema soportado: {url}"
elif host.endswith(".github.com"): )
host = "github.com"
if host not in SUPPORTED_REMOTE_APIS: # Cuando el host es seteado por pipeline puede ser "http://gitea/OWNER/REPO", en ese caso
raise GitFlowError("El host del repositorio remoto es inválido") # la libreria parsea OWNER="" y REPO="OWNER/REPO"
# En GitLab, los proyectos pueden estar dentro de grupos y subgrupos (ej:
repository.removesuffix(".git") # owner/grupo/subgrupo/repo), que la libreria expone por separado en groups_path
repository = (
return [host, repository] "/".join(
filter(
None,
[parsed_url.owner, parsed_url.groups_path, parsed_url.repo],
)
)
.removeprefix("/")
.removesuffix(".git")
)
return [parsed_url.protocol + "://", parsed_url.host, repository]
@abstractmethod @abstractmethod
def create_pull_request( def create_pull_request(
@@ -56,6 +60,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
@@ -63,6 +68,18 @@ class RemoteAPI(ABC):
def create_tag(self, tag: str, commit: str) -> str: def create_tag(self, tag: str, commit: str) -> str:
pass pass
@abstractmethod
def get_merge_commits(self, merge_commit: str) -> list[Commit]:
"""Busca el PR que generó el merge commit y devuelve sus commits (del más nuevo al más
viejo), sin depender del historial local."""
pass
@abstractmethod
def list_tags(self, suffix: str = "") -> list[str]:
"""Nombres de los tags del repositorio, sin orden garantizado. `suffix` es solo una pista
para que la API filtre; el filtrado definitivo se hace en `get_latest_tag`."""
pass
@abstractmethod @abstractmethod
def get_endpoint(self, resource: str) -> str: def get_endpoint(self, resource: str) -> str:
pass pass
@@ -73,3 +90,45 @@ class RemoteAPI(ABC):
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": "Bearer " + self.token, "Authorization": "Bearer " + self.token,
} }
def get_json(self, url: str, params: dict | None = None):
response = requests.get(url, headers=self.get_headers(), params=params)
if not response.ok:
raise GitFlowError(f"Ocurrió un error al consultar {url}: {response.text}")
return response
def get_latest_tag(self, suffix: str = "") -> str | None:
"""Tag SemVer más alto (vX.Y.Z + suffix exacto), o None si no hay ninguno."""
pattern = re.compile(rf"^v(\d+)\.(\d+)\.(\d+){re.escape(suffix)}$")
versions = {}
for name in self.list_tags(suffix):
match = pattern.match(name)
if match:
versions[tuple(map(int, match.groups()))] = name
return versions[max(versions)] if versions else None
def get_all_pages(
self,
url: str,
page_size: int = 50,
params: dict | None = None,
size_param: str = "per_page",
) -> list:
"""Recorre un endpoint paginado por número de página (page / per_page)."""
items: list = []
page = 1
while True:
query = {**(params or {}), "page": page, size_param: page_size}
batch = self.get_json(url, query).json()
items += batch
if len(batch) < page_size:
return items
page += 1
+51 -1
View File
@@ -1,5 +1,8 @@
import re
import requests import requests
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.git import Commit
from git_flow.remote.base import RemoteAPI from git_flow.remote.base import RemoteAPI
@@ -12,6 +15,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 +25,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,
}, },
) )
@@ -43,5 +47,51 @@ class BitbucketRemoteAPI(RemoteAPI):
else: else:
raise GitFlowError("Ocurrió un error al crear el tag: " + response.text) raise GitFlowError("Ocurrió un error al crear el tag: " + response.text)
def get_merge_commits(self, merge_commit: str) -> list[Commit]:
prs = self.get_json(self.get_endpoint(f"commit/{merge_commit}/pullrequests")).json()
pr = next((p for p in prs["values"] if p["state"] == "MERGED"), None)
if not pr:
raise GitFlowError(f"No se encontró un PR para el commit {merge_commit}")
commits: list[Commit] = []
url: str | None = self.get_endpoint(f"pullrequests/{pr['id']}/commits")
# Bitbucket pagina siguiendo el link "next", y devuelve del más nuevo al más viejo
while url:
page = self.get_json(url).json()
for c in page["values"]:
raw = c["author"].get("raw", "")
match = re.search(r"<(.+?)>", raw)
commits.append(
Commit(
c["hash"][:7],
c["message"].splitlines()[0],
match.group(1) if match else raw,
len(c["parents"]) > 1,
)
)
url = page.get("next")
return commits
def list_tags(self, suffix: str = "") -> list[str]:
names: list[str] = []
url: str | None = self.get_endpoint("refs/tags")
params: dict[str, int|str] | None = {"pagelen": 100}
if suffix:
params["q"] = f'name ~ "{suffix}"'
while url:
page = self.get_json(url, params).json()
names += [t["name"] for t in page["values"]]
url = page.get("next")
params = None # "next" ya incluye la query
return names
def get_endpoint(self, resource: str) -> str: def get_endpoint(self, resource: str) -> str:
return f"{BitbucketRemoteAPI.API_ENDPOINT}/{self.repository}/{resource}" return f"{BitbucketRemoteAPI.API_ENDPOINT}/{self.repository}/{resource}"
+90
View File
@@ -0,0 +1,90 @@
import requests
from git_flow import GitFlowError
from git_flow.git import Commit
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_merge_commits(self, merge_commit: str) -> list[Commit]:
pr = self.get_json(self.get_endpoint(f"/commits/{merge_commit}/pull")).json()
url = self.get_endpoint(f"/pulls/{pr['number']}/commits")
items: list = []
page = 1
# Gitea pagina con page / limit
while True:
batch = self.get_json(
url, {"page": page, "limit": 50, "files": "false", "verification": "false"}
).json()
items += batch
if len(batch) < 50:
break
page += 1
# La API los devuelve del más viejo al más nuevo
return [
Commit(
c["sha"][:7],
c["commit"]["message"].splitlines()[0],
c["commit"]["author"]["email"],
len(c["parents"]) > 1,
)
for c in reversed(items)
]
def list_tags(self, suffix: str = "") -> list[str]:
tags = self.get_all_pages(self.get_endpoint("/tags"), 50, size_param="limit")
return [t["name"] for t in tags]
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,
}
+53 -4
View File
@@ -1,9 +1,11 @@
import requests
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.git import Commit
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 +13,58 @@ 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_merge_commits(self, merge_commit: str) -> list[Commit]:
prs = self.get_json(self.get_endpoint(f"/commits/{merge_commit}/pulls")).json()
pr = next((p for p in prs if p.get("merge_commit_sha") == merge_commit), None)
if not pr:
raise GitFlowError(f"No se encontró un PR para el commit {merge_commit}")
items = self.get_all_pages(self.get_endpoint(f"/pulls/{pr['number']}/commits"), 100)
# La API los devuelve del más viejo al más nuevo
return [
Commit(
c["sha"][:7],
c["commit"]["message"].splitlines()[0],
c["commit"]["author"]["email"],
len(c["parents"]) > 1,
)
for c in reversed(items)
]
def list_tags(self, suffix: str = "") -> list[str]:
return [t["name"] for t in self.get_all_pages(self.get_endpoint("/tags"), 100)]
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}"
+89
View File
@@ -0,0 +1,89 @@
from urllib.parse import quote
import requests
from git_flow import GitFlowError
from git_flow.git import Commit
from git_flow.remote.base import RemoteAPI
class GitlabRemoteAPI(RemoteAPI):
def __init__(self, base_url: str, repository: str, token: str) -> None:
super().__init__(repository, token)
self.base_url = base_url
def create_pull_request(
self,
source: str,
destination: str,
title: str | None = None,
description: str | None = None,
close_source_branch: bool = True
) -> str:
response = requests.post(
self.get_endpoint("/merge_requests"),
headers=self.get_headers(),
json={
"title": title,
"description": description,
"source_branch": source,
"target_branch": destination,
"remove_source_branch": close_source_branch,
},
)
if response.ok:
return "PR creado exitosamente: " + response.json()["web_url"]
else:
raise GitFlowError("Ocurrió un error al crear el PR: " + response.text)
def create_tag(self, tag: str, commit: str) -> str:
response = requests.post(
self.get_endpoint("/repository/tags"),
headers=self.get_headers(),
json={"tag_name": tag, "ref": commit},
)
if response.ok:
return "Tag creado exitosamente: " + tag
else:
raise GitFlowError("Ocurrió un error al crear el tag para repositorio " + quote(self.repository, safe="") + ": " + response.text)
def get_merge_commits(self, merge_commit: str) -> list[Commit]:
mrs = self.get_json(
self.get_endpoint(f"/repository/commits/{merge_commit}/merge_requests")
).json()
mr = next((m for m in mrs if m.get("merge_commit_sha") == merge_commit), None)
if not mr:
raise GitFlowError(f"No se encontró un MR para el commit {merge_commit}")
items = self.get_all_pages(self.get_endpoint(f"/merge_requests/{mr['iid']}/commits"), 100)
# GitLab ya los devuelve del más nuevo al más viejo
return [
Commit(
c["short_id"][:7],
c["title"],
c["author_email"],
len(c["parent_ids"]) > 1,
)
for c in items
]
def list_tags(self, suffix: str = "") -> list[str]:
# "search" soporta "$" para anclar al final del nombre
params = {"search": suffix + "$"} if suffix else None
tags = self.get_all_pages(self.get_endpoint("/repository/tags"), 100, params)
return [t["name"] for t in tags]
def get_endpoint(self, resource: str) -> str:
project_id = quote(self.repository, safe="")
return f"{self.base_url}/api/v4/projects/{project_id}{resource}"
def get_headers(self):
return {
"Accept": "application/json",
"Content-Type": "application/json",
"PRIVATE-TOKEN": self.token,
}
Generated
+113 -1
View File
@@ -1,6 +1,6 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.14" requires-python = ">=3.10"
[[package]] [[package]]
name = "annotated-doc" name = "annotated-doc"
@@ -26,6 +26,70 @@ version = "3.4.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
{ url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
{ url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
{ url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
{ url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
{ url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
{ url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
{ url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
{ url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
{ url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
{ url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
{ url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
{ url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
{ url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
{ url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
@@ -70,16 +134,31 @@ wheels = [
name = "git-flow-envs" name = "git-flow-envs"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "giturlparse" },
{ name = "questionary" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" },
{ name = "typer" }, { name = "typer" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "giturlparse" },
{ name = "questionary" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" },
{ name = "typer" }, { name = "typer" },
] ]
[[package]]
name = "giturlparse"
version = "0.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/8a/b70b84cc78f9059d627f09560c81a11e8f046570d220a24e169489cad6c9/giturlparse-0.15.0.tar.gz", hash = "sha256:9af3f1fd5c4a0cac94ddb283593635005646393ee0debbe330d1bdff8866bf2c", size = 16138, upload-time = "2026-06-16T07:28:56.021Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/96/147a2771ab655b9353781fb2f95c94eaf1d8576dccc991c1a61d0d355067/giturlparse-0.15.0-py2.py3-none-any.whl", hash = "sha256:76d2e6983b037356ab99b30683e533ac3db96409b68e2163a20fc3aff6446f10", size = 16683, upload-time = "2026-06-16T07:28:55.184Z" },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.11" version = "3.11"
@@ -110,6 +189,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
] ]
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.20.0" version = "2.20.0"
@@ -119,6 +210,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
] ]
[[package]]
name = "questionary"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "prompt-toolkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.5" version = "2.32.5"
@@ -179,3 +282,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
] ]
[[package]]
name = "wcwidth"
version = "0.8.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
]