Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-core/src/trees.ts
Snapshot at 34e3175
1import type { Repository } from "./types";
2
3export const ZERO_SHA = "0".repeat(40);
4
5/** Read object IDs only along requested paths; never walk unrelated subtrees or read blobs. */
6export async function pathHashes(
7 repo: Repository,
8 sha: string,
9 paths: string[],
10): Promise<Map<string, string | undefined>> {
11 const hashes = new Map<string, string | undefined>();
12 if (sha === ZERO_SHA) return new Map(paths.map(path => [path, undefined]));
13 const commit = await repo.readCommit(sha);
14 if (!commit) throw new Error("Commit unavailable");
15 const cache = new Map<string, ArtifactsTreeEntry[]>();
16 let visited = 0;
17 for (const path of paths) {
18 const parts = path.split("/");
19 if (parts.length > 64) throw new Error("Tree depth limit exceeded");
20 let tree = commit.treeHash;
21 let hash: string | undefined;
22 for (const [index, part] of parts.entries()) {
23 let entries = cache.get(tree);
24 if (!entries) {
25 entries = (await repo.readTree(tree)) ?? undefined;
26 if (!entries) throw new Error("Tree unavailable");
27 visited += entries.length;
28 if (visited > 10000) throw new Error("Tree entry limit exceeded");
29 cache.set(tree, entries);
30 }
31 const entry = entries.find(candidate => candidate.name === part);
32 if (!entry) break;
33 if (index === parts.length - 1) hash = entry.hash;
34 else if (entry.type === "tree") tree = entry.hash;
35 else break;
36 }
37 hashes.set(path, hash);
38 }
39 return hashes;
40}
41
42/** Compare complete snapshots, including modes, removals, symlinks and submodules. */
43export async function changedPaths(
44 repo: Repository,
45 before: string,
46 after: string,
47): Promise<string[]> {
48 const cache = new Map<string, ArtifactsTreeEntry[]>();
49 async function snapshot(commitHash: string): Promise<Map<string, string>> {
50 const files = new Map<string, string>();
51 if (commitHash === ZERO_SHA) return files;
52 const commit = await repo.readCommit(commitHash);
53 if (!commit) throw new Error("Commit unavailable");
54 let visited = 0;
55 async function visit(hash: string, prefix: string, depth: number): Promise<void> {
56 if (depth > 64) throw new Error("Tree depth limit exceeded");
57 let entries = cache.get(hash);
58 if (!entries) {
59 entries = (await repo.readTree(hash)) ?? undefined;
60 if (!entries) throw new Error("Tree unavailable");
61 cache.set(hash, entries);
62 }
63 for (const entry of entries) {
64 if (++visited > 10000) throw new Error("Tree entry limit exceeded");
65 const path = prefix + entry.name;
66 if (entry.type === "tree") await visit(entry.hash, path + "/", depth + 1);
67 else files.set(path, `${entry.mode}:${entry.hash}`);
68 }
69 }
70 await visit(commit.treeHash, "", 0);
71 return files;
72 }
73 const oldFiles = await snapshot(before);
74 const newFiles = await snapshot(after);
75 return [...new Set([...oldFiles.keys(), ...newFiles.keys()])]
76 .filter((path) => oldFiles.get(path) !== newFiles.get(path))
77 .toSorted();
78}
79