from typing import Optional import typer import git_flow.command.base as base from git_flow import GitFlowError from git_flow.changelog import Changelog from git_flow.git import Git app = typer.Typer() @app.command() def merge(): """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado""" base.ensure_initialized() branch = base.ensure_right_branch() if branch in base.environments: if branch == base.environments[-1]: raise GitFlowError("No se puede hacer un merge del último entorno.") base.io_info( """Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""", "Merge de entornos", ) if not base.io_confirm("¿Desea continuar?"): raise GitFlowError("Ejecución abortada") target = base.environments[base.environments.index(branch) + 1] else: target, _ = base.get_branch_env_and_type(branch) if "flow.remote" in base.flowconfig: remote = base.flowconfig["flow.remote"] token = base.ensure_repository_token() run_remote(remote, token, branch, target) else: run_local(branch, target) def run_remote(remote: str, token: str, branch: str, target: str): base.ensure_clean_worktree(True) Git("switch", target).exec(print="Cambiando a rama destino") Git("pull").exec(print="Obteniendo ultimos cambios del remoto") Git("switch", "-").exec(print="Volviendo a rama a mergear") check_merge_conflicts(target) show_commits_to_merge(target) if base.io_confirm(f"¿Crear PR de '{branch}' a '{target}'?"): if branch not in base.environments: Git("push", remote, branch, set_upstream=True).exec( print="Subiendo rama al remoto para crear PR" ) create_pull_request(token, branch, target) def run_local(branch: str, target: str): base.ensure_clean_worktree(False) check_merge_conflicts(target) show_commits_to_merge(target) if base.io_confirm(f"¿Mergear rama '{branch}' a '{target}'?"): Git("switch", target).exec(print="Cambiando a rama destino") Git("merge", branch, ff=False).exec(print="Mergeando") def show_commits_to_merge(target): commits = Git("log", target + "..", format="%s").lines() if not commits: raise GitFlowError("No hay cambios a mergear.") print("Cambios a mergear:") for commit in commits: print("- " + commit) def check_merge_conflicts(target: str): merge_conflicts = Git("merge", target, ff=False, commit=False).code( print="Realizando merge de prueba para verificar conflictos" ) Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False) if merge_conflicts: base.io_error( f"La rama actual tiene conflictos con {target}.\n" f"Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente,\n" "y ejecutar nuevamente este comando", title="Conflictos de merge", ) raise GitFlowError("Ejecución abortada.") else: base.io_success("No se detectaron merge conflicts.") def create_pull_request(token: str, branch: str, target: str): changelog = Changelog() commits = Git("log", target + "..", format="%s").lines() title = _get_pr_title(branch, commits) base.panel("Título del PR", title) if base.io_confirm("¿Desea cambiar el título del PR?", False): title = base.io_prompt("Título del PR", persistent=True) commits = Git.get_commits(target, branch) message = base.get_remote_api(token).create_pull_request( branch, target, title, changelog.generate_content(commits), branch not in base.environments, ) base.io_success(message, "Rama creada") def _get_pr_title(branch: str, commits: list[str]) -> str: if branch in base.environments: target = base.environments[base.environments.index(branch) + 1] return f"Sincronización de entorno {branch} a {target}" components = branch.split("/") # / or release/// release_prefix = f"[Pasaje a {components[1]}]" if len(components) == 4 else None branch_type = components[-2] branch_ticket, branch_desc = _extract_ticket_from_branch(components[-1]) # when there's a single commit, use it as the PR description instead of # 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: = ABC-123-my-branch-name""" first_dash = branch_desc.find("-") second_dash = branch_desc.find("-", first_dash + 1) if first_dash >= 0 and second_dash >= 0: maybe_ticket = branch_desc[:second_dash] if base.is_valid_ticket(maybe_ticket): return maybe_ticket, branch_desc[second_dash + 1 :] return None, branch_desc