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 | /**
* Git Smart HTTP routes.
*
* Mounted at /:owner/:repo.git/ — handles clone, fetch, push.
*/
import { Hono } from "hono";
import { getInfoRefs, serviceRpc } from "../git/protocol";
import { repoExists } from "../git/repository";
import { onPostReceive } from "../hooks/post-receive";
import { invalidateRepoCache } from "../lib/cache";
const git = new Hono();
// Discovery: GET /:owner/:repo.git/info/refs?service=...
git.get("/:owner/:repo.git/info/refs", async (c) => {
const { owner, repo } = c.req.param();
const service = c.req.query("service");
if (!service || !["git-upload-pack", "git-receive-pack"].includes(service)) {
return c.text("Invalid service", 400);
}
if (!(await repoExists(owner, repo))) {
return c.text("Repository not found", 404);
}
return getInfoRefs(owner, repo, service);
});
// GET /:owner/:repo.git/HEAD
git.get("/:owner/:repo.git/HEAD", async (c) => {
const { owner, repo } = c.req.param();
if (!(await repoExists(owner, repo))) {
return c.text("Repository not found", 404);
}
const path = `repos/${owner}/${repo}.git/HEAD`;
const file = Bun.file(path);
if (!(await file.exists())) return c.text("Not found", 404);
return c.text(await file.text());
});
// Upload pack (clone/fetch)
git.post("/:owner/:repo.git/git-upload-pack", async (c) => {
const { owner, repo } = c.req.param();
if (!(await repoExists(owner, repo))) {
return c.text("Repository not found", 404);
}
return serviceRpc(owner, repo, "git-upload-pack", c.req.raw.body);
});
// Receive pack (push)
git.post("/:owner/:repo.git/git-receive-pack", async (c) => {
const { owner, repo } = c.req.param();
if (!(await repoExists(owner, repo))) {
return c.text("Repository not found", 404);
}
// Parse the incoming refs from the request body before passing to git
const bodyBuffer = await c.req.arrayBuffer();
const response = await serviceRpc(
owner,
repo,
"git-receive-pack",
bodyBuffer
);
// Invalidate cached git data for this repo immediately
invalidateRepoCache(owner, repo);
// Fire post-receive hooks asynchronously (don't block response)
// We parse updated refs from the pkt-line protocol in the request
const refs = parseReceivePackRefs(new Uint8Array(bodyBuffer));
if (refs.length > 0) {
onPostReceive(owner, repo, refs).catch((err) =>
console.error("[post-receive] hook error:", err)
);
}
return response;
});
/**
* Parse ref updates from git-receive-pack request body.
* Format: <old-sha> <new-sha> <ref-name>
*/
function parseReceivePackRefs(
data: Uint8Array
): Array<{ oldSha: string; newSha: string; refName: string }> {
const text = new TextDecoder().decode(data);
const refs: Array<{ oldSha: string; newSha: string; refName: string }> = [];
// Pkt-line format: 4-hex-length followed by data
let offset = 0;
while (offset < text.length) {
const lenHex = text.slice(offset, offset + 4);
const len = parseInt(lenHex, 16);
if (len === 0) {
offset += 4;
break; // flush packet
}
if (len < 4) break;
const line = text.slice(offset + 4, offset + len);
offset += len;
// Match: <old-sha> <new-sha> <ref-name>\0<capabilities>
// or: <old-sha> <new-sha> <ref-name>
const match = line.match(
/^([0-9a-f]{40}) ([0-9a-f]{40}) ([^\0\n]+)/
);
if (match) {
refs.push({
oldSha: match[1],
newSha: match[2],
refName: match[3].trim(),
});
}
}
return refs;
}
export default git;
|