tawasuyu

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 1572Shell 203Rust 109Markdown 993728 Definitions

⎇ 534014c3ec69daea789a1d0523e1d00a69c6ed65 scripts/fuentes/mirror-git-poblar.py Python · 137 lines · 6.6 KB History Who touched what raw

takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4 476168bb0 · Sergio ·

1#!/usr/bin/env python3
2"""Genera y sube bundles shallow de las fuentes git del corpus. Ver ADR 0013 y el .sh hermano.
3
4Es INCREMENTAL: consulta primero qué commits ya están en el mirror y sólo trabaja los que faltan.
5Interrumpible sin penalización — cada bundle es independiente.
6"""
7import os, re, subprocess, sys, glob, tempfile, shutil
8
9ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10os.chdir(ROOT)
11SB_USER = os.environ.get("SB_USER", "u647150")
12SB_HOST = os.environ.get("SB_HOST", "u647150.your-storagebox.de")
13SB_PORT = os.environ.get("SB_PORT", "23")
14KEY     = os.environ.get("KEY", os.path.expanduser("~/.ssh/github5"))
15LIMITE  = int(os.environ.get("LIMITE", "0"))
16SOLO    = os.environ.get("SOLO", "")
17DEST    = "hammer/fuentes-git"
18SSH = ["ssh", "-4", "-p", SB_PORT, "-i", KEY, "-o", "StrictHostKeyChecking=accept-new",
19       "-o", "ConnectTimeout=30"]
20
21def fuentes_git():
22    vistos, out = set(), []
23    for f in sorted(glob.glob("recipes/**/*.toml", recursive=True)):
24        s = open(f, errors="ignore").read()
25        repo = re.search(r'^\s*repo\s*=\s*"([^"]+)"', s, re.M)
26        com  = re.search(r'^\s*commit\s*=\s*"([0-9a-fA-F]{7,40})"', s, re.M)
27        if not (repo and com):
28            continue
29        nombre = (re.search(r'^\s*name\s*=\s*"([^"]+)"', s, re.M) or [None, os.path.basename(f)[:-5]])[1]
30        c = com.group(1).lower()
31        if c in vistos:              # el mismo commit desde dos colas = un solo bundle
32            continue
33        vistos.add(c)
34        out.append({"receta": nombre, "repo": repo.group(1), "commit": c})
35    return out
36
37def ya_en_mirror():
38    r = subprocess.run(SSH + [f"{SB_USER}@{SB_HOST}", f"ls {DEST}"],
39                       capture_output=True, text=True, timeout=180)
40    return {l.strip()[:-7] for l in r.stdout.splitlines() if l.strip().endswith(".bundle")}
41
42def bundle(e, tmpdir):
43    """Clona --depth 1 el commit exacto y emite un bundle.
44
45    Devuelve (ruta, None) si sale bien y (None, motivo) si no. El motivo es el stderr REAL de git,
46    no una conjetura: un fallo acá puede ser el servidor negando el fetch por sha suelto, pero
47    tambien un timeout, DNS, o un rate-limit, y cada uno se arregla distinto. Ver ADR 0013.
48    """
49    d = os.path.join(tmpdir, "r")
50    shutil.rmtree(d, ignore_errors=True)
51    os.makedirs(d)
52    def git(*a, **kw):
53        return subprocess.run(["git", "-C", d] + list(a), capture_output=True, text=True,
54                              timeout=kw.get("t", 900))
55    def paso(etiqueta, *a, **kw):
56        try:
57            r = git(*a, **kw)
58        except subprocess.TimeoutExpired:
59            return f"{etiqueta}: timeout"
60        return None if r.returncode == 0 else f"{etiqueta}: {(r.stderr or r.stdout).strip().splitlines()[-1][:160] if (r.stderr or r.stdout).strip() else 'rc=' + str(r.returncode)}"
61
62    out = os.path.join(tmpdir, f'{e["commit"]}.bundle')
63    for etiqueta, args, kw in (
64        ("init",   ("init", "-q"), {}),
65        ("remote", ("remote", "add", "origin", e["repo"]), {}),
66        # --depth 1 del SHA exacto: el servidor debe permitir fetch por sha (uploadpack.allowReachableSHA1).
67        ("fetch",  ("fetch", "-q", "--depth", "1", "origin", e["commit"]), {"t": 1800}),
68    ):
69        motivo = paso(etiqueta, *args, **kw)
70        if motivo: return None, motivo
71
72    # El campo `commit` de una receta NO siempre es un commit: puede pinear un objeto TAG anotado.
73    # Un SHA de tag es una identidad igual de buena (`git archive` lo acepta) y por eso hash_inputs
74    # no lo distingue, pero `refs/heads/*` sólo apunta a commits. Se pela para la rama y, si hubo
75    # que pelar, el objeto tag viaja aparte: sin él, el `cat-file -e <commit>` del lado de takana
76    # falla y el mirror queda correcto-pero-inútil justo para estas recetas.
77    r = git("rev-parse", e["commit"] + "^{commit}")
78    if r.returncode != 0:
79        return None, f"rev-parse: {(r.stderr or '').strip().splitlines()[-1][:160]}"
80    pelado = r.stdout.strip()
81    refs = ["refs/heads/hammer"]
82    pasos = [("update-ref", ("update-ref", "refs/heads/hammer", pelado), {})]
83    if pelado != e["commit"]:
84        refs.append("refs/tags/hammer-objeto")
85        pasos.append(("update-ref-tag", ("update-ref", "refs/tags/hammer-objeto", e["commit"]), {}))
86    pasos.append(("bundle", tuple(["bundle", "create", out] + refs), {"t": 1800}))
87    for etiqueta, args, kw in pasos:
88        motivo = paso(etiqueta, *args, **kw)
89        if motivo: return None, motivo
90    return (out, None) if os.path.exists(out) else (None, "bundle: git dijo OK pero no hay fichero")
91
92def main():
93    fs = fuentes_git()
94    if SOLO: fs = [e for e in fs if e["receta"] == SOLO]
95    print(f"== fuentes git en el corpus: {len(fs)} commits distintos", flush=True)
96    try:
97        hay = ya_en_mirror()
98    except Exception as ex:
99        print(f"!! no pude listar el mirror ({ex}); asumo vacío", flush=True); hay = set()
100    print(f"== ya en el mirror: {len(hay)}", flush=True)
101    faltan = [e for e in fs if e["commit"] not in hay]
102    if LIMITE: faltan = faltan[:LIMITE]
103    print(f"== a espejar ahora: {len(faltan)}", flush=True)
104
105    subprocess.run(SSH + [f"{SB_USER}@{SB_HOST}", f"mkdir hammer {DEST}"],
106                   capture_output=True, text=True)
107    ok = fallo = 0
108    tmpdir = tempfile.mkdtemp(prefix="hammer-git-mirror-")
109    try:
110        for i, e in enumerate(faltan, 1):
111            # Placeholder deliberado (llimphi-counter): su commit es 000…0 y no se puede espejar
112            # nunca. Se distingue del fallo real a propósito — un ✗ permanente en cada tanda entrena
113            # a no mirar los rojos, y entonces el que sí importa pasa desapercibido.
114            if set(e["commit"]) == {"0"}:
115                print(f"  [{i}/{len(faltan)}] {e['receta']:<24} · placeholder, no espejable", flush=True)
116                continue
117            b, motivo = bundle(e, tmpdir)
118            if not b:
119                fallo += 1
120                print(f"  [{i}/{len(faltan)}] {e['receta']:<24} ✗ {motivo}", flush=True)
121                continue
122            mb = os.path.getsize(b) / 1e6
123            r = subprocess.run(["rsync", "-a", "-e", " ".join(SSH), b,
124                                f"{SB_USER}@{SB_HOST}:{DEST}/"], capture_output=True, text=True, timeout=3600)
125            if r.returncode == 0:
126                ok += 1
127                print(f"  [{i}/{len(faltan)}] {e['receta']:<24} ✓ {mb:6.1f} MB", flush=True)
128            else:
129                fallo += 1
130                print(f"  [{i}/{len(faltan)}] {e['receta']:<24} ✗ subida: {r.stderr.strip()[:60]}", flush=True)
131            os.remove(b)
132    finally:
133        shutil.rmtree(tmpdir, ignore_errors=True)
134    print(f"== espejados {ok}   fallidos {fallo}")
135    return 0
136
137sys.exit(main())