import { guardedFetch, relayPack, trimUrl } from "../git/relay"; import type { RelaySource } from "../git/relay"; import { parseReportStatus, receivePackBody } from "../git/ref-sync-protocol"; import type { RefCommand } from "../git/ref-sync-protocol"; import { badRequest } from "../errors"; import { ZERO_SHA } from "./plan"; const MAX_PUSH_BYTES = 1024 * 1024 * 1024; const PUSH_TIMEOUT_MS = 10 * 60_000; export type RemoteReport = { accepted: Set; rejected: Map }; function emptyPack(): ReadableStream { return new ReadableStream({ start: (controller) => controller.close() }); } function splitReport(commands: RefCommand[], rejected: string[]): RemoteReport { const refused = new Map(); for (const entry of rejected) { const space = entry.indexOf(" "); const name = space === -1 ? entry : entry.slice(0, space); refused.set(name, space === -1 ? "rejected by the remote" : entry.slice(space + 1)); } return { accepted: new Set(commands.map((command) => command.name).filter((name) => !refused.has(name))), rejected: refused, }; } /** Deletions carry no packfile, so they go straight to receive-pack instead of through relayPack. */ async function sendDeletions(remote: RelaySource, commands: RefCommand[]): Promise { const headers: Record = { "content-type": "application/x-git-receive-pack-request", accept: "application/x-git-receive-pack-result", "user-agent": "git/gitfor", }; if (remote.authorization) headers.authorization = remote.authorization; const response = await guardedFetch(remote, `${trimUrl(remote.url)}/git-receive-pack`, { method: "POST", headers, body: receivePackBody(commands, emptyPack()), signal: AbortSignal.timeout(PUSH_TIMEOUT_MS), }); if (!response.ok) throw badRequest(`The remote refused the push (HTTP ${response.status})`); return parseReportStatus(new Uint8Array(await response.arrayBuffer())).rejected; } /** * Pushes forge refs to the remote: updates relay a pack from Artifacts' upload-pack into the remote's * receive-pack (`haves` are remote tips we already hold, so only new objects travel); deletions follow. */ export async function pushToRemote(input: { self: RelaySource; remote: RelaySource; commands: RefCommand[]; haves: string[]; }): Promise { const updates = input.commands.filter((command) => command.sha !== ZERO_SHA); const deletions = input.commands.filter((command) => command.sha === ZERO_SHA); const rejected: string[] = []; if (updates.length > 0) { const report = await relayPack({ source: input.self, target: input.remote, wants: [...new Set(updates.map((command) => command.sha))], haves: input.haves, commands: updates, maxBytes: MAX_PUSH_BYTES, }); rejected.push(...report.rejected); } if (deletions.length > 0) rejected.push(...(await sendDeletions(input.remote, deletions))); return splitReport(input.commands, rejected); }