CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | /**
* Git Smart HTTP Protocol implementation.
*
* Handles the server side of:
* - GET /:owner/:repo.git/info/refs?service=git-upload-pack
* - GET /:owner/:repo.git/info/refs?service=git-receive-pack
* - POST /:owner/:repo.git/git-upload-pack
* - POST /:owner/:repo.git/git-receive-pack
*
* Reference: https://git-scm.com/docs/http-protocol
*/
import { getRepoPath } from "./repository";
function pktLine(data: string): string {
const len = (data.length + 4).toString(16).padStart(4, "0");
return `${len}${data}`;
}
function pktFlush(): string {
return "0000";
}
export function infoRefsResponse(
service: string,
advertise: string
): Response {
const body = pktLine(`# service=${service}\n`) + pktFlush() + advertise;
return new Response(body, {
headers: {
"Content-Type": `application/x-${service}-advertisement`,
"Cache-Control": "no-cache",
},
});
}
export async function getInfoRefs(
owner: string,
repo: string,
service: string
): Promise<Response> {
const repoDir = getRepoPath(owner, repo);
const proc = Bun.spawn([service, "--stateless-rpc", "--advertise-refs", repoDir], {
stdout: "pipe",
stderr: "pipe",
});
const stdout = await new Response(proc.stdout).text();
await proc.exited;
return infoRefsResponse(service, stdout);
}
export async function serviceRpc(
owner: string,
repo: string,
service: string,
body: ReadableStream<Uint8Array> | ArrayBuffer | null,
extraEnv?: Record<string, string>
): Promise<Response> {
const repoDir = getRepoPath(owner, repo);
const rawBytes = body
? body instanceof ArrayBuffer
? new Uint8Array(body)
: new Uint8Array(await new Response(body).arrayBuffer())
: new Uint8Array();
// git gzips the negotiation body once it gets large enough (a full
// clone/fetch against a repo with real history routinely crosses that
// threshold; small requests like a shallow clone don't). We were piping
// the raw, still-compressed bytes straight into `git <service>`'s stdin,
// which choked with "fatal: protocol error: bad line length character" —
// gzip's magic bytes don't parse as pkt-line. Sniff and decompress
// rather than trust Content-Encoding, since it's the actual bytes that
// matter and gzip's 2-byte magic number is unambiguous.
const inputBytes =
rawBytes.length >= 2 && rawBytes[0] === 0x1f && rawBytes[1] === 0x8b
? Bun.gunzipSync(rawBytes)
: rawBytes;
const proc = Bun.spawn([service, "--stateless-rpc", repoDir], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
});
proc.stdin.write(inputBytes);
proc.stdin.end();
// git-receive-pack (push) callers install a temp pre-receive hook and
// delete it the instant this call resolves, so pushes must keep waiting
// for the subprocess to fully exit before we hand back a Response.
// git-upload-pack (clone/fetch) has no such coupling, and buffering its
// stdout via `.arrayBuffer()` was the bug: for a repo with any real
// history the pack can be many MB, the child starts writing it to a
// pipe with a small OS buffer well before we ever start reading, and
// nothing drains stdout until the process exits -- on large enough
// packs this deadlocks and the client sees the connection die mid-clone
// (reproduced directly against this repo: shallow clone fine, full
// clone/fetch "remote end hung up unexpectedly"). Stream it straight
// through instead so the OS pipe stays drained the whole time.
if (service === "git-upload-pack") {
proc.exited.then((code) => {
if (code !== 0) {
new Response(proc.stderr)
.text()
.then((stderr) =>
console.error(
`[git] ${service} exited ${code} for ${owner}/${repo}: ${stderr}`
)
)
.catch(() => {});
}
});
return new Response(proc.stdout, {
headers: {
"Content-Type": `application/x-${service}-result`,
"Cache-Control": "no-cache",
},
});
}
const stdout = await new Response(proc.stdout).arrayBuffer();
await proc.exited;
return new Response(stdout, {
headers: {
"Content-Type": `application/x-${service}-result`,
"Cache-Control": "no-cache",
},
});
}
|