import { jsonGet } from "$lib/api/_request"; import { coAuthorsFrom, didFromSignature, FULL_HASH_RE, splitMessage, type CommitDetail, type CommitSummary } from "$lib/api/repo"; import type { Did } from "@atcute/lexicons/syntax"; import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; import type { Revspec } from "$lib/api/pullRoute"; import type * as ShTangledGitDefs from "$lib/api/lexicons/types/sh/tangled/git/defs"; import type * as GetBlame from "$lib/api/lexicons/types/sh/tangled/git/temp2/getBlame"; import type * as GetDiff from "$lib/api/lexicons/types/sh/tangled/git/temp2/getDiff"; import type * as GetInterdiff from "$lib/api/lexicons/types/sh/tangled/git/temp2/getInterdiff"; import type * as ListCommits from "$lib/api/lexicons/types/sh/tangled/git/temp2/listCommits"; import type * as MergeCheck from "$lib/api/lexicons/types/sh/tangled/git/temp2/mergeCheck"; // gitmirror's own xrpc surface, proxied through bobbin — its grpc service is // internal and streaming, so the read paths the frontend needs live here const LIST_COMMITS_NSID = "sh.tangled.git.temp2.listCommits"; const GET_DIFF_NSID = "sh.tangled.git.temp2.getDiff"; const GET_INTERDIFF_NSID = "sh.tangled.git.temp2.getInterdiff"; const GET_BLAME_NSID = "sh.tangled.git.temp2.getBlame"; const MERGE_CHECK_NSID = "sh.tangled.git.temp2.mergeCheck"; export const getBlame = (ctx: BobbinContext, params: GetBlame.$params, init?: XrpcRequestInit) => jsonGet(ctx, GET_BLAME_NSID, params, init); export type MirrorCommit = ListCommits.Commit; export const listCommits = ( ctx: BobbinContext, params: ListCommits.$params, init?: XrpcRequestInit ) => jsonGet(ctx, LIST_COMMITS_NSID, params, init); export const commitAtRef = async ( ctx: BobbinContext, repo: Did, ref: string, init?: XrpcRequestInit ): Promise => { const page = await listCommits(ctx, { repo, ranges: [ref], limit: 1 }, init); const first = page.commits[0]; if (!first || !FULL_HASH_RE.test(ref) || first.oid.toLowerCase() === ref.toLowerCase()) { return first; } const exact = await listCommits(ctx, { repo, ranges: [`${ref}^..${ref}`], limit: 100 }, init); return exact.commits.find((commit) => commit.oid.toLowerCase() === ref.toLowerCase()); }; const LOG_LIMIT = 100; // every commit a pull version brought, newest first export const listLog = async ( ctx: BobbinContext, repo: Did, range: Revspec, includeStats = false, init?: XrpcRequestInit ): Promise => { const page = await listCommits( ctx, { repo, ranges: [`${range.base}..${range.head}`], limit: LOG_LIMIT, includeStats }, init ); return page.commits.map(toMirrorCommitSummary); }; export type MirrorFileDiff = ShTangledGitDefs.FileDiff; export const getDiff = (ctx: BobbinContext, params: GetDiff.$params, init?: XrpcRequestInit) => jsonGet(ctx, GET_DIFF_NSID, params, init); // `base*` is the older version's span, `head*` the newer one's: gitmirror // rebases base1..base2 onto head1 and diffs the result against head2 export const getInterdiff = ( ctx: BobbinContext, params: GetInterdiff.$params, init?: XrpcRequestInit ) => jsonGet(ctx, GET_INTERDIFF_NSID, params, init); export type MirrorConflict = MergeCheck.Conflict; export const mergeCheck = ( ctx: BobbinContext, params: MergeCheck.$params, init?: XrpcRequestInit ) => jsonGet(ctx, MERGE_CHECK_NSID, params, init); export type MirrorFileKind = "new" | "deleted" | "renamed" | "changed"; // an add or delete carries the same path on both sides — gix names both resources // after the change's location — so the side that doesn't exist is the one whose oid // is null, not the one whose path is empty export const diffSideExists = (src: ShTangledGitDefs.DiffSrc): boolean => src.path !== "" && !/^0+$/.test(src.oid); const EMPTY_TREE_SHA1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; const EMPTY_TREE_SHA256 = "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321"; export const emptyTreeOid = (like: string): string => like.length === 64 ? EMPTY_TREE_SHA256 : EMPTY_TREE_SHA1; export const diffFileName = (file: MirrorFileDiff): string => file.rhsSrc.path || file.lhsSrc.path; export const diffFileKind = (file: MirrorFileDiff): MirrorFileKind => { if (!diffSideExists(file.lhsSrc)) return "new"; if (!diffSideExists(file.rhsSrc)) return "deleted"; return file.lhsSrc.path === file.rhsSrc.path ? "changed" : "renamed"; }; // the payload has no line text, only aligned line numbers: a pair with one side // missing is a pure insertion or deletion, a pair with both is a replaced line export const diffFileStat = (file: MirrorFileDiff): { insertions: number; deletions: number } => { let insertions = 0; let deletions = 0; for (const hunk of file.hunks) { for (const line of hunk.lines) { if (line.rhs !== undefined) insertions++; if (line.lhs !== undefined) deletions++; } } return { insertions, deletions }; }; export const mirrorChangeId = (commit: MirrorCommit): string | undefined => commit.extraHeaders.find((header) => header.key === "change-id")?.value; export const toMirrorCommitSummary = (commit: MirrorCommit): CommitSummary => { const [subject, body] = splitMessage(commit.message); return { hash: commit.oid, shortHash: commit.oid.slice(0, 8), subject, body, authorName: commit.author.name, authorEmail: commit.author.email, when: commit.committer.when ?? commit.author.when, // the pull page links a commit row as `firstParent..commit` parent: commit.parents[0], changeId: mirrorChangeId(commit), stat: commit.stat && { insertions: commit.stat.insertions, deletions: commit.stat.deletions, files_changed: commit.stat.filesChanged } }; }; export const toMirrorCommitDetail = (commit: MirrorCommit): CommitDetail => { const [subject, body] = splitMessage(commit.message); const committerWhen = commit.committer.when ?? ""; return { hash: commit.oid, shortHash: commit.oid.slice(0, 8), subject, body, authorName: commit.author.name, authorEmail: commit.author.email, authorDid: didFromSignature(commit.author.email), authorWhen: commit.author.when ?? "", committerName: commit.committer.name, committerEmail: commit.committer.email, committerWhen, coAuthors: coAuthorsFrom(commit.message, committerWhen).map(({ Name, Email }) => ({ name: Name, email: Email })) }; };