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 => { 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 => { 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 }; };