import csv import datetime 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%(author)\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") 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}") 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]) -> 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", "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: 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}'" )