From 6a4f6c9804d2817755e6c50d255c482ce2f18a7e Mon Sep 17 00:00:00 2001 From: Jonathan Teran Carballo Date: Tue, 22 Sep 2026 01:32:10 -0300 Subject: [PATCH] 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}'" + )