import childProcess from "node:child_process"; import crypto from "node:crypto"; import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat } from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; import type { CapabilityRepositoryResolver } from "./assembly.js"; const execFile = promisify(childProcess.execFile); const sourceKey = (kind: string, repository: string, commit: string) => `${kind}\0${repository}\0${commit.toLowerCase()}`; const checkoutName = (kind: string, repository: string, commit: string) => { const digest = crypto .createHash("sha256") .update(sourceKey(kind, repository, commit)) .digest("hex") .slice(0, 24); return `${kind}-${digest}`; }; export const createGitCapabilityResolver = async (options: { checkoutRoot: string; snapshotMap?: string; snapshotOnly?: boolean; }): Promise => { await mkdir(options.checkoutRoot, { recursive: true }); const checkoutRoot = await realpath(options.checkoutRoot); const snapshots = new Map(); if (options.snapshotMap) { const snapshotMapPath = await realpath(options.snapshotMap); const document = JSON.parse(await readFile(snapshotMapPath, "utf8")) as { resources?: Array<{ kind: string; repository: string; commit: string; directory: string; }>; }; for (const entry of document.resources ?? []) { if (entry.kind !== "interface" && entry.kind !== "package") { throw new Error(`Snapshot map has unsupported resource kind ${entry.kind}`); } const key = sourceKey(entry.kind, entry.repository, entry.commit); if (snapshots.has(key)) throw new Error(`Snapshot map repeats ${key}`); const directory = path.resolve(path.dirname(snapshotMapPath), entry.directory); snapshots.set(key, await realpath(directory)); } } const checkouts = new Map>(); return async (source, kind) => { const key = sourceKey(kind, source.repository, source.commit); const snapshot = snapshots.get(key); if (snapshot) return { directory: snapshot }; if (options.snapshotOnly) throw new Error(`No offline snapshot for ${kind} ${source.repository}@${source.commit}`); const existing = checkouts.get(key); if (existing) return await existing; const pending = (async () => { const directory = path.join( checkoutRoot, checkoutName(kind, source.repository, source.commit), ); const verify = async (checkout: string) => { const { stdout } = await execFile("git", ["-C", checkout, "rev-parse", "HEAD"]); if (stdout.trim().toLowerCase() !== source.commit.toLowerCase()) { throw new Error(`Locked commit mismatch for ${source.repository}: wanted ${source.commit}, fetched ${stdout.trim()}`); } const { stdout: changes } = await execFile("git", ["-C", checkout, "status", "--porcelain", "--untracked-files=all"]); if (changes.trim()) throw new Error(`Dependency checkout was modified: ${checkout}`); }; // Only complete, checked clones become visible under the deterministic name. // Concurrent resolvers may fetch independently, but cannot observe a partial clone. if (await stat(directory).then(() => true, (error: NodeJS.ErrnoException) => { if (error.code === "ENOENT") return false; throw error; })) { await verify(directory); return { directory }; } const staging = await mkdtemp(path.join(checkoutRoot, ".fetch-")); const checkout = path.join(staging, "checkout"); try { await execFile("git", [ "-c", "advice.detachedHead=false", "clone", "--depth", "1", "--single-branch", "--branch", `quixos-reachability/${source.commit.toLowerCase()}`, source.repository, checkout, ]); await verify(checkout); try { await rename(checkout, directory); } catch (error) { if (!["EEXIST", "ENOTEMPTY"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error; await verify(directory); } } finally { await rm(staging, { recursive: true, force: true }); } return { directory }; })(); checkouts.set(key, pending); try { return await pending; } catch (error) { checkouts.delete(key); throw error; } }; };