Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
103 linesCodeBlameRaw
1import type { Comparison, DiffLine, FileDiff } from "@g1t/contracts";
2
3/** A diff line, with its text as highlighted HTML when the language is known. */
4export type HighlightedLine = DiffLine & { html?: string };
5export type HighlightedFile = Omit<FileDiff, "hunks"> & { hunks: { lines: HighlightedLine[] }[]; deferred?: Deferred };
6export type HighlightedComparison = Omit<Comparison, "files"> & { files: HighlightedFile[] };
7
8/**
9 * A file sent without its lines, which the page fetches when it gets near
10 * (components/diff-view.tsx): `lines` is how many it has, null when the
11 * comparison did not read them (`omitted`); a `large` one waits to be asked
12 * for.
13 */
14export type Deferred = { lines: number | null; large: boolean };
15
16/** How many diff lines a page is sent with: about its first screens. */
17export const FIRST_LINES = 2_000;
18/** Past this a file's lines wait for "Load diff": generated files, lockfiles. */
19export const LARGE_FILE_LINES = 1_000;
20
21/** How many lines a file's diff has. */
22export function linesOf(file: FileDiff): number {
23 return file.hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
24}
25
26/**
27 * A comparison as a page is sent it: the first files with their lines,
28 * until about `budget` lines, and every file after that, and any very large
29 * one, with its path and counts only (`deferred`), so a pull request of
30 * hundreds of files is not drawn, sent and hydrated all at once. Files
31 * named in `eager` (ones with comments on their lines) keep their lines
32 * wherever they are. The list itself is whole: the tree and counts show at once.
33 */
34export function firstScreens(
35 comparison: Comparison | HighlightedComparison,
36 { eager = [], budget = FIRST_LINES, large = LARGE_FILE_LINES }: { eager?: Iterable<string>; budget?: number; large?: number } = {},
37): HighlightedComparison {
38 const wanted = new Set(eager);
39 let spent = 0;
40 let full = false;
41 const files = comparison.files.map((file): HighlightedFile => {
42 if (file.omitted) return { ...file, hunks: [], deferred: { lines: null, large: false } };
43 if (file.binary || file.hunks.length === 0) return file;
44 const lines = linesOf(file);
45 if (wanted.has(file.path)) {
46 spent += lines;
47 return file;
48 }
49 if (lines > large) return { ...file, hunks: [], deferred: { lines, large: true } };
50 // In order, so what is drawn is the top of the change; once one does
51 // not fit, the rest wait too.
52 if (!full && spent + lines <= budget) {
53 spent += lines;
54 return file;
55 }
56 full = true;
57 return { ...file, hunks: [], deferred: { lines, large: false } };
58 });
59 return { ...comparison, files };
60}
61
62/** The file a page's address points to, as `#file-<path>` (or `#diff-<path>`), if it is one of `files`. */
63export function linkedFile(hash: string, files: { path: string }[]): string | null {
64 const match = /^#(?:file|diff)-(.+)$/.exec(hash);
65 if (!match) return null;
66 let path = match[1]!;
67 try {
68 path = decodeURIComponent(path);
69 } catch {
70 // Not encoded: as it is.
71 }
72 return files.some((file) => file.path === path) ? path : null;
73}
74
75/** Each hunk's lines' HTML, null for a line without any: what a highlighted file adds to its diff. */
76export type FileHtml = (string | null)[][];
77
78/** The HTML `highlighted` gives its lines, to keep apart from the diff it came from. */
79export function htmlOfFile(highlighted: HighlightedFile): FileHtml {
80 return highlighted.hunks.map((hunk) => hunk.lines.map((line) => line.html ?? null));
81}
82
83/** `file` with `html` given back to its lines: the same as the highlighted file it was taken from. */
84export function withHtml(file: FileDiff, html: FileHtml): HighlightedFile {
85 return {
86 ...file,
87 hunks: file.hunks.map((hunk, h) => ({
88 lines: hunk.lines.map((line, n): HighlightedLine => {
89 const row = html[h]?.[n];
90 return row == null ? { ...line } : { ...line, html: row };
91 }),
92 })),
93 };
94}
95
96/**
97 * What a file's highlighting depends on besides its language: each line's
98 * side and text, hunk by hunk. Line numbers and the path do not change the
99 * colours, so they are left out.
100 */
101export function diffContent(file: FileDiff): string {
102 return JSON.stringify(file.hunks.map((hunk) => hunk.lines.map((line) => [line.kind, line.text])));
103}