Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-core/src/trees.ts

Snapshot at 34e3175

Containing directory

3,040 bytes · typescriptView raw
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