import type { Head, Ref, RepoStateSchema } from "@/worker/do/repo/repoState"; import type { TreeEntry } from "@/worker/git/operations/read/types"; import { concatChunks, encodeGitObject } from "@/worker/git/core"; import { hexToBytes } from "@/worker/common"; import { asTypedStorage, objKey } from "@/worker/do/repo/repoState"; import { doPrefix, r2LooseKey, r2PackKey } from "@/worker/keys"; import { getDb, listActivePackCatalog, listPackCatalog, upsertPackCatalogRow, } from "@/worker/do/repo/db"; import { buildPack } from "./git-pack"; import { indexTestPack } from "./test-indexer"; import { runDOWithRetry, type RepoDOStubFactory } from "./do-retry"; export type EncodedGitObject = Awaited>; export type SeedLegacyPackedRepoArgs = { env: Env; repoId: string; getStub: RepoDOStubFactory; packs: Array<{ name: string; packBytes: Uint8Array }>; refs?: Ref[]; head?: Head; looseObjects?: EncodedGitObject[]; mirrorLooseToR2?: boolean; }; export type SeedPackedRepoResult = { getStub: RepoDOStubFactory; packKeys: string[]; blob: EncodedGitObject; tree: EncodedGitObject; commit: EncodedGitObject; tag: EncodedGitObject; objectOids: string[]; }; export type RepoCatalogStateSnapshot = { packsetVersion: number; nextPackSeq: number; activeCatalog: Awaited>; catalog: Awaited>; }; export function buildTreePayload(entries: TreeEntry[]): Uint8Array { const encoder = new TextEncoder(); const parts: Uint8Array[] = []; for (const entry of entries) { parts.push( encoder.encode(`${entry.mode} ${entry.name}`), Uint8Array.from([0]), hexToBytes(entry.oid) ); } return concatChunks(parts); } export async function seedPackedRepoState( args: SeedLegacyPackedRepoArgs ): Promise<{ getStub: RepoDOStubFactory; packKeys: string[] }> { const packKeys: string[] = []; await runDOWithRetry(args.getStub, async (_instance, state) => { const prefix = doPrefix(state.id.toString()); const store = asTypedStorage(state.storage); const db = getDb(state.storage); const packSpecs = args.packs.map((pack) => ({ ...pack, packKey: r2PackKey(prefix, pack.name), })); for (const obj of args.looseObjects ?? []) { await store.put(objKey(obj.oid), obj.zdata); if (args.mirrorLooseToR2) { await args.env.REPO_BUCKET.put(r2LooseKey(prefix, obj.oid), obj.zdata); } } for (const pack of packSpecs) { packKeys.push(pack.packKey); } let nextSeq = (await store.get("nextPackSeq")) || 1; const catalogSoFar: import("@/worker/do/repo/db/schema").PackCatalogRow[] = []; // Historical test helpers passed packs newest-first because `packList` // mirrored that ordering. Preserve that caller contract for assertions and // returned `packKeys`, but index oldest-to-newest so REF_DELTA bases in // older packs are visible while newer packs are being indexed. for (const pack of [...packSpecs].reverse()) { await args.env.REPO_BUCKET.put(pack.packKey, pack.packBytes); // Pass already-indexed packs so REF_DELTA bases in earlier packs can be resolved const resolveResult = await indexTestPack( args.env, pack.packKey, pack.packBytes.byteLength, catalogSoFar.length > 0 ? catalogSoFar : undefined ); const seq = nextSeq++; const row: import("@/worker/do/repo/db/schema").PackCatalogRow = { packKey: pack.packKey, kind: "receive", state: "active", tier: 0, seqLo: seq, seqHi: seq, objectCount: resolveResult.objectCount, packBytes: pack.packBytes.byteLength, idxBytes: resolveResult.idxBytes, createdAt: Date.now(), supersededBy: null, }; await upsertPackCatalogRow(db, row); catalogSoFar.push(row); } if (packKeys.length === 0) throw new Error("seedPackedRepoState requires at least one pack"); // Bump packset version and persist next sequence number const packsetVersion = ((await store.get("packsetVersion")) || 0) + 1; await store.put("packsetVersion", packsetVersion); await store.put("nextPackSeq", nextSeq); if (args.refs) await store.put("refs", args.refs); if (args.head) await store.put("head", args.head); }); return { getStub: args.getStub, packKeys }; } /** @deprecated Alias for backward compatibility during migration */ export const seedLegacyPackedRepo = seedPackedRepoState; type SeedPackedRepoArgs = { env: Env; repoId: string; getStub: RepoDOStubFactory; options?: { mirrorLooseToR2?: boolean }; }; export async function seedPackedRepo(args: SeedPackedRepoArgs): Promise; export async function seedPackedRepo( env: Env, repoId: string, getStub: RepoDOStubFactory, options?: { mirrorLooseToR2?: boolean } ): Promise; export async function seedPackedRepo( envOrArgs: Env | SeedPackedRepoArgs, repoId?: string, getStub?: RepoDOStubFactory, options?: { mirrorLooseToR2?: boolean } ): Promise { const args = "env" in envOrArgs ? envOrArgs : { env: envOrArgs, repoId: repoId!, getStub: getStub!, options, }; const author = "You 0 +0000"; const blobPayload = new TextEncoder().encode("hello from packed storage\n"); const blob = await encodeGitObject("blob", blobPayload); const treePayload = buildTreePayload([{ mode: "100644", name: "hello.txt", oid: blob.oid }]); const tree = await encodeGitObject("tree", treePayload); const commitPayload = new TextEncoder().encode( `tree ${tree.oid}\n` + `author ${author}\n` + `committer ${author}\n\npacked commit\n` ); const commit = await encodeGitObject("commit", commitPayload); const tagPayload = new TextEncoder().encode( `object ${commit.oid}\n` + `type commit\n` + `tag v1\n` + `tagger ${author}\n\n` + `packed tag\n` ); const tag = await encodeGitObject("tag", tagPayload); const packBytes = await buildPack([ { type: "blob", payload: blobPayload }, { type: "tree", payload: treePayload }, { type: "commit", payload: commitPayload }, { type: "tag", payload: tagPayload }, ]); const seeded = await seedPackedRepoState({ env: args.env, repoId: args.repoId, getStub: args.getStub, packs: [{ name: "pack-initial.pack", packBytes }], refs: [ { name: "refs/heads/main", oid: commit.oid }, { name: "refs/tags/v1", oid: tag.oid }, ], head: { target: "refs/heads/main", oid: commit.oid }, looseObjects: [blob, tree, commit, tag], mirrorLooseToR2: args.options?.mirrorLooseToR2, }); return { ...seeded, blob, tree, commit, tag, objectOids: [blob.oid, tree.oid, commit.oid, tag.oid], }; } export async function readRepoCatalogState( getStub: RepoDOStubFactory ): Promise { return await runDOWithRetry(getStub, async (_instance, state: DurableObjectState) => { const store = asTypedStorage(state.storage); const db = getDb(state.storage); return { packsetVersion: (await store.get("packsetVersion")) || 0, nextPackSeq: (await store.get("nextPackSeq")) || 0, activeCatalog: await listActivePackCatalog(db), catalog: await listPackCatalog(db), }; }); } export async function deleteLooseObjectCopies(args: { env: Env; getStub: RepoDOStubFactory; objectOids: string[]; }): Promise; export async function deleteLooseObjectCopies( env: Env, getStub: RepoDOStubFactory, objectOids: string[] ): Promise; export async function deleteLooseObjectCopies( envOrArgs: | Env | { env: Env; getStub: RepoDOStubFactory; objectOids: string[]; }, getStub?: RepoDOStubFactory, objectOids?: string[] ): Promise { const args = "env" in envOrArgs && "getStub" in envOrArgs && "objectOids" in envOrArgs ? envOrArgs : { env: envOrArgs as Env, getStub: getStub!, objectOids: objectOids!, }; await runDOWithRetry(args.getStub, async (_instance, state) => { const prefix = doPrefix(state.id.toString()); for (const oid of args.objectOids) { await state.storage.delete(objKey(oid)); await args.env.REPO_BUCKET.delete(r2LooseKey(prefix, oid)); } }); } /** * Build a pack from raw payloads, upload to R2, index, and register in pack_catalog. * Use this to make loose-only test objects readable via the pack-first read path. */ export async function registerTestPack(args: { env: Env; repoId: string; getStub: RepoDOStubFactory; packName: string; objects: Array<{ type: "commit" | "tree" | "blob" | "tag"; payload: Uint8Array }>; }): Promise { const packBytes = await buildPack(args.objects); let packKey = ""; await runDOWithRetry(args.getStub, async (_instance, state) => { const prefix = doPrefix(state.id.toString()); const store = asTypedStorage(state.storage); const db = getDb(state.storage); packKey = r2PackKey(prefix, args.packName); await args.env.REPO_BUCKET.put(packKey, packBytes); const resolveResult = await indexTestPack(args.env, packKey, packBytes.byteLength); const seq = (await store.get("nextPackSeq")) || 1; await upsertPackCatalogRow(db, { packKey, kind: "receive", state: "active", tier: 0, seqLo: seq, seqHi: seq, objectCount: resolveResult.objectCount, packBytes: packBytes.byteLength, idxBytes: resolveResult.idxBytes, createdAt: Date.now(), supersededBy: null, }); const packsetVersion = ((await store.get("packsetVersion")) || 0) + 1; await store.put("packsetVersion", packsetVersion); await store.put("nextPackSeq", seq + 1); }); return packKey; }