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 Definitions

mirror git: soportar pines que son objetos tag anotados

diffutils, findutils-xargs y kustomize pinean el SHA de un tag, no de un commit. Rompía las dos
puntas por la misma suposición: el poblador apuntaba una rama al tag (imposible) y hammer escribía
ese SHA en el fichero shallow (que sólo admite commits).

El poblador pela con ^{commit} y manda el objeto tag aparte en refs/tags/hammer-objeto; sin él el
cat-file -e del otro lado no encuentra lo que la receta pide. hammer lee la frontera del bundle con
git bundle list-heads, que no necesita los objetos, y trae refs/*:refs/*.

Los 339 bundles del formato viejo siguen sirviendo: comprobado con una receta de cada forma contra
un repo que no resuelve por DNS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o

c2b1cafff10d4c9cf093ea1e40092fc21a935588 · by Sergio · (1 month ago) · parent 276efdece · browse the tree at this commit

Semantic delta · 3

crates/hammer-build/src/fetch.rs

scripts/fuentes/mirror-git-poblar.py

2 files · +60 −8

diff --git a/crates/hammer-build/src/fetch.rs b/crates/hammer-build/src/fetch.rs
index 0a3a3947..828923fb 100644
--- a/crates/hammer-build/src/fetch.rs
+++ b/crates/hammer-build/src/fetch.rs
@@ -152,20 +152,55 @@ fn hidratar_desde_bundle(commit: &str, destino: &Path) -> bool {
         return false;
     }
     // La frontera de historia. Sin esto el fetch aborta aunque los objetos estén completos.
-    let _ = std::fs::write(destino.join("shallow"), format!("{commit}\n"));
+    //
+    // La frontera se LEE DEL BUNDLE, no se supone. `git bundle list-heads` imprime la lista de refs
+    // sin necesitar los objetos, así que sirve justo antes del fetch. Suponerla igual a `commit`
+    // estaba mal: el campo `commit` de una receta puede pinear un OBJETO TAG anotado (así están
+    // diffutils, findutils-xargs y kustomize), y el fichero `shallow` sólo admite SHAs de commit.
+    // El poblador deja siempre `refs/heads/hammer` en el commit ya pelado.
+    let Some(frontera) = frontera_del_bundle(&tmp) else {
+        let _ = std::fs::remove_file(&tmp);
+        return false;
+    };
+    let _ = std::fs::write(destino.join("shallow"), format!("{frontera}\n"));
     // ⚠ RUTA ABSOLUTA, obligatoria. `run_git` invoca `git -C <destino> …`, y git resuelve una ruta
     // relativa de bundle DENTRO de ese `-C` — o sea buscaría `<destino>/work/repos/x.bundle.partial`.
     // El fallo es mudo aquí (se traga y se cae al remoto), así que el síntoma aparece lejos: el build
     // muere diciendo «commit … no existe en <repo> tras fetch», culpando a upstream de un error de
     // ruta local.
     let bundle_abs = std::fs::canonicalize(&tmp).unwrap_or_else(|_| tmp.clone());
-    let refspec = "refs/heads/hammer:refs/heads/hammer";
-    let ok = run_git(&["fetch", bundle_abs.to_str().unwrap(), refspec], Some(destino)).is_ok()
+    // `refs/*:refs/*` y no un ref concreto: cuando el pin es un tag anotado el bundle trae DOS refs
+    // (el commit pelado y el objeto tag), y hace falta el objeto tag para que el `cat-file -e` de
+    // abajo — y el `git archive <commit>` de después — encuentren lo que la receta pide.
+    let ok = run_git(
+        &["fetch", bundle_abs.to_str().unwrap(), "refs/*:refs/*"],
+        Some(destino),
+    )
+    .is_ok()
         && run_git(&["cat-file", "-e", commit], Some(destino)).is_ok();
     let _ = std::fs::remove_file(&tmp);
     ok
 }
 
+/// SHA del commit que `refs/heads/hammer` señala dentro del bundle, leído sin desempaquetarlo.
+fn frontera_del_bundle(bundle: &Path) -> Option<String> {
+    let abs = std::fs::canonicalize(bundle).ok()?;
+    let salida = Command::new("git")
+        .args(["bundle", "list-heads"])
+        .arg(&abs)
+        .output()
+        .ok()?;
+    if !salida.status.success() {
+        return None;
+    }
+    String::from_utf8_lossy(&salida.stdout)
+        .lines()
+        .find_map(|l| {
+            let (sha, refname) = l.split_once(char::is_whitespace)?;
+            (refname.trim() == "refs/heads/hammer").then(|| sha.trim().to_string())
+        })
+}
+
 fn fetch_tarball(
     recipe: &Recipe,
     url: &str,
diff --git a/scripts/fuentes/mirror-git-poblar.py b/scripts/fuentes/mirror-git-poblar.py
index 43dc7931..d5f3e8e1 100755
--- a/scripts/fuentes/mirror-git-poblar.py
+++ b/scripts/fuentes/mirror-git-poblar.py
@@ -61,15 +61,32 @@ def bundle(e, tmpdir):
 
     out = os.path.join(tmpdir, f'{e["commit"]}.bundle')
     for etiqueta, args, kw in (
-        ("init",      ("init", "-q"), {}),
-        ("remote",    ("remote", "add", "origin", e["repo"]), {}),
+        ("init",   ("init", "-q"), {}),
+        ("remote", ("remote", "add", "origin", e["repo"]), {}),
         # --depth 1 del SHA exacto: el servidor debe permitir fetch por sha (uploadpack.allowReachableSHA1).
-        ("fetch",     ("fetch", "-q", "--depth", "1", "origin", e["commit"]), {"t": 1800}),
-        ("update-ref",("update-ref", "refs/heads/hammer", e["commit"]), {}),
-        ("bundle",    ("bundle", "create", out, "refs/heads/hammer"), {"t": 1800}),
+        ("fetch",  ("fetch", "-q", "--depth", "1", "origin", e["commit"]), {"t": 1800}),
     ):
         motivo = paso(etiqueta, *args, **kw)
         if motivo: return None, motivo
+
+    # El campo `commit` de una receta NO siempre es un commit: puede pinear un objeto TAG anotado.
+    # Un SHA de tag es una identidad igual de buena (`git archive` lo acepta) y por eso hash_inputs
+    # no lo distingue, pero `refs/heads/*` sólo apunta a commits. Se pela para la rama y, si hubo
+    # que pelar, el objeto tag viaja aparte: sin él, el `cat-file -e <commit>` del lado de hammer
+    # falla y el mirror queda correcto-pero-inútil justo para estas recetas.
+    r = git("rev-parse", e["commit"] + "^{commit}")
+    if r.returncode != 0:
+        return None, f"rev-parse: {(r.stderr or '').strip().splitlines()[-1][:160]}"
+    pelado = r.stdout.strip()
+    refs = ["refs/heads/hammer"]
+    pasos = [("update-ref", ("update-ref", "refs/heads/hammer", pelado), {})]
+    if pelado != e["commit"]:
+        refs.append("refs/tags/hammer-objeto")
+        pasos.append(("update-ref-tag", ("update-ref", "refs/tags/hammer-objeto", e["commit"]), {}))
+    pasos.append(("bundle", tuple(["bundle", "create", out] + refs), {"t": 1800}))
+    for etiqueta, args, kw in pasos:
+        motivo = paso(etiqueta, *args, **kw)
+        if motivo: return None, motivo
     return (out, None) if os.path.exists(out) else (None, "bundle: git dijo OK pero no hay fichero")
 
 def main():