gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1389Shell 137Markdown 80Rust 802602 Definiciones

mudanza: guión que le da copia FUERA a los 25 repos que sólo existen en gioser

Hace por un árbol entero lo que `scripts/espejo-setup.sh` hace por takana: `pushurl` doble sobre
`origin`, de modo que cualquier `git push origin` que ya exista empiece a espejar sin tocar ningún
script. Crea el repo en GitHub como PRIVADO.

Cuatro decisiones:

· **Por defecto NO hace nada**: imprime qué haría; hay que pedirlo con `--apply`. Crear repos y
  empujar código a un servicio externo no es algo que deba pasar por correr un script sin leerlo.
· **Varios clones del MISMO origen van a UN repo remoto.** En gioser hay cinco de
  `tawasuyu/tawasuyu`; cinco repos distintos multiplican la confusión y empujarlos todos al mismo
  `main` los haría chocar. El primario empuja normal; un secundario sólo se empuja —bajo
  `refs/heads/clon/<nombre>/*`— SI aporta historia propia, y eso se comprueba preguntándole al
  primario si ya tiene esos objetos (`cat-file -e`). Medido: los cinco tienen CERO commits que no
  estén en su remoto, así que los cuatro secundarios se saltan y se dice por qué.
· **Sólo agrega; nunca toca el `fetch` de `origin`**, que sigue siendo el canónico.
· **Lo sucio no viaja, y se dice.** Un espejo guarda historia commiteada, no el árbol de trabajo, y
  `tawasuyu` tiene 980 ficheros sin commitear. Creer que está a salvo cuando la mitad del trabajo no
  está commiteado es justo el fallo que este guión existe para evitar.

⚠ Y un criterio que escribí mal y corregí midiendo: el primario era «el clon con más commits
alcanzables», y contra los cinco de tawasuyu dio un EMPATE EXACTO en 9343 — `rev-list --all` cuenta
las refs de `origin`, que los cinco comparten, así que la métrica estaba dominada por lo común y no
medía nada. El desempate quedaba en el orden del `find` y elegía `tw-deploy` (HEAD del 6/9) sobre
`tawasuyu` (del 12/9), que es el que se está trabajando. Ahora manda la fecha del HEAD en ISO, que
distingue y ordena como texto.

Ensayo sobre gioser: 25 repos sin copia fuera ⇒ 21 a espejar, 4 saltados por redundantes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x

713128f26d4f439d89dd3f1694b5f3762b6b8e9f · por Sergio · (hace 1 mes) · padre 8600ba888 · ver el árbol en este commit

Delta semántico

Ninguna definición cambió: el cambio es de texto, documentación o datos.

1 archivos · +182 −0

diff --git a/scripts/mudanza/espejar-repos.sh b/scripts/mudanza/espejar-repos.sh
new file mode 100755
index 00000000..d85a5d47
--- /dev/null
+++ b/scripts/mudanza/espejar-repos.sh
@@ -0,0 +1,182 @@
+#!/usr/bin/env bash
+# espejar-repos.sh — le da una copia FUERA de esta máquina a cada repo que sólo existe acá (SDD 29).
+#
+# ── POR QUÉ EXISTE ──────────────────────────────────────────────────────────────────────────────
+# Medido en gioser el 2026-09-12: **28 repositorios con remoto en la propia máquina, 26 sin ninguna
+# copia fuera**. Entre ellos `tawasuyu`, que produce 10 de los 13 binarios que nadie más provee.
+# Apagar el origen no borra unos servicios: borra EL CÓDIGO CON EL QUE SE VOLVERÍAN A CONSTRUIR, y
+# los clones de trabajo están en el mismo disco que también muere.
+#
+# Hace lo mismo que `scripts/espejo-setup.sh` hace por takana —`pushurl` doble sobre `origin`, de
+# modo que cualquier `git push origin` que ya exista espeje solo— pero sobre un árbol entero.
+#
+# ── LAS CUATRO DECISIONES ───────────────────────────────────────────────────────────────────────
+# 1. **Por defecto NO hace nada.** Imprime lo que haría. Hay que pedirlo con `--apply`. Crear repos
+#    y empujar código a un servicio externo no es algo que deba pasar por correr un script sin leer.
+# 2. **Varios clones del MISMO origen van a UN repo remoto.** En gioser hay cinco clones de
+#    `tawasuyu/tawasuyu` (`tawasuyu`, `-b`, `-j1`, `-arbol`, `tw-deploy`). Crear cinco repos
+#    distintos multiplica la confusión; empujarlos todos al mismo `main` los haría chocar. El
+#    primario empuja normal y los demás van a `refs/heads/clon/<nombre>/*`: **nada se pierde y nada
+#    colisiona**.
+# 3. **Sólo agrega; nunca toca el `fetch` de `origin`.** El canónico sigue siendo el que estaba.
+# 4. **Lo sucio NO viaja, y se dice.** Un espejo guarda historia commiteada, no el árbol de trabajo:
+#    `tawasuyu` tiene 472 ficheros `.rs` modificados sin commitear. Un repo sucio se reporta con su
+#    cuenta, porque creer que está a salvo cuando la mitad del trabajo no está commiteado es
+#    exactamente el fallo que este guión existe para evitar.
+#
+# Uso:
+#   scripts/mudanza/espejar-repos.sh                      # ensayo: dice qué haría
+#   scripts/mudanza/espejar-repos.sh --apply              # crea los repos privados y empuja
+#   scripts/mudanza/espejar-repos.sh --root /mnt/vvv --owner usuario --apply
+set -uo pipefail
+
+OWNER=""; APPLY=0; ROOTS=(); DEPTH=4
+while [ $# -gt 0 ]; do
+  case "$1" in
+    --apply) APPLY=1;;
+    --owner) OWNER="$2"; shift;;
+    --root)  ROOTS+=("$2"); shift;;
+    --depth) DEPTH="$2"; shift;;
+    -h|--help) sed -n '2,30p' "$0"; exit 0;;
+    *) echo "opción desconocida: $1" >&2; exit 2;;
+  esac; shift
+done
+[ ${#ROOTS[@]} -eq 0 ] && ROOTS=(/mnt/vvv /home /opt /srv /var/www)
+
+command -v gh >/dev/null || { echo "✗ falta \`gh\`" >&2; exit 2; }
+if ! gh auth status >/dev/null 2>&1; then
+  echo "✗ \`gh\` no está autenticado. Sin token no se puede crear ni empujar." >&2; exit 2
+fi
+[ -z "$OWNER" ] && OWNER="$(gh api user -q .login 2>/dev/null)"
+[ -z "$OWNER" ] && { echo "✗ no se pudo averiguar la cuenta; pasá --owner" >&2; exit 2; }
+
+# Los nombres/IPs de ESTA máquina, para saber qué remoto es «de acá». No se cablean: se preguntan.
+MIS_IPS="$(ip -4 -o addr show scope global 2>/dev/null | awk '{split($4,a,"/"); print a[1]}')"
+es_local() {  # $1 = url
+  local h="${1#*://}"; h="${h%%/*}"; h="${h##*@}"; h="${h%%:*}"
+  [ -z "$h" ] && return 1
+  case "$h" in localhost|127.0.0.1) return 0;; esac
+  local ip; ip="$(getent ahostsv4 "$h" 2>/dev/null | awk 'NR==1{print $1}')"
+  [ -z "$ip" ] && return 1
+  echo "$MIS_IPS" | grep -qx "$ip"
+}
+
+echo "══ repos sin copia fuera de esta máquina ══"
+[ "$APPLY" = 1 ] || echo "   (ENSAYO — nada se crea ni se empuja; agregá --apply)"
+echo "   cuenta destino: $OWNER · raíces: ${ROOTS[*]}"
+echo
+
+declare -A GRUPO_PRIM GRUPO_N
+CANDIDATOS=()
+while IFS= read -r g; do
+  r="${g%/.git}"
+  urls="$(git -C "$r" config --get-regexp 'remote\..*\.(url|pushurl)' 2>/dev/null | awk '{print $2}')"
+  [ -z "$urls" ] && continue
+  dentro=""; fuera=""
+  while IFS= read -r u; do
+    [ -z "$u" ] && continue
+    if es_local "$u"; then dentro="$u"; else fuera="$u"; fi
+  done <<< "$urls"
+  [ -z "$dentro" ] && continue       # no depende de esta máquina
+  [ -n "$fuera" ] && continue        # ya tiene copia afuera: no se toca
+  # clave del grupo: la RUTA del remoto, no su host ni su puerto — cinco clones del mismo upstream
+  # llegan por URLs distintas y son el mismo proyecto.
+  ruta="${dentro#*://}"; ruta="${ruta##*@}"; ruta="${ruta#*[:/]}"; ruta="${ruta%.git}"
+  clave="$(basename "$(dirname "$ruta")")/$(basename "$ruta")"
+  CANDIDATOS+=("$clave|$r|$dentro")
+  GRUPO_N["$clave"]=$(( ${GRUPO_N["$clave"]:-0} + 1 ))
+done < <(find "${ROOTS[@]}" -maxdepth "$DEPTH" -name .git -prune 2>/dev/null)
+
+[ ${#CANDIDATOS[@]} -eq 0 ] && { echo "   nada que espejar: todos tienen copia fuera."; exit 0; }
+
+# El primario de cada grupo: el clon cuyo HEAD es MÁS NUEVO.
+#
+# ⚠ El primer criterio que escribí era «el que más commits alcanzables tiene», y contra los cinco
+# clones de tawasuyu dio un empate exacto en 9343: `rev-list --all` cuenta las refs de `origin`, que
+# los cinco comparten, así que la métrica estaba dominada por lo que tienen en común y no medía nada.
+# El desempate quedaba en manos del orden del `find` y eligió `tw-deploy` (HEAD del 6/9) sobre
+# `tawasuyu` (del 12/9), que es el que se está trabajando. La fecha del HEAD sí distingue, y es
+# determinista: se compara en ISO, que ordena como texto.
+for c in "${CANDIDATOS[@]}"; do
+  clave="${c%%|*}"; resto="${c#*|}"; r="${resto%%|*}"
+  f="$(git -C "$r" log -1 --format=%cI 2>/dev/null || echo 0)"
+  prev="${GRUPO_PRIM[$clave]:-}"
+  if [ -z "$prev" ] || [[ "$f" > "${prev##*|}" ]]; then GRUPO_PRIM["$clave"]="$r|$f"; fi
+done
+
+# ¿Un clon secundario aporta algo? Se contesta preguntándole al PRIMARIO si ya tiene esos objetos.
+# Medido: los cinco clones de tawasuyu tienen CERO commits que no estén en su remoto, así que
+# empujar cuatro copias bajo `refs/heads/clon/...` sería ruido puro. Namespacear sólo cuando hay
+# historia propia de verdad.
+aporta_algo() {  # $1 = clon secundario   $2 = primario
+  local sha
+  while IFS= read -r sha; do
+    [ -z "$sha" ] && continue
+    git -C "$2" cat-file -e "$sha" 2>/dev/null || return 0
+  done < <(git -C "$1" for-each-ref --format='%(objectname)' refs/heads refs/tags 2>/dev/null)
+  return 1
+}
+
+fallos=0; hechos=0; a_espejar=0; saltados=0; sucios=()
+for c in "${CANDIDATOS[@]}"; do
+  clave="${c%%|*}"; resto="${c#*|}"; r="${resto%%|*}"; dentro="${resto#*|}"
+  nombre="$(basename "$clave")"
+  primario="${GRUPO_PRIM[$clave]%%|*}"
+  es_prim=0; [ "$r" = "$primario" ] && es_prim=1
+  destino="https://github.com/$OWNER/$nombre.git"
+  sucio="$(git -C "$r" status --porcelain 2>/dev/null | wc -l)"
+  [ "$sucio" -gt 0 ] && sucios+=("$(basename "$r"): $sucio fichero(s) sin commitear")
+
+  if [ "$es_prim" = 1 ]; then
+    etq="→ $OWNER/$nombre"
+    refspec="--all"
+  elif aporta_algo "$r" "$primario"; then
+    etq="→ $OWNER/$nombre  bajo refs/heads/clon/$(basename "$r")/*  (tiene historia propia)"
+    refspec="refs/heads/*:refs/heads/clon/$(basename "$r")/*"
+  else
+    printf "  %-30s ·  nada propio que salvar: todo lo suyo ya está en %s\n" \
+           "$(basename "$r")" "$(basename "$primario")"
+    saltados=$((saltados+1)); continue
+  fi
+  a_espejar=$((a_espejar+1))
+  printf "  %-30s %s\n" "$(basename "$r")" "$etq"
+  [ "${GRUPO_N[$clave]}" -gt 1 ] && [ "$es_prim" = 1 ] &&
+    printf "  %-30s   (primario del grupo «%s», %s clones)\n" "" "$clave" "${GRUPO_N[$clave]}"
+
+  [ "$APPLY" = 1 ] || continue
+
+  if ! gh repo view "$OWNER/$nombre" >/dev/null 2>&1; then
+    if ! gh repo create "$OWNER/$nombre" --private >/dev/null 2>&1; then
+      echo "      ✗ no se pudo crear $OWNER/$nombre"; fallos=$((fallos+1)); continue
+    fi
+    echo "      · repo privado creado"
+  fi
+  # `--add --push` sobre `origin`: el fetch sigue siendo el canónico y todo `git push origin` que ya
+  # exista empieza a espejar sin tocar ningún script.
+  git -C "$r" remote get-url --push --all origin 2>/dev/null | grep -qxF "$destino" ||
+    git -C "$r" remote set-url --add --push origin "$destino" 2>/dev/null
+  git -C "$r" remote get-url --push --all origin 2>/dev/null | grep -qxF "$dentro" ||
+    git -C "$r" remote set-url --add --push origin "$dentro" 2>/dev/null
+  if git -C "$r" push "$destino" $refspec >/dev/null 2>&1 &&
+     git -C "$r" push "$destino" --tags >/dev/null 2>&1; then
+    echo "      ✓ empujado"; hechos=$((hechos+1))
+  else
+    echo "      ✗ el push falló"; fallos=$((fallos+1))
+  fi
+done
+
+echo
+if [ ${#sucios[@]} -gt 0 ]; then
+  echo "  ⚠ ${#sucios[@]} repo(s) con trabajo SIN COMMITEAR — el espejo NO lo lleva:"
+  printf '     %s\n' "${sucios[@]}"
+  echo "     Un espejo guarda historia commiteada, no el árbol de trabajo. Commitealo antes de"
+  echo "     apagar el origen, o ese trabajo se va con la máquina."
+fi
+if [ "$APPLY" = 1 ]; then
+  echo "  ${hechos} espejado(s) · ${fallos} fallo(s)"
+  [ "$fallos" -gt 0 ] && exit 1
+else
+  echo "  ${#CANDIDATOS[@]} repo(s) sin copia fuera: ${a_espejar} a espejar, ${saltados} sin historia propia."
+  echo "  Volvé a correrlo con --apply para crearlos (privados) y empujar."
+fi
+exit 0