~james/core

core/web/src/lib/api/repoCompare.ts -rw-r--r-- 3.7 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 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
125
126
127
import type { Did } from "@atcute/lexicons/syntax";
import type { GitRepoInfo } from "#lib/components/repo/types.ts";
import { createBobbinClient } from "#lib/api/client.ts";
import {
	branches as listBranches,
	tags as listTags,
	gitTarget,
	type GitServiceConfig
} from "#lib/api/gitclient.ts";
import {
	commitAtRef,
	getMergeBase,
	listCommits,
	toMirrorCommitSummary
} from "#lib/api/knotmirror.ts";
import { sortBranchesByRecency } from "#lib/api/pullCompose.ts";
import {
	branchSummaries,
	toTagSummary,
	type BranchSummary,
	type CommitSummary,
	type TagSummary
} from "#lib/api/repo.ts";
import type { Fetch } from "#lib/fetch.ts";

const REF_LIMIT = 100;
const COMMIT_LIMIT = 100;

// the spec is `base...head` as on github. a git ref cannot contain `..`, thus the split is not ambiguous
export const parseCompareSpec = (spec: string): { base: string; head: string } | null => {
	const at = spec.indexOf("...");
	if (at < 0) return null;
	return { base: spec.slice(0, at), head: spec.slice(at + 3) };
};

export const comparePath = (repoPath: string, base: string, head: string): string =>
	base && head
		? `${repoPath}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`
		: `${repoPath}/compare?${new URLSearchParams({ ...(base && { base }), ...(head && { head }) })}`;

export interface CompareData {
	branches: BranchSummary[];
	tags: TagSummary[];
	base: string;
	head: string;
	baseHead: string;
	headHead: string;
	mergeBase: string;
	commits: CommitSummary[];
	error: string;
}

export const loadCompare = async (args: {
	config: GitServiceConfig;
	repo: GitRepoInfo;
	base: string;
	head: string;
	fetch: Fetch;
}): Promise<CompareData> => {
	const { config, repo, fetch: fetchFn } = args;
	const repoDid = repo.repoDid as Did;
	const ctx = createBobbinClient({ serviceUrl: config.bobbinUrl, fetch: fetchFn });
	const git = gitTarget(config, repo, fetchFn);

	const [branchList, tagList] = await Promise.all([
		listBranches(git, REF_LIMIT)
			.then(branchSummaries)
			.catch(() => [] as BranchSummary[]),
		listTags(git, REF_LIMIT)
			.then((page) => page.tags.map(toTagSummary))
			.catch(() => [] as TagSummary[])
	]);

	const base = args.base || repo.git.defaultBranch;
	const head =
		args.head ||
		sortBranchesByRecency(branchList).find((branch) => branch.name !== base)?.name ||
		"";

	// a listed ref resolves without a request. other refs, for example a commit hash, go to the mirror
	const resolveRef = async (ref: string): Promise<string> => {
		if (!ref) return "";
		const listed =
			branchList.find((branch) => branch.name === ref)?.hash ??
			tagList.find((tag) => tag.name === ref)?.commitHash;
		if (listed) return listed;
		return (await commitAtRef(ctx, repoDid, ref))?.oid ?? "";
	};

	let baseHead = "";
	let headHead = "";
	let mergeBase = "";
	let commits: CommitSummary[] = [];
	let error = "";
	try {
		[baseHead, headHead] = await Promise.all([resolveRef(base), resolveRef(head)]);
		if (base && !baseHead) error = `Could not find ${base} in this repository.`;
		else if (head && !headHead) error = `Could not find ${head} in this repository.`;
		else if (baseHead && headHead && baseHead !== headHead) {
			const [page, merge] = await Promise.all([
				listCommits(ctx, {
					repos: [repoDid],
					ranges: [`${baseHead}..${headHead}`],
					limit: COMMIT_LIMIT,
					includeStats: true
				}),
				getMergeBase(ctx, { repos: [repoDid], base: baseHead, head: headHead })
			]);
			commits = page.commits.map(toMirrorCommitSummary);
			mergeBase = merge.commit;
		}
	} catch (cause) {
		error = cause instanceof Error ? cause.message : String(cause);
	}

	return {
		branches: branchList,
		tags: tagList,
		base,
		head,
		baseHead,
		headHead,
		mergeBase,
		commits,
		error
	};
};