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