From c2b1cafff10d4c9cf093ea1e40092fc21a935588 Mon Sep 17 00:00:00 2001 From: Sergio Date: Wed, 26 Aug 2026 21:12:43 +0000 Subject: [PATCH] mirror git: soportar pines que son objetos tag anotados MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o --- crates/hammer-build/src/fetch.rs | 41 ++++++++++++++++++++++++++-- scripts/fuentes/mirror-git-poblar.py | 27 ++++++++++++++---- 2 files changed, 60 insertions(+), 8 deletions(-) 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 …`, y git resuelve una ruta // relativa de bundle DENTRO de ese `-C` — o sea buscaría `/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 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 ` 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 { + 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 ` 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():