Skip to content
File

Blob: src/worker/git/operations/read/tree.ts

typescript116 lines
1import type { CacheContext } from "@/worker/cache";
2import type { TreeEntry } from "./types";
3import { readLooseObjectRaw, parseTree } from "./objects";
4import { resolveRef } from "./refs";
5import { readCommit } from "./commits";
6import { parseTagTarget } from "@/worker/git/core";
7 
8export 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 
21export 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 
94export 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 
105export function isTreeMode(mode: string): boolean {
106 return mode.startsWith("40000");
107}
108 
109export function isSymlinkMode(mode: string): boolean {
110 return mode === "120000";
111}
112 
113export function joinTreePath(basePath: string, name: string): string {
114 return basePath ? `${basePath}/${name}` : name;
115}