import { concatBytes, FLUSH_PKT, packetText, parseAllPackets, pktLine } from "./pktline"; const ZERO_SHA = "0000000000000000000000000000000000000000"; const PEELED_SUFFIX = "^{}"; const PACK_MAGIC = [0x50, 0x41, 0x43, 0x4b]; const MAX_PREAMBLE_BYTES = 64 * 1024; const decoder = new TextDecoder(); export type AdvertisedRef = { name: string; sha: string }; /** Every branch and tag in a v0 upload-pack advertisement, using the tag object (not the peeled commit) for annotated tags. */ export function parseSyncableRefs(body: Uint8Array): AdvertisedRef[] { const refs: AdvertisedRef[] = []; for (const packet of parseAllPackets(body)) { const line = packetText(packet); if (line === null || line.startsWith("#")) continue; const [refPart] = line.split("\0"); const [sha, name] = refPart.split(" "); if (!sha || !name || sha === ZERO_SHA || name.endsWith(PEELED_SUFFIX)) continue; if (name.startsWith("refs/heads/") || name.startsWith("refs/tags/")) refs.push({ name, sha }); } return refs; } /** * A stateless v0 upload-pack request. No side-band and no thin-pack, so the response is a plain, * self-contained packfile that receive-pack will accept verbatim. */ export function uploadPackRequest(wants: string[], haves: string[]): Uint8Array { const lines: Uint8Array[] = wants.map((sha, index) => pktLine(index === 0 ? `want ${sha} ofs-delta no-progress agent=gitfor\n` : `want ${sha}\n`), ); lines.push(FLUSH_PKT); for (const sha of haves) lines.push(pktLine(`have ${sha}\n`)); lines.push(pktLine("done\n")); return concatBytes(lines); } function packStart(buffer: Uint8Array): number { for (let index = 0; index + PACK_MAGIC.length <= buffer.byteLength; index++) { if (PACK_MAGIC.every((byte, offset) => buffer[index + offset] === byte)) return index; } return -1; } /** Drops the NAK/ACK pkt-lines that precede the packfile in an upload-pack response, streaming the rest. */ export async function packStream(body: ReadableStream): Promise> { const reader = body.getReader(); let buffered = new Uint8Array(0); while (true) { const { value, done } = await reader.read(); if (done) throw new Error(`Source sent no packfile: ${decoder.decode(buffered).slice(0, 200)}`); buffered = concatBytes([buffered, value]); const start = packStart(buffered); if (start !== -1) { const head = buffered.subarray(start); return new ReadableStream({ start(controller) { controller.enqueue(head); }, async pull(controller) { const next = await reader.read(); if (next.done) controller.close(); else controller.enqueue(next.value); }, cancel(reason) { return reader.cancel(reason); }, }); } if (buffered.byteLength > MAX_PREAMBLE_BYTES) throw new Error("Source response did not contain a packfile"); } } /** receive-pack body: ref-creation commands followed by the packfile stream. */ export function receivePackBody(refs: AdvertisedRef[], pack: ReadableStream): ReadableStream { const commands = concatBytes([ ...refs.map((ref, index) => pktLine( index === 0 ? `${ZERO_SHA} ${ref.sha} ${ref.name}\0report-status agent=gitfor\n` : `${ZERO_SHA} ${ref.sha} ${ref.name}\n`, ), ), FLUSH_PKT, ]); const reader = pack.getReader(); let sentCommands = false; return new ReadableStream({ async pull(controller) { if (!sentCommands) { sentCommands = true; controller.enqueue(commands); return; } const next = await reader.read(); if (next.done) controller.close(); else controller.enqueue(next.value); }, cancel(reason) { return reader.cancel(reason); }, }); } /** Refs that `ok`'d and the first rejection, from an un-sidebanded report-status response. */ export function parseReportStatus(body: Uint8Array): { unpackOk: boolean; accepted: number; rejected: string[] } { let unpackOk = false; let accepted = 0; const rejected: string[] = []; for (const packet of parseAllPackets(body)) { const line = packetText(packet); if (line === null) continue; if (line.startsWith("unpack ")) unpackOk = line === "unpack ok"; else if (line.startsWith("ok ")) accepted++; else if (line.startsWith("ng ")) rejected.push(line.slice(3)); } return { unpackOk, accepted, rejected }; }