From 5ece118c05cb31a2f7ba175ede6cd839ba4ef7d3 Mon Sep 17 00:00:00 2001 From: Jonathan Teran Carballo Date: Tue, 22 Sep 2026 01:20:33 -0300 Subject: [PATCH 1/3] refactor: move branch command to branch ls subcommand --- README.md | 4 ++-- src/git_flow/command/branch/__init__.py | 6 ++++++ src/git_flow/command/{branch.py => branch/ls.py} | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 src/git_flow/command/branch/__init__.py rename src/git_flow/command/{branch.py => branch/ls.py} (98%) diff --git a/README.md b/README.md index 80c6486..bf0e71d 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ más información): - `merge`: Mergea la rama actual a una de las ramas principales. - `tag`: Crea un tag sobre el ultimo merge siguiendo SemVer (ver [Tag](#tag)). - `release`: Prepara la rama actual para pasarse al siguiente entorno configurado. -- `branch`: Lista las ramas del repositorio, agrupadas por entorno objetivo. +- `branch ls`: Lista las ramas del repositorio, agrupadas por entorno objetivo. El workflow para el cual se penso la herramienta es el siguiente: @@ -88,7 +88,7 @@ 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 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 +En cualquier momento se puede usar `git-flow branch ls` para ver en qué entorno está cada rama, y cuáles están todavía en progreso (`wip/`) o descartadas (`trash/`). ### Tag diff --git a/src/git_flow/command/branch/__init__.py b/src/git_flow/command/branch/__init__.py new file mode 100644 index 0000000..219afcf --- /dev/null +++ b/src/git_flow/command/branch/__init__.py @@ -0,0 +1,6 @@ +import typer + +from git_flow.command.branch import ls + +app = typer.Typer(name="branch") +app.add_typer(ls.app) diff --git a/src/git_flow/command/branch.py b/src/git_flow/command/branch/ls.py similarity index 98% rename from src/git_flow/command/branch.py rename to src/git_flow/command/branch/ls.py index 1c78dc3..4b07696 100644 --- a/src/git_flow/command/branch.py +++ b/src/git_flow/command/branch/ls.py @@ -3,12 +3,12 @@ from typing import Annotated import typer from rich.console import Console -import git_flow.command.base as base from git_flow import ( TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError, ) +from git_flow.command import base from git_flow.git import Git BRANCH_FORMAT = "%(refname:short)" @@ -18,7 +18,7 @@ console = Console() @app.command() -def branch( +def ls( environment: Annotated[ str | None, typer.Argument( -- 2.54.0 From 6a4f6c9804d2817755e6c50d255c482ce2f18a7e Mon Sep 17 00:00:00 2001 From: Jonathan Teran Carballo Date: Tue, 22 Sep 2026 01:32:10 -0300 Subject: [PATCH 2/3] feature: add branch clean subcommand --- README.md | 3 + branch-clean-report.csv | 2 + src/git_flow/command/branch/__init__.py | 3 +- src/git_flow/command/branch/clean.py | 118 ++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 branch-clean-report.csv create mode 100644 src/git_flow/command/branch/clean.py diff --git a/README.md b/README.md index bf0e71d..74dce2f 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,9 @@ más información): - `tag`: Crea un tag sobre el ultimo merge siguiendo SemVer (ver [Tag](#tag)). - `release`: Prepara la rama actual para pasarse al siguiente entorno configurado. - `branch ls`: Lista las ramas del repositorio, agrupadas por entorno objetivo. +- `branch clean`: Elimina las ramas alcanzables desde HEAD (locales por defecto, o remotas con + `--remote`), dejando un reporte CSV (`branch-clean-report.csv`) con nombre, commit, autor y + fecha de cada rama eliminada. El workflow para el cual se penso la herramienta es el siguiente: diff --git a/branch-clean-report.csv b/branch-clean-report.csv new file mode 100644 index 0000000..d77228f --- /dev/null +++ b/branch-clean-report.csv @@ -0,0 +1,2 @@ +name,commit sha,author,date +worktree-kind-watching-hearth,e8c0c0b777d31fa94cd3f01fbf9c68b165d77015,Jonathan Teran Carballo,2026-09-13T23:18:47-03:00 diff --git a/src/git_flow/command/branch/__init__.py b/src/git_flow/command/branch/__init__.py index 219afcf..67e2bcf 100644 --- a/src/git_flow/command/branch/__init__.py +++ b/src/git_flow/command/branch/__init__.py @@ -1,6 +1,7 @@ import typer -from git_flow.command.branch import ls +from git_flow.command.branch import clean, ls app = typer.Typer(name="branch") app.add_typer(ls.app) +app.add_typer(clean.app) diff --git a/src/git_flow/command/branch/clean.py b/src/git_flow/command/branch/clean.py new file mode 100644 index 0000000..7c44b16 --- /dev/null +++ b/src/git_flow/command/branch/clean.py @@ -0,0 +1,118 @@ +import csv +from dataclasses import dataclass +from typing import Annotated + +import typer + +from git_flow import GitFlowError +from git_flow.command import base +from git_flow.git import Git + +REPORT_FORMAT = "%(refname:short)\t%(objectname)\t%(authorname)\t%(authordate:iso-strict)" +REPORT_FILENAME = "branch-clean-report.csv" + +app = typer.Typer() + + +@dataclass +class BranchInfo: + name: str + sha: str + author: str + date: str + + +@app.command() +def clean( + remote: Annotated[ + bool, + typer.Option( + help="Elimina las ramas remotas alcanzables desde HEAD, en vez de las locales" + ), + ] = False, +): + """Elimina las ramas alcanzables desde HEAD, dejando un reporte CSV de las ramas eliminadas""" + base.ensure_initialized() + + branches = get_remote_branches() if remote else get_local_branches() + + if not branches: + base.io_info("No hay ramas alcanzables desde HEAD para eliminar.") + return + + base.io_info( + "\n".join(f"- {branch.name}" for branch in branches), + title=f"Ramas a eliminar ({'remoto' if remote else 'local'})", + ) + + if not base.io_confirm(f"¿Eliminar {len(branches)} rama(s)?", False): + raise GitFlowError("Ejecución abortada") + + write_report(branches) + + for branch in branches: + delete_branch(branch, remote) + + base.io_success(f"Se eliminaron {len(branches)} rama(s). Reporte generado en {REPORT_FILENAME}") + + +def get_local_branches() -> list[BranchInfo]: + current_branch = Git.get_current_branch() + branches = get_merged_branches("refs/heads/") + + return [b for b in branches if b.name != current_branch and b.name not in base.environments] + + +def get_remote_branches() -> list[BranchInfo]: + if "flow.remote" not in base.flowconfig: + raise GitFlowError("El repositorio no tiene configurado un remoto.") + + remote = base.flowconfig["flow.remote"] + current_tracking = Git.get_tracking_branch(Git.get_current_branch()) + branches = get_merged_branches(f"refs/remotes/{remote}/") + + result = [] + for branch in branches: + if branch.name == current_tracking or branch.name == f"{remote}/HEAD": + continue + + name = branch.name.removeprefix(f"{remote}/") + + if name in base.environments: + continue + + result.append(BranchInfo(name, branch.sha, branch.author, branch.date)) + + return result + + +def get_merged_branches(pattern: str) -> list[BranchInfo]: + lines = Git("for-each-ref", pattern, merged="HEAD", format=REPORT_FORMAT).lines() + + return [parse_branch(line) for line in lines] + + +def parse_branch(line: str) -> BranchInfo: + name, sha, author, date = line.split("\t") + return BranchInfo(name, sha, author, date) + + +def write_report(branches: list[BranchInfo]): + with open(REPORT_FILENAME, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["name", "commit sha", "author", "date"]) + + for branch in branches: + writer.writerow([branch.name, branch.sha, branch.author, branch.date]) + + +def delete_branch(branch: BranchInfo, remote: bool): + if remote: + remote_name = base.flowconfig["flow.remote"] + Git("push", remote_name, branch.name, delete=True).exec( + print=f"Eliminando rama remota '{branch.name}'" + ) + else: + Git("branch", branch.name, d=True).exec( + print=f"Eliminando rama local '{branch.name}'" + ) -- 2.54.0 From b20c7e0cb5ac31d8fecffec3d0f50ac83e6d16d3 Mon Sep 17 00:00:00 2001 From: Jonathan Teran Carballo Date: Tue, 22 Sep 2026 21:39:48 -0300 Subject: [PATCH 3/3] feature: include author email in report, generate report filename --- src/git_flow/command/branch/clean.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/git_flow/command/branch/clean.py b/src/git_flow/command/branch/clean.py index 7c44b16..d7ec7b1 100644 --- a/src/git_flow/command/branch/clean.py +++ b/src/git_flow/command/branch/clean.py @@ -1,4 +1,5 @@ import csv +import datetime from dataclasses import dataclass from typing import Annotated @@ -8,7 +9,7 @@ from git_flow import GitFlowError from git_flow.command import base from git_flow.git import Git -REPORT_FORMAT = "%(refname:short)\t%(objectname)\t%(authorname)\t%(authordate:iso-strict)" +REPORT_FORMAT = "%(refname:short)\t%(objectname)\t%(author)\t%(authordate:iso-strict)" REPORT_FILENAME = "branch-clean-report.csv" app = typer.Typer() @@ -48,12 +49,12 @@ def clean( if not base.io_confirm(f"¿Eliminar {len(branches)} rama(s)?", False): raise GitFlowError("Ejecución abortada") - write_report(branches) + report = write_report(branches) for branch in branches: delete_branch(branch, remote) - base.io_success(f"Se eliminaron {len(branches)} rama(s). Reporte generado en {REPORT_FILENAME}") + base.io_success(f"Se eliminaron {len(branches)} rama(s). Reporte generado en {report}") def get_local_branches() -> list[BranchInfo]: @@ -97,14 +98,19 @@ def parse_branch(line: str) -> BranchInfo: return BranchInfo(name, sha, author, date) -def write_report(branches: list[BranchInfo]): - with open(REPORT_FILENAME, "w", newline="") as f: +def write_report(branches: list[BranchInfo]) -> str: + now = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + report = "branch-clean-report-" + now + ".csv" + + with open(report, "w", newline="") as f: writer = csv.writer(f) - writer.writerow(["name", "commit sha", "author", "date"]) + writer.writerow(["name", "commit", "author", "date"]) for branch in branches: writer.writerow([branch.name, branch.sha, branch.author, branch.date]) + return report + def delete_branch(branch: BranchInfo, remote: bool): if remote: -- 2.54.0