Skip to content
File

Blob: test/util/packed-repo.ts

typescript317 lines
1import type { Head, Ref, RepoStateSchema } from "@/worker/do/repo/repoState";
2import type { TreeEntry } from "@/worker/git/operations/read/types";
3 
4import { concatChunks, encodeGitObject } from "@/worker/git/core";
5import { hexToBytes } from "@/worker/common";
6import { asTypedStorage, objKey } from "@/worker/do/repo/repoState";
7import { doPrefix, r2LooseKey, r2PackKey } from "@/worker/keys";
8import {
9 getDb,
10 listActivePackCatalog,
11 listPackCatalog,
12 upsertPackCatalogRow,
13} from "@/worker/do/repo/db";
14import { buildPack } from "./git-pack";
15import { indexTestPack } from "./test-indexer";
16import { runDOWithRetry, type RepoDOStubFactory } from "./do-retry";
17 
18export type EncodedGitObject = Awaited<ReturnType<typeof encodeGitObject>>;
19 
20export type SeedLegacyPackedRepoArgs = {
21 env: Env;
22 repoId: string;
23 getStub: RepoDOStubFactory;
24 packs: Array<{ name: string; packBytes: Uint8Array }>;
25 refs?: Ref[];
26 head?: Head;
27 looseObjects?: EncodedGitObject[];
28 mirrorLooseToR2?: boolean;
29};
30 
31export type SeedPackedRepoResult = {
32 getStub: RepoDOStubFactory;
33 packKeys: string[];
34 blob: EncodedGitObject;
35 tree: EncodedGitObject;
36 commit: EncodedGitObject;
37 tag: EncodedGitObject;
38 objectOids: string[];
39};
40 
41export type RepoCatalogStateSnapshot = {
42 packsetVersion: number;
43 nextPackSeq: number;
44 activeCatalog: Awaited<ReturnType<typeof listActivePackCatalog>>;
45 catalog: Awaited<ReturnType<typeof listPackCatalog>>;
46};
47 
48export function buildTreePayload(entries: TreeEntry[]): Uint8Array {
49 const encoder = new TextEncoder();
50 const parts: Uint8Array[] = [];
51 for (const entry of entries) {
52 parts.push(
53 encoder.encode(`${entry.mode} ${entry.name}`),
54 Uint8Array.from([0]),
55 hexToBytes(entry.oid)
56 );
57 }
58 return concatChunks(parts);
59}
60 
61export async function seedPackedRepoState(
62 args: SeedLegacyPackedRepoArgs
63): Promise<{ getStub: RepoDOStubFactory; packKeys: string[] }> {
64 const packKeys: string[] = [];
65 
66 await runDOWithRetry(args.getStub, async (_instance, state) => {
67 const prefix = doPrefix(state.id.toString());
68 const store = asTypedStorage<RepoStateSchema>(state.storage);
69 const db = getDb(state.storage);
70 const packSpecs = args.packs.map((pack) => ({
71 ...pack,
72 packKey: r2PackKey(prefix, pack.name),
73 }));
74 
75 for (const obj of args.looseObjects ?? []) {
76 await store.put(objKey(obj.oid), obj.zdata);
77 if (args.mirrorLooseToR2) {
78 await args.env.REPO_BUCKET.put(r2LooseKey(prefix, obj.oid), obj.zdata);
79 }
80 }
81 
82 for (const pack of packSpecs) {
83 packKeys.push(pack.packKey);
84 }
85 
86 let nextSeq = (await store.get("nextPackSeq")) || 1;
87 const catalogSoFar: import("@/worker/do/repo/db/schema").PackCatalogRow[] = [];
88 // Historical test helpers passed packs newest-first because `packList`
89 // mirrored that ordering. Preserve that caller contract for assertions and
90 // returned `packKeys`, but index oldest-to-newest so REF_DELTA bases in
91 // older packs are visible while newer packs are being indexed.
92 for (const pack of [...packSpecs].reverse()) {
93 await args.env.REPO_BUCKET.put(pack.packKey, pack.packBytes);
94 
95 // Pass already-indexed packs so REF_DELTA bases in earlier packs can be resolved
96 const resolveResult = await indexTestPack(
97 args.env,
98 pack.packKey,
99 pack.packBytes.byteLength,
100 catalogSoFar.length > 0 ? catalogSoFar : undefined
101 );
102 
103 const seq = nextSeq++;
104 const row: import("@/worker/do/repo/db/schema").PackCatalogRow = {
105 packKey: pack.packKey,
106 kind: "receive",
107 state: "active",
108 tier: 0,
109 seqLo: seq,
110 seqHi: seq,
111 objectCount: resolveResult.objectCount,
112 packBytes: pack.packBytes.byteLength,
113 idxBytes: resolveResult.idxBytes,
114 createdAt: Date.now(),
115 supersededBy: null,
116 };
117 await upsertPackCatalogRow(db, row);
118 catalogSoFar.push(row);
119 }
120 
121 if (packKeys.length === 0) throw new Error("seedPackedRepoState requires at least one pack");
122 
123 // Bump packset version and persist next sequence number
124 const packsetVersion = ((await store.get("packsetVersion")) || 0) + 1;
125 await store.put("packsetVersion", packsetVersion);
126 await store.put("nextPackSeq", nextSeq);
127 
128 if (args.refs) await store.put("refs", args.refs);
129 if (args.head) await store.put("head", args.head);
130 });
131 
132 return { getStub: args.getStub, packKeys };
133}
134 
135/** @deprecated Alias for backward compatibility during migration */
136export const seedLegacyPackedRepo = seedPackedRepoState;
137 
138type SeedPackedRepoArgs = {
139 env: Env;
140 repoId: string;
141 getStub: RepoDOStubFactory;
142 options?: { mirrorLooseToR2?: boolean };
143};
144 
145export async function seedPackedRepo(args: SeedPackedRepoArgs): Promise<SeedPackedRepoResult>;
146export async function seedPackedRepo(
147 env: Env,
148 repoId: string,
149 getStub: RepoDOStubFactory,
150 options?: { mirrorLooseToR2?: boolean }
151): Promise<SeedPackedRepoResult>;
152export async function seedPackedRepo(
153 envOrArgs: Env | SeedPackedRepoArgs,
154 repoId?: string,
155 getStub?: RepoDOStubFactory,
156 options?: { mirrorLooseToR2?: boolean }
157): Promise<SeedPackedRepoResult> {
158 const args =
159 "env" in envOrArgs
160 ? envOrArgs
161 : {
162 env: envOrArgs,
163 repoId: repoId!,
164 getStub: getStub!,
165 options,
166 };
167 
168 const author = "You <you@example.com> 0 +0000";
169 const blobPayload = new TextEncoder().encode("hello from packed storage\n");
170 const blob = await encodeGitObject("blob", blobPayload);
171 const treePayload = buildTreePayload([{ mode: "100644", name: "hello.txt", oid: blob.oid }]);
172 const tree = await encodeGitObject("tree", treePayload);
173 const commitPayload = new TextEncoder().encode(
174 `tree ${tree.oid}\n` + `author ${author}\n` + `committer ${author}\n\npacked commit\n`
175 );
176 const commit = await encodeGitObject("commit", commitPayload);
177 const tagPayload = new TextEncoder().encode(
178 `object ${commit.oid}\n` +
179 `type commit\n` +
180 `tag v1\n` +
181 `tagger ${author}\n\n` +
182 `packed tag\n`
183 );
184 const tag = await encodeGitObject("tag", tagPayload);
185 
186 const packBytes = await buildPack([
187 { type: "blob", payload: blobPayload },
188 { type: "tree", payload: treePayload },
189 { type: "commit", payload: commitPayload },
190 { type: "tag", payload: tagPayload },
191 ]);
192 
193 const seeded = await seedPackedRepoState({
194 env: args.env,
195 repoId: args.repoId,
196 getStub: args.getStub,
197 packs: [{ name: "pack-initial.pack", packBytes }],
198 refs: [
199 { name: "refs/heads/main", oid: commit.oid },
200 { name: "refs/tags/v1", oid: tag.oid },
201 ],
202 head: { target: "refs/heads/main", oid: commit.oid },
203 looseObjects: [blob, tree, commit, tag],
204 mirrorLooseToR2: args.options?.mirrorLooseToR2,
205 });
206 
207 return {
208 ...seeded,
209 blob,
210 tree,
211 commit,
212 tag,
213 objectOids: [blob.oid, tree.oid, commit.oid, tag.oid],
214 };
215}
216 
217export async function readRepoCatalogState(
218 getStub: RepoDOStubFactory
219): Promise<RepoCatalogStateSnapshot> {
220 return await runDOWithRetry(getStub, async (_instance, state: DurableObjectState) => {
221 const store = asTypedStorage<RepoStateSchema>(state.storage);
222 const db = getDb(state.storage);
223 return {
224 packsetVersion: (await store.get("packsetVersion")) || 0,
225 nextPackSeq: (await store.get("nextPackSeq")) || 0,
226 activeCatalog: await listActivePackCatalog(db),
227 catalog: await listPackCatalog(db),
228 };
229 });
230}
231 
232export async function deleteLooseObjectCopies(args: {
233 env: Env;
234 getStub: RepoDOStubFactory;
235 objectOids: string[];
236}): Promise<void>;
237export async function deleteLooseObjectCopies(
238 env: Env,
239 getStub: RepoDOStubFactory,
240 objectOids: string[]
241): Promise<void>;
242export async function deleteLooseObjectCopies(
243 envOrArgs:
244 | Env
245 | {
246 env: Env;
247 getStub: RepoDOStubFactory;
248 objectOids: string[];
249 },
250 getStub?: RepoDOStubFactory,
251 objectOids?: string[]
252): Promise<void> {
253 const args =
254 "env" in envOrArgs && "getStub" in envOrArgs && "objectOids" in envOrArgs
255 ? envOrArgs
256 : {
257 env: envOrArgs as Env,
258 getStub: getStub!,
259 objectOids: objectOids!,
260 };
261 
262 await runDOWithRetry(args.getStub, async (_instance, state) => {
263 const prefix = doPrefix(state.id.toString());
264 for (const oid of args.objectOids) {
265 await state.storage.delete(objKey(oid));
266 await args.env.REPO_BUCKET.delete(r2LooseKey(prefix, oid));
267 }
268 });
269}
270 
271/**
272 * Build a pack from raw payloads, upload to R2, index, and register in pack_catalog.
273 * Use this to make loose-only test objects readable via the pack-first read path.
274 */
275export async function registerTestPack(args: {
276 env: Env;
277 repoId: string;
278 getStub: RepoDOStubFactory;
279 packName: string;
280 objects: Array<{ type: "commit" | "tree" | "blob" | "tag"; payload: Uint8Array }>;
281}): Promise<string> {
282 const packBytes = await buildPack(args.objects);
283 
284 let packKey = "";
285 await runDOWithRetry(args.getStub, async (_instance, state) => {
286 const prefix = doPrefix(state.id.toString());
287 const store = asTypedStorage<RepoStateSchema>(state.storage);
288 const db = getDb(state.storage);
289 
290 packKey = r2PackKey(prefix, args.packName);
291 await args.env.REPO_BUCKET.put(packKey, packBytes);
292 
293 const resolveResult = await indexTestPack(args.env, packKey, packBytes.byteLength);
294 
295 const seq = (await store.get("nextPackSeq")) || 1;
296 await upsertPackCatalogRow(db, {
297 packKey,
298 kind: "receive",
299 state: "active",
300 tier: 0,
301 seqLo: seq,
302 seqHi: seq,
303 objectCount: resolveResult.objectCount,
304 packBytes: packBytes.byteLength,
305 idxBytes: resolveResult.idxBytes,
306 createdAt: Date.now(),
307 supersededBy: null,
308 });
309 
310 const packsetVersion = ((await store.get("packsetVersion")) || 0) + 1;
311 await store.put("packsetVersion", packsetVersion);
312 await store.put("nextPackSeq", seq + 1);
313 });
314 
315 return packKey;
316}