~james/core

core/web/src/lib/api/blame.ts -rw-r--r-- 3.8 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import { isDid, type Did } from "@atcute/lexicons/syntax";
import type { BobbinContext, XrpcRequestInit } from "#lib/api/client.ts";
import { resolveEmailToDid } from "#lib/api/emailDid.ts";
import { getBlame } from "#lib/api/knotmirror.ts";
import { resolveMiniDoc } from "#lib/api/identity.ts";
import { didFromSignature, splitMessage } from "#lib/api/repo.ts";
import type * as GetBlame from "#lib/api/lexicons/types/sh/tangled/git/temp2/getBlame.ts";
import type { Blame, BlameCommit } from "#lib/components/repo/blame/types.ts";

type RunTable = { start: number; count: number; commit: string };

// knot and mirror payloads share the run-table contract: commits must be unique
// and runs must be a gap-free, sorted cover of the file starting at line 1
const toBlame = (commits: readonly BlameCommit[], runs: readonly RunTable[]): Blame => {
	const byShort = new Map<string, BlameCommit>();
	for (const commit of commits) {
		if (byShort.has(commit.sha)) throw new Error(`duplicate blame commit: ${commit.sha}`);
		byShort.set(commit.sha, commit);
	}
	const blame: Blame = [];
	let nextLine = 1;
	for (const run of runs) {
		if (run.start !== nextLine || run.count < 1 || !Number.isSafeInteger(run.count)) {
			throw new Error(`invalid blame run at line ${run.start}`);
		}
		const commit = byShort.get(run.commit);
		if (!commit) throw new Error(`unknown blame commit: ${run.commit}`);
		blame.push({ start: run.start, count: run.count, commit });
		nextLine += run.count;
	}
	return blame;
};

// a did in the author slot is a tangled account signing its own commits
const toAuthor = ({ name, email }: { name: string; email: string }) => ({
	name,
	email,
	did: didFromSignature(email)
});

const fromMirror = (result: GetBlame.$output): Blame =>
	toBlame(
		result.commits.map((commit) => ({
			sha: commit.oid,
			subject: splitMessage(commit.message)[0],
			when: commit.author.when,
			author: toAuthor(commit.author)
		})),
		result.runs
	);

export const blameFor = async (
	ctx: BobbinContext,
	repo: Did,
	ref: string,
	path: string,
	init?: XrpcRequestInit
): Promise<Blame> => fromMirror(await getBlame(ctx, { repo, ref, path }, init));

const MAX_AUTHOR_HYDRATIONS = 250;
const AUTHOR_HYDRATION_CONCURRENCY = 8;

const mapConcurrent = async <T, U>(
	items: T[],
	concurrency: number,
	fn: (item: T) => Promise<U>
): Promise<U[]> => {
	const results = new Array<U>(items.length);
	let next = 0;
	await Promise.all(
		Array.from({ length: Math.min(concurrency, items.length) }, async () => {
			while (next < items.length) {
				const index = next++;
				results[index] = await fn(items[index]);
			}
		})
	);
	return results;
};

export const hydrateBlameAuthors = async (blame: Blame, ctx: BobbinContext): Promise<Blame> => {
	const emails = [
		...new Set(
			blame.flatMap(({ commit }) =>
				commit.author.did || !commit.author.email ? [] : [commit.author.email]
			)
		)
	].slice(0, MAX_AUTHOR_HYDRATIONS);
	const emailDids = await resolveEmailToDid(ctx.fetch, emails);
	const didFor = (email: string): string | undefined =>
		isDid(email) ? email : emailDids.get(email);
	const dids = [
		...new Set(
			blame.flatMap(({ commit }) => {
				const did = commit.author.did ?? didFor(commit.author.email);
				return did ? [did] : [];
			})
		)
	].slice(0, MAX_AUTHOR_HYDRATIONS);
	const docs = await mapConcurrent(
		dids,
		AUTHOR_HYDRATION_CONCURRENCY,
		async (did) => [did, await resolveMiniDoc(ctx, did).catch(() => null)] as const
	);
	const handles = new Map(
		docs.flatMap(([did, doc]) =>
			doc && !doc.handle.endsWith(".invalid") ? [[did, doc.handle] as const] : []
		)
	);
	return blame.map((run) => {
		const did = run.commit.author.did ?? didFor(run.commit.author.email);
		return {
			...run,
			commit: {
				...run.commit,
				author: {
					...run.commit.author,
					did,
					handle: did ? handles.get(did) : undefined
				}
			}
		};
	});
};