File
Blob: test/ls-refs-filters.worker.test.ts
| 1 | import { it, expect } from "vitest"; |
| 2 | import { env, exports as workerExports } from "cloudflare:workers"; |
| 3 | import { pktLine, delimPkt, flushPkt, concatChunks, encodeGitObject } from "@/worker/git"; |
| 4 | import { uniqueRepoId, runDOWithRetry, toRequestBody } from "./util/test-helpers"; |
| 5 | import { setupRepoForTests } from "./util/repoSeed"; |
| 6 | import { registerTestPack } from "./util/packed-repo"; |
| 7 | import { decodePktLinePayloads } from "./util/fetch-protocol"; |
| 8 | |
| 9 | function buildLsRefsBody(args: string[] = []) { |
| 10 | const chunks: Uint8Array[] = []; |
| 11 | chunks.push(pktLine("command=ls-refs\n")); |
| 12 | chunks.push(delimPkt()); |
| 13 | for (const a of args) chunks.push(pktLine(a + "\n")); |
| 14 | chunks.push(flushPkt()); |
| 15 | return concatChunks(chunks); |
| 16 | } |
| 17 | |
| 18 | it("ls-refs: ref-prefix filters refs and peel adds peeled attribute for annotated tags", async () => { |
| 19 | const owner = "o"; |
| 20 | const repo = uniqueRepoId("r-lsrefs-filters"); |
| 21 | await setupRepoForTests(env, owner, repo); |
| 22 | const repoId = `${owner}/${repo}`; |
| 23 | |
| 24 | // Seed minimal repo to create HEAD -> refs/heads/main |
| 25 | const id = env.REPO_DO.idFromName(repoId); |
| 26 | const { commitOid } = await runDOWithRetry( |
| 27 | () => env.REPO_DO.get(id), |
| 28 | async (instance) => instance.seedMinimalRepo() |
| 29 | ); |
| 30 | |
| 31 | // Create an annotated tag pointing to the commit |
| 32 | const tagPayload = new TextEncoder().encode( |
| 33 | `object ${commitOid}\n` + |
| 34 | `type commit\n` + |
| 35 | `tag v1\n` + |
| 36 | `tagger You <you@example.com> 0 +0000\n\nmsg\n` |
| 37 | ); |
| 38 | const { oid: tagOid } = await encodeGitObject("tag", tagPayload); |
| 39 | |
| 40 | // Register the annotated tag in a pack (post-closure: no loose objects from push) |
| 41 | await registerTestPack({ |
| 42 | env, |
| 43 | repoId, |
| 44 | getStub: () => env.REPO_DO.get(id), |
| 45 | packName: `pack-tag-${Date.now()}.pack`, |
| 46 | objects: [{ type: "tag" as const, payload: tagPayload }], |
| 47 | }); |
| 48 | // Add the tag ref |
| 49 | await runDOWithRetry( |
| 50 | () => env.REPO_DO.get(id), |
| 51 | async (instance) => { |
| 52 | const { refs } = await instance.getHeadAndRefs(); |
| 53 | refs.push({ name: "refs/tags/v1", oid: tagOid }); |
| 54 | await instance.setRefs(refs); |
| 55 | } |
| 56 | ); |
| 57 | |
| 58 | // Ask only for refs/tags/* and request peeling |
| 59 | const body = buildLsRefsBody(["ref-prefix refs/tags/", "peel"]); |
| 60 | const url = `https://example.com/${owner}/${repo}/git-upload-pack`; |
| 61 | const res = await workerExports.default.fetch(url, { |
| 62 | method: "POST", |
| 63 | headers: { |
| 64 | "Content-Type": "application/x-git-upload-pack-request", |
| 65 | "Git-Protocol": "version=2", |
| 66 | }, |
| 67 | body: toRequestBody(body), |
| 68 | }); |
| 69 | expect(res.status).toBe(200); |
| 70 | const lines = decodePktLinePayloads(new Uint8Array(await res.arrayBuffer())); |
| 71 | |
| 72 | // HEAD should be first and present |
| 73 | expect(lines[0]?.startsWith("unborn HEAD") || lines[0]?.startsWith(commitOid + " HEAD")).toBe( |
| 74 | true |
| 75 | ); |
| 76 | // The tag must be listed and include peeled:<commitOid> |
| 77 | const tagLine = lines.find((l: string) => l.includes(" refs/tags/v1")); |
| 78 | expect(tagLine).toBeDefined(); |
| 79 | expect(tagLine!.includes(`peeled:${commitOid}`)).toBe(true); |
| 80 | // No branch refs (refs/heads/*) beyond HEAD line |
| 81 | expect(lines.slice(1).some((l: string) => /\srefs\/heads\//.test(l))).toBe(false); |
| 82 | }); |
| 83 | |
| 84 | it("ls-refs: peel resolves many annotated tags stored across multiple packs", async () => { |
| 85 | const owner = "o"; |
| 86 | const repo = uniqueRepoId("r-lsrefs-many-tags"); |
| 87 | await setupRepoForTests(env, owner, repo); |
| 88 | const repoId = `${owner}/${repo}`; |
| 89 | const id = env.REPO_DO.idFromName(repoId); |
| 90 | const getStub = () => env.REPO_DO.get(id); |
| 91 | |
| 92 | const { commitOid } = await runDOWithRetry(getStub, async (instance) => |
| 93 | instance.seedMinimalRepo() |
| 94 | ); |
| 95 | |
| 96 | const tagCount = 48; |
| 97 | const refs: { name: string; oid: string }[] = []; |
| 98 | const firstPackObjects: Array<{ type: "tag"; payload: Uint8Array }> = []; |
| 99 | const secondPackObjects: Array<{ type: "tag"; payload: Uint8Array }> = []; |
| 100 | |
| 101 | for (let index = 0; index < tagCount; index++) { |
| 102 | const tagName = `v${index.toString().padStart(2, "0")}`; |
| 103 | const tagPayload = new TextEncoder().encode( |
| 104 | `object ${commitOid}\n` + |
| 105 | `type commit\n` + |
| 106 | `tag ${tagName}\n` + |
| 107 | `tagger You <you@example.com> 0 +0000\n\nmsg ${tagName}\n` |
| 108 | ); |
| 109 | const { oid } = await encodeGitObject("tag", tagPayload); |
| 110 | refs.push({ name: `refs/tags/${tagName}`, oid }); |
| 111 | const object = { type: "tag" as const, payload: tagPayload }; |
| 112 | if (index < tagCount / 2) { |
| 113 | firstPackObjects.push(object); |
| 114 | } else { |
| 115 | secondPackObjects.push(object); |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | await registerTestPack({ |
| 120 | env, |
| 121 | repoId, |
| 122 | getStub, |
| 123 | packName: `pack-tags-a-${Date.now()}.pack`, |
| 124 | objects: firstPackObjects, |
| 125 | }); |
| 126 | await registerTestPack({ |
| 127 | env, |
| 128 | repoId, |
| 129 | getStub, |
| 130 | packName: `pack-tags-b-${Date.now()}.pack`, |
| 131 | objects: secondPackObjects, |
| 132 | }); |
| 133 | |
| 134 | await runDOWithRetry(getStub, async (instance) => { |
| 135 | const current = await instance.getHeadAndRefs(); |
| 136 | await instance.setRefs([...current.refs, ...refs]); |
| 137 | }); |
| 138 | |
| 139 | const url = `https://example.com/${owner}/${repo}/git-upload-pack`; |
| 140 | const res = await workerExports.default.fetch(url, { |
| 141 | method: "POST", |
| 142 | headers: { |
| 143 | "Content-Type": "application/x-git-upload-pack-request", |
| 144 | "Git-Protocol": "version=2", |
| 145 | }, |
| 146 | body: toRequestBody(buildLsRefsBody(["ref-prefix refs/tags/", "peel"])), |
| 147 | }); |
| 148 | expect(res.status).toBe(200); |
| 149 | |
| 150 | const lines = decodePktLinePayloads(new Uint8Array(await res.arrayBuffer())); |
| 151 | const tagLines = lines.filter((line) => line.includes(" refs/tags/")); |
| 152 | expect(tagLines.length).toBe(tagCount); |
| 153 | for (let index = 0; index < tagCount; index++) { |
| 154 | const tagName = `refs/tags/v${index.toString().padStart(2, "0")}`; |
| 155 | const line = tagLines.find((candidate) => candidate.includes(` ${tagName}`)); |
| 156 | expect(line, `missing ${tagName}`).toBeDefined(); |
| 157 | expect(line?.includes(`peeled:${commitOid}`), `missing peeled attr for ${tagName}`).toBe(true); |
| 158 | } |
| 159 | }); |