#!/usr/bin/env bash # git-sincronizar.sh — publicar un commit propio en un árbol COMPARTIDO sin perderlo. # # ── POR QUÉ EXISTE ────────────────────────────────────────────────────────────────────────────── # Este repo lo trabajan varios agentes a la vez sobre el MISMO árbol, y ahí `git pull --rebase` se # lleva commits por delante: el 2026-09-16/17 pasó CINCO veces, siempre con el mismo reflog — # `commit:` propio, `(start): checkout `, `(finish): returning to main`, y **ni un `pick` en el # medio**. El comando no es el culpable (probado en un repo de juguete: ahí reaplica bien); lo que # falla es que alguien mueve `refs/heads/main` mientras el rebase está en vuelo. # # La receta manual está en CLAUDE.md regla 2 ter y funciona: mirar el log después del pull y, si el # commit no está, `git reflog` + `git cherry-pick`. Esto es esa receta, automatizada, porque una # regla que hay que acordarse de aplicar en cada push es una regla que un día no se aplica. # # ── QUÉ HACE, EN ORDEN ────────────────────────────────────────────────────────────────────────── # 1. anota el sha del HEAD propio ANTES de tocar nada; # 2. `pull --rebase`; # 3. **comprueba que ese commit sigue en la rama** (por `git cherry`, que compara por parche, no # por sha: el rebase lo reescribe); # 4. si no está, lo recupera con `cherry-pick` desde el reflog y lo vuelve a comprobar; # 5. empuja, y si el ESPEJO de GitHub quedó divergente lo realinea con `--force-with-lease` # —nunca `--force` a secas— después de verificar que no se pierde contenido. # # Uso: scripts/git-sincronizar.sh # sincroniza y empuja el HEAD actual set -euo pipefail cd "$(cd "$(dirname "$0")/.." && pwd)" TMPERR=$(mktemp); trap 'rm -f "$TMPERR"' EXIT MIO=$(git rev-parse HEAD) MSG=$(git log -1 --format=%s "$MIO") echo "==> mi HEAD: ${MIO:0:9} · $MSG" git fetch -q origin main if git merge-base --is-ancestor "$MIO" FETCH_HEAD 2>/dev/null; then echo " ya está publicado; nada que rebasar" else git pull --rebase -q origin main || { echo "!! el rebase falló — resolvé a mano"; exit 1; } # `git cherry` compara por PARCHE: el rebase le cambia el sha al commit, así que buscarlo por sha # daría un falso negativo y esto entraría en un bucle de cherry-picks duplicados. # ⚠ NADA DE `git log … | grep -q` ACÁ, y la causa es sutil: este script corre con `set -o # pipefail`, y `grep -q` SALE EN CUANTO ENCUENTRA — lo que le manda SIGPIPE a `git log`, que # termina ≠0, y con `pipefail` la tubería entera se lee como FALLO **aunque el grep haya # encontrado**. Resultado: el guión anunciaba «EL COMMIT DESAPARECIÓ» en cada corrida, se iba a # recuperarlo, y el `cherry-pick` le decía que ya estaba. Un guardián que grita siempre se ignora. # La salida se captura primero y se compara después: sin tubería, sin SIGPIPE, sin falso positivo. RECIENTES=$(git log --format=%s -20) if printf '%s\n' "$RECIENTES" | grep -qF "$MSG"; then echo " ✓ el commit sobrevivió al rebase" else echo "!! el commit no aparece en los últimos 20 tras el rebase — intento recuperarlo" # ⚠ `cherry-pick` puede salir con «the previous cherry-pick is now empty»: eso NO es un fallo, # es que el CONTENIDO ya estaba en el árbol (la comprobación por mensaje puede errar cuando el # rebase lo dejó más atrás de 20 commits, o cuando otro agente lo trajo por su lado). Se aborta # el cherry-pick y se sigue: lo que importa es que el contenido esté, no que el sha coincida. if git cherry-pick "$MIO" 2>"$TMPERR"; then echo " ✓ recuperado" elif grep -q "now empty\|nothing to commit" "$TMPERR"; then git cherry-pick --abort 2>/dev/null || true echo " ✓ el contenido ya estaba (cherry-pick vacío): falso positivo de la comprobación" else git cherry-pick --abort 2>/dev/null || true echo "!! no pude recuperarlo. Está en el reflog:"; git reflog -8 | sed "s/^/ /"; exit 1 fi fi fi git push -q origin main 2>/dev/null || true GITEA=$(git ls-remote --heads origin 2>/dev/null | cut -f1) LOCAL=$(git rev-parse HEAD) [ "$GITEA" = "$LOCAL" ] || { echo "!! gitea quedó en ${GITEA:0:9} y yo en ${LOCAL:0:9} — revisalo"; exit 1; } echo " gitea ✓ ${GITEA:0:9}" # ── el espejo ─────────────────────────────────────────────────────────────────────────────────── # El `push` con doble `pushurl` puede triunfar en un remoto y fallar en el otro; cuando pasa, el # espejo se queda con un sha huérfano. Se realinea con lease (verifica que el remoto sigue donde # creemos) y sólo después de comprobar que su contenido ya está en nuestra historia. ESPEJO_URL=$(git remote get-url --push --all origin | grep -i github || true) if [ -n "$ESPEJO_URL" ]; then GH=$(git ls-remote --heads "$ESPEJO_URL" 2>/dev/null | cut -f1 || true) if [ -n "$GH" ] && [ "$GH" != "$LOCAL" ]; then if git merge-base --is-ancestor "$GH" "$LOCAL" 2>/dev/null; then git push -q "$ESPEJO_URL" main && echo " espejo ✓ (avance normal)" else echo " espejo divergente (${GH:0:9}); realineando con lease" git push -q --force-with-lease=main:"$GH" "$ESPEJO_URL" main && echo " espejo ✓ realineado" fi else echo " espejo ✓ ${GH:0:9}" fi fi