import { browser } from "$app/environment"; import { MAX_INLINE_SIZE } from "./blob"; import { blob } from "./gitclient"; import { diffFileKind, diffFileStat, diffSideExists, getDiff, getInterdiff, type MirrorFileDiff, type MirrorFileKind } from "./gitmirror"; import { pierreDiffOptions, type DiffStyle } from "$lib/components/repo/pierre"; import type { FileContents } from "@pierre/diffs"; import type { BobbinContext } from "./client"; import type { GitTarget } from "./gitclient"; import type { DiffStat } from "./diff"; import type * as GetDiff from "./lexicons/types/sh/tangled/git/temp2/getDiff"; import type * as GetInterdiff from "./lexicons/types/sh/tangled/git/temp2/getInterdiff"; const FILE_LIMIT = 25; export interface PullDiffFile { // stable across sides, used for anchors and the prerender map key: string; name: string; oldName?: string; kind: MirrorFileKind; stat: { insertions: number; deletions: number }; // set when there is nothing to render: binary, submodule, or over the cap note?: string; oldFile?: FileContents; newFile?: FileContents; } export interface PullDiff { files: PullDiffFile[]; stat: DiffStat; // how many files were listed but not fetched truncated: number; // server-prerendered shadow dom per file key, only for the initial ssr prerendered?: Record; } // the diff endpoint answers metadata only — `diffSrc` is path/oid/size/flags and // a hunk is aligned line numbers — so the text comes from the blob endpoint and // pierre diffs the two sides itself const sideContents = async ( git: GitTarget, ref: string, src: MirrorFileDiff["lhsSrc"] ): Promise => { // a side that doesn't exist at this ref is an empty side, not a failed fetch if (!diffSideExists(src)) return ""; const output = await blob(git, { ref, path: src.path }, MAX_INLINE_SIZE).catch(() => null); if (!output || output.encoding !== "utf-8") return null; return output.content ?? ""; }; // both sides of one file, `null` for a side whose text could not be read type Sides = (file: MirrorFileDiff) => Promise<[string | null, string | null]>; const buildDiff = async ( diffs: MirrorFileDiff[], style: DiffStyle, sides: Sides ): Promise => { const stat: DiffStat = { insertions: 0, deletions: 0, files_changed: diffs.length }; const files = await Promise.all( diffs.map(async (file, index): Promise => { const fileStat = diffFileStat(file); stat.insertions += fileStat.insertions; stat.deletions += fileStat.deletions; const kind = diffFileKind(file); const name = file.rhsSrc.path || file.lhsSrc.path; const base: PullDiffFile = { key: `${file.lhsSrc.path}→${file.rhsSrc.path}`, name, oldName: kind === "renamed" ? file.lhsSrc.path : undefined, kind, stat: fileStat }; if (file.lhsSrc.isSubmodule || file.rhsSrc.isSubmodule) { return { ...base, note: "Submodule pointer, not shown." }; } if (file.lhsSrc.isBinary || file.rhsSrc.isBinary) { return { ...base, note: "This is a binary file and will not be displayed." }; } if (index >= FILE_LIMIT) { return { ...base, note: "Not rendered: too many files in this diff." }; } const [oldContents, newContents] = await sides(file); if (oldContents === null || newContents === null) { return { ...base, note: "Contents could not be loaded." }; } return { ...base, // pierre infers the language from the name, same as the blob page oldFile: { name: file.lhsSrc.path || name, contents: oldContents }, newFile: { name, contents: newContents } }; }) ); return { files, stat, truncated: Math.max(diffs.length - FILE_LIMIT, 0), prerendered: await prerender(files, style) }; }; export const loadPullDiff = async ( ctx: BobbinContext, git: GitTarget, params: GetDiff.$params, style: DiffStyle ): Promise => { const { diffs } = await getDiff(ctx, params); return buildDiff(diffs, style, (file) => Promise.all([ // the left side is read at baseCommit, while gitmirror diffs from // merge_base(base, head). identical when base is the merge base, which // is what a pull version stores sideContents(git, params.baseCommit, file.lhsSrc), sideContents(git, params.headCommit, file.rhsSrc) ]) ); }; // an interdiff's right side is version2's head tree, so it reads like any other // diff — but its left side is a tree gitmirror rebased in memory for this one // request, reachable by no ref, so that text can only arrive inlined export const loadPullInterdiff = async ( ctx: BobbinContext, git: GitTarget, params: GetInterdiff.$params, style: DiffStyle ): Promise => { const { diffs } = await getInterdiff(ctx, params); return buildDiff(diffs, style, async (file) => [ diffSideExists(file.lhsSrc) ? (file.lhsSrc.content ?? null) : "", await sideContents(git, params.headCommit2, file.rhsSrc) ]); }; // the first paint ships highlighted, like the commit page does const prerender = async ( files: PullDiffFile[], style: DiffStyle ): Promise | undefined> => { if (browser) return undefined; const { preloadMultiFileDiff } = await import("@pierre/diffs/ssr"); const options = pierreDiffOptions(style); const entries = await Promise.all( files .filter((file) => file.oldFile && file.newFile) .map(async (file) => { const { prerenderedHTML } = await preloadMultiFileDiff({ oldFile: file.oldFile as FileContents, newFile: file.newFile as FileContents, options }); return [file.key, prerenderedHTML] as const; }) ); return Object.fromEntries(entries); };