#!/usr/bin/env bash # tbd_branch_finish — integra rama TBD a master/main y publica # # Uso: # tbd_branch_finish [] # # Opciones especiales: # --test ejecutar suite de tests internos y salir set -euo pipefail # --------------------------------------------------------------------------- # helpers # --------------------------------------------------------------------------- _tbd_finish_detect_base() { if git show-ref --verify --quiet refs/heads/master 2>/dev/null; then echo "master" elif git show-ref --verify --quiet refs/heads/main 2>/dev/null; then echo "main" else echo "" fi } _tbd_finish_require_git_repo() { if ! git rev-parse --git-dir > /dev/null 2>&1; then echo "ERROR: el directorio actual no es un repositorio git." >&2 return 1 fi } # --------------------------------------------------------------------------- # funcion principal # --------------------------------------------------------------------------- tbd_branch_finish() { local merge_title="${1:-}" # Verificar repo git _tbd_finish_require_git_repo # Detectar rama actual local branch branch=$(git branch --show-current) # Validar que es una rama TBD (issue/* o quick/*) if [[ "$branch" != issue/* && "$branch" != quick/* ]]; then if [[ "$branch" == "master" || "$branch" == "main" ]]; then echo "ERROR: ya estas en ${branch}, nada que mergear." >&2 return 1 else echo "ERROR: la rama actual '${branch}' no es una rama TBD (issue/* o quick/*)." >&2 return 1 fi fi # Verificar working tree limpio local dirty dirty=$(git status --porcelain) if [[ -n "$dirty" ]]; then echo "ERROR: hay cambios sin commitear. Haz commit antes de mergear." >&2 return 1 fi # Detectar rama base local base base=$(_tbd_finish_detect_base) if [[ -z "$base" ]]; then echo "ERROR: no se encontro rama 'master' ni 'main' en el repo local." >&2 return 1 fi # Componer titulo del merge commit local title if [[ -n "$merge_title" ]]; then title="merge: ${branch} — ${merge_title}" else title="merge: ${branch}" fi # Cambiar a base y actualizar echo "Cambiando a ${base}..." git checkout "$base" echo "Actualizando ${base}..." git pull --rebase # Merge --no-ff echo "Mergeando ${branch} en ${base}..." local merge_exit=0 git merge --no-ff "$branch" -m "$title" || merge_exit=$? if [[ $merge_exit -ne 0 ]]; then echo "merge conflict: resolver manualmente y continuar (git add + git commit)." >&2 return 2 fi # Push echo "Publicando ${base}..." local push_exit=0 git push || push_exit=$? if [[ $push_exit -ne 0 ]]; then echo "ERROR: git push fallo (exit ${push_exit}). La rama '${branch}' NO fue eliminada." >&2 return 1 fi # Eliminar rama local git branch -d "$branch" echo "Rama '${branch}' integrada a ${base} y publicada. Rama local eliminada." return 0 } # --------------------------------------------------------------------------- # modo --test # --------------------------------------------------------------------------- _tbd_branch_finish_tests() { local PASS=0 local FAIL=0 _assert_eq() { local name="$1" expected="$2" got="$3" if [[ "$expected" == "$got" ]]; then echo "PASS: $name" PASS=$((PASS + 1)) else echo "FAIL: $name — expected '${expected}', got '${got}'" FAIL=$((FAIL + 1)) fi } _assert_exit() { local name="$1" expected_exit="$2" shift 2 local got_exit=0 "$@" > /dev/null 2>&1 || got_exit=$? if [[ "$expected_exit" == "$got_exit" ]]; then echo "PASS: $name (exit ${got_exit})" PASS=$((PASS + 1)) else echo "FAIL: $name — expected exit ${expected_exit}, got ${got_exit}" FAIL=$((FAIL + 1)) fi } # --------------------------------------------------------------------------- # Setup: bare remote + repo de trabajo # --------------------------------------------------------------------------- local tmpdir tmpdir=$(mktemp -d) trap "rm -rf '$tmpdir'" EXIT local bare="$tmpdir/remote.git" local work="$tmpdir/work" # Crear remote bare git -c init.defaultBranch=master init --bare -q "$bare" # Clonar para tener origin git clone -q "$bare" "$work" ( cd "$work" git config user.email "test@test.com" git config user.name "Test" echo "init" > README.md git add README.md git commit -q -m "chore: init" git push -q -u origin master ) # --------------------------------------------------------------------------- # Test: merge issue branch sin titulo # --------------------------------------------------------------------------- ( cd "$work" git checkout -q -b issue/0001-add-feature echo "feature" > feature.txt git add feature.txt git commit -q -m "feat: add feature" ) local finish_exit=0 ( cd "$work" tbd_branch_finish ) > /dev/null 2>&1 || finish_exit=$? local current current=$(cd "$work" && git branch --show-current) _assert_eq "finish lands on base branch" "master" "$current" local branch_gone=0 (cd "$work" && git show-ref --verify --quiet refs/heads/issue/0001-add-feature) && branch_gone=1 || branch_gone=0 _assert_eq "issue branch deleted locally" "0" "$branch_gone" _assert_eq "finish exits 0" "0" "$finish_exit" # Verificar merge commit en remote local remote_log remote_log=$(cd "$work" && git log --oneline -2 origin/master) local has_merge=0 [[ "$remote_log" == *"merge: issue/0001-add-feature"* ]] && has_merge=1 || has_merge=0 _assert_eq "merge commit pushed to remote" "1" "$has_merge" # --------------------------------------------------------------------------- # Test: merge quick branch con titulo # --------------------------------------------------------------------------- ( cd "$work" git checkout -q -b quick/fix-typo echo "fix" > fix.txt git add fix.txt git commit -q -m "fix: typo" ) ( cd "$work" tbd_branch_finish "corregir typo en README" ) > /dev/null 2>&1 current=$(cd "$work" && git branch --show-current) _assert_eq "quick branch finish lands on master" "master" "$current" remote_log=$(cd "$work" && git log --oneline -2 origin/master) local has_title=0 [[ "$remote_log" == *"merge: quick/fix-typo — corregir typo en README"* ]] && has_title=1 || has_title=0 _assert_eq "merge title included in commit" "1" "$has_title" # --------------------------------------------------------------------------- # Test: dirty tree → exit 1 # --------------------------------------------------------------------------- ( cd "$work" git checkout -q -b quick/dirty-test ) echo "dirty" > "$work/dirty.txt" _assert_exit "dirty tree exits 1" 1 bash -c " cd '$work' source '${BASH_SOURCE[0]}' tbd_branch_finish " rm -f "$work/dirty.txt" (cd "$work" && git checkout -q master) (cd "$work" && git branch -D quick/dirty-test 2>/dev/null || true) # --------------------------------------------------------------------------- # Test: ya en master → exit 1 # --------------------------------------------------------------------------- _assert_exit "on master exits 1" 1 bash -c " cd '$work' source '${BASH_SOURCE[0]}' tbd_branch_finish " # --------------------------------------------------------------------------- # Test: rama no TBD → exit 1 # --------------------------------------------------------------------------- (cd "$work" && git checkout -q -b feature/non-tbd 2>/dev/null) _assert_exit "non-TBD branch exits 1" 1 bash -c " cd '$work' source '${BASH_SOURCE[0]}' tbd_branch_finish " (cd "$work" && git checkout -q master && git branch -D feature/non-tbd 2>/dev/null || true) # --------------------------------------------------------------------------- # Test: funciona con 'main' como rama base # --------------------------------------------------------------------------- local tmpdir2 tmpdir2=$(mktemp -d) local bare2="$tmpdir2/remote.git" local work2="$tmpdir2/work" git -c init.defaultBranch=main init --bare -q "$bare2" git clone -q "$bare2" "$work2" ( cd "$work2" git config user.email "test@test.com" git config user.name "Test" echo "init" > README.md git add README.md git commit -q -m "chore: init" git push -q -u origin main git checkout -q -b quick/use-main echo "x" > x.txt git add x.txt git commit -q -m "feat: x" tbd_branch_finish ) > /dev/null 2>&1 current=$(cd "$work2" && git branch --show-current) _assert_eq "works with main as base" "main" "$current" rm -rf "$tmpdir2" echo "---" echo "Results: ${PASS} passed, ${FAIL} failed" [[ $FAIL -eq 0 ]] || exit 1 } # --------------------------------------------------------------------------- # entry point # --------------------------------------------------------------------------- if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then if [[ "${1:-}" == "--test" ]]; then _tbd_branch_finish_tests else tbd_branch_finish "$@" fi fi