File
Blob: src/worker/git/operations/read/tree.ts
| 1 | import type { CacheContext } from "@/worker/cache"; |
| 2 | import type { TreeEntry } from "./types"; |
| 3 | import { readLooseObjectRaw, parseTree } from "./objects"; |
| 4 | import { resolveRef } from "./refs"; |
| 5 | import { readCommit } from "./commits"; |
| 6 | import { parseTagTarget } from "@/worker/git/core"; |
| 7 | |
| 8 | export async function readTree( |
| 9 | env: Env, |
| 10 | repoId: string, |
| 11 | oid: string, |
| 12 | cacheCtx?: CacheContext |
| 13 | ): Promise<TreeEntry[]> { |
| 14 | const obj = await readLooseObjectRaw(env, repoId, oid, cacheCtx); |
| 15 | if (!obj || obj.type !== "tree") { |
| 16 | throw new Error("Not a tree"); |
| 17 | } |
| 18 | return parseTree(obj.payload); |
| 19 | } |
| 20 | |
| 21 | export async function readPath( |
| 22 | env: Env, |
| 23 | repoId: string, |
| 24 | ref: string, |
| 25 | path?: string, |
| 26 | cacheCtx?: CacheContext |
| 27 | ): Promise<ReadPathResult> { |
| 28 | let startOid: string | undefined = await resolveRef(env, repoId, ref); |
| 29 | if (!startOid && /^[0-9a-f]{40}$/i.test(ref)) startOid = ref.toLowerCase(); |
| 30 | if (!startOid) throw new Error("Ref not found"); |
| 31 | |
| 32 | const startObj = await readLooseObjectRaw(env, repoId, startOid, cacheCtx); |
| 33 | if (!startObj) throw new Error("Object not found"); |
| 34 | |
| 35 | let currentTreeOid: string | undefined; |
| 36 | if (startObj.type === "commit") { |
| 37 | const { tree } = await readCommit(env, repoId, startOid, cacheCtx); |
| 38 | currentTreeOid = tree; |
| 39 | } else if (startObj.type === "tree") { |
| 40 | currentTreeOid = startOid; |
| 41 | } else if (startObj.type === "tag") { |
| 42 | const t = parseTagTarget(startObj.payload); |
| 43 | if (!t || !t.targetOid) throw new Error("Unsupported object type"); |
| 44 | const target = t.targetOid; |
| 45 | const { tree } = await readCommit(env, repoId, target, cacheCtx); |
| 46 | currentTreeOid = tree; |
| 47 | } else if (startObj.type === "blob") { |
| 48 | if (path && path !== "") throw new Error("Path not a directory"); |
| 49 | return { type: "blob", oid: startOid, content: startObj.payload, base: "" }; |
| 50 | } else { |
| 51 | throw new Error("Unsupported object type"); |
| 52 | } |
| 53 | |
| 54 | const parts = (path || "").split("/").filter(Boolean); |
| 55 | let base = ""; |
| 56 | for (let i = 0; i < parts.length; i++) { |
| 57 | const entries = await readTree(env, repoId, currentTreeOid, cacheCtx); |
| 58 | const ent = entries.find((e) => e.name === parts[i]); |
| 59 | if (!ent) throw new Error("Path not found"); |
| 60 | base = parts.slice(0, i + 1).join("/"); |
| 61 | if (ent.mode.startsWith("40000")) { |
| 62 | currentTreeOid = ent.oid; |
| 63 | if (i === parts.length - 1) { |
| 64 | const finalEntries = await readTree(env, repoId, currentTreeOid, cacheCtx); |
| 65 | return { type: "tree", entries: finalEntries, base }; |
| 66 | } |
| 67 | } else { |
| 68 | if (i !== parts.length - 1) throw new Error("Path not a directory"); |
| 69 | const MAX_SIZE = 5 * 1024 * 1024; |
| 70 | const blob = await readLooseObjectRaw(env, repoId, ent.oid, cacheCtx); |
| 71 | if (!blob || blob.type !== "blob") throw new Error("Not a blob"); |
| 72 | |
| 73 | // Pack reads are authoritative here, so the UI size gate must be based |
| 74 | // on the resolved blob payload instead of legacy loose-object metadata. |
| 75 | const actualSize = blob.payload.byteLength; |
| 76 | if (actualSize > MAX_SIZE) { |
| 77 | return { |
| 78 | type: "blob", |
| 79 | oid: ent.oid, |
| 80 | content: new Uint8Array(0), |
| 81 | base, |
| 82 | size: actualSize, |
| 83 | tooLarge: true, |
| 84 | }; |
| 85 | } |
| 86 | |
| 87 | return { type: "blob", oid: ent.oid, content: blob.payload, base }; |
| 88 | } |
| 89 | } |
| 90 | const rootEntries = await readTree(env, repoId, currentTreeOid, cacheCtx); |
| 91 | return { type: "tree", entries: rootEntries, base }; |
| 92 | } |
| 93 | |
| 94 | export type ReadPathResult = |
| 95 | | { type: "tree"; entries: TreeEntry[]; base: string } |
| 96 | | { |
| 97 | type: "blob"; |
| 98 | oid: string; |
| 99 | content: Uint8Array; |
| 100 | base: string; |
| 101 | size?: number; |
| 102 | tooLarge?: boolean; |
| 103 | }; |
| 104 | |
| 105 | export function isTreeMode(mode: string): boolean { |
| 106 | return mode.startsWith("40000"); |
| 107 | } |
| 108 | |
| 109 | export function isSymlinkMode(mode: string): boolean { |
| 110 | return mode === "120000"; |
| 111 | } |
| 112 | |
| 113 | export function joinTreePath(basePath: string, name: string): string { |
| 114 | return basePath ? `${basePath}/${name}` : name; |
| 115 | } |