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…
⎇ b89695739b0547d8d01b3438a9d800affd2fdf61
scripts/fuentes/upstream-bump.py
Historia
Quién tocó qué
crudo
SDD 33 + la vigía de río arriba: 660 fuentes git en 17 s, y 462 tienen algo más nuevo
1#!/usr/bin/env python3
2# upstream-bump.py — sube el pin de lo NUESTRO, de a un repositorio entero.
3#
4# ══ QUÉ SUBE Y QUÉ NO ═════════════════════════════════════════════════════════════════════════
5# Sólo recetas cuyo `repo` case con `--only` (por defecto `tawasuyu`). **Terceros no**, y no es
6# pereza: una versión nueva de upstream puede cambiar el sistema de build, las banderas, las
7# dependencias y el resultado, así que aplicarla sola es cambiar la receta sin leerla. Para eso
8# está la vigía, que PROPONE (SDD 33 §3).
9#
10# ⚠ **EL BUMP ES ATÓMICO POR REPOSITORIO, y ésa es la razón de que este guion exista.** tawasuyu
11# son 37 recetas sobre el MISMO monorepo. Subirlas de a una da una imagen con 37 commits distintos
12# del mismo árbol — una mezcla que nunca existió junta y que nadie probó. O saltan todas al mismo
13# commit, o no salta ninguna.
14#
15# No construye y no commitea: deja el árbol modificado y dice qué tocó. Construir es del worker
16# (con su `flock`) y commitear es de quien mira el resultado.
17#
18# Uso: scripts/fuentes/upstream-bump.py [--only tawasuyu] [--branch main] [--apply]
19# Sin `--apply` no escribe nada: dice qué haría.
20import argparse, glob, os, re, subprocess, sys
21
22RAIZ = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
23
24
25def recetas():
26 for r in sorted(set(glob.glob(os.path.join(RAIZ, 'recipes/*.toml'))
27 + glob.glob(os.path.join(RAIZ, 'recipes/*/*.toml')))):
28 txt = open(r, encoding='utf-8', errors='replace').read()
29 repo = re.search(r'^\s*repo\s*=\s*"([^"]+)"', txt, re.M)
30 com = re.search(r'^(\s*commit\s*=\s*")([0-9a-fA-F]{7,40})(")', txt, re.M)
31 if repo and com:
32 yield os.path.relpath(r, RAIZ), repo.group(1), com.group(2), txt
33
34
35def cabeza(repo, rama, timeout=30):
36 env = dict(os.environ, GIT_TERMINAL_PROMPT='0', GIT_ASKPASS='/bin/true')
37 p = subprocess.run(['git', 'ls-remote', repo, 'refs/heads/' + rama],
38 capture_output=True, text=True, timeout=timeout, env=env)
39 if p.returncode != 0 or not p.stdout.strip():
40 return None
41 return p.stdout.split()[0]
42
43
44def main():
45 ap = argparse.ArgumentParser(description='sube el pin de un repositorio propio, entero')
46 ap.add_argument('--only', default='tawasuyu', help='subcadena que el `repo` tiene que contener')
47 ap.add_argument('--branch', default='main')
48 ap.add_argument('--apply', action='store_true', help='escribir; sin esto es un ensayo')
49 a = ap.parse_args()
50
51 porrepo = {}
52 for receta, repo, commit, txt in recetas():
53 if a.only not in repo:
54 continue
55 porrepo.setdefault(repo, []).append((receta, commit, txt))
56 if not porrepo:
57 print('no hay ninguna receta cuyo repo contenga «%s»' % a.only)
58 return 0
59
60 tocadas = 0
61 for repo, filas in sorted(porrepo.items()):
62 nuevo = cabeza(repo, a.branch)
63 if not nuevo:
64 print('✗ %s — no pude leer refs/heads/%s' % (repo, a.branch))
65 continue
66 pins = sorted({c for _, c, _ in filas})
67 print('== %s' % repo)
68 print(' %d receta(s) · %d pin(s) distinto(s) · cabeza de %s: %s' %
69 (len(filas), len(pins), a.branch, nuevo[:12]))
70 # ⚠ Si las recetas del mismo repo ya venían con pins DISTINTOS, el árbol estaba mezclado
71 # antes de que este guion tocara nada. Subirlas todas lo arregla, pero se dice: es un dato
72 # sobre cómo se llegó hasta acá, y callarlo haría parecer que el estado anterior era sano.
73 if len(pins) > 1:
74 print(' ⚠ venían con %d pins distintos: el árbol ya estaba mezclado' % len(pins))
75 if len(pins) == 1 and pins[0] == nuevo:
76 print(' = ya está en la cabeza; nada que hacer')
77 continue
78 for receta, commit, txt in filas:
79 if commit == nuevo:
80 continue
81 nuevotxt = re.sub(r'^(\s*commit\s*=\s*")([0-9a-fA-F]{7,40})(")',
82 lambda m: m.group(1) + nuevo + m.group(3), txt, count=1, flags=re.M)
83 if nuevotxt == txt:
84 print(' ✗ %s — no pude reescribir el commit' % receta)
85 continue
86 if a.apply:
87 open(os.path.join(RAIZ, receta), 'w', encoding='utf-8').write(nuevotxt)
88 print(' %s %-44s %s… → %s…' % ('·' if a.apply else '~', receta, commit[:10], nuevo[:10]))
89 tocadas += 1
90
91 print()
92 if a.apply:
93 print('%d receta(s) reescritas. NO se construyó ni se commiteó nada:' % tocadas)
94 print(' · construir es del worker, con su flock')
95 print(' · commitear es de quien mire el resultado (y acotado por pathspec, regla 2)')
96 else:
97 print('%d receta(s) subirían. Ensayo: no se tocó nada (pasá --apply).' % tocadas)
98 return 0
99
100
101if __name__ == '__main__':
102 sys.exit(main())