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.

198 linesCodeBlameRaw
1/**
2 * Code a page cites, and whether a change touched it. Pure.
3 *
4 * A citation names a repository (`owner/name`) and a path in it: a file
5 * (`src/export.ts`), a folder (`src/export`, everything under it) or a
6 * glob (`src/**\/*.sql`, `*` within a folder, `**` across folders, `?` one
7 * character). Its kind says what at that path the page describes: the
8 * path itself, a symbol, an endpoint or an environment variable, named by
9 * its label. A page's citations come from its text (the editor's citation
10 * chips, and links to files in a repository) and from its header.
11 */
12import type { DocCitation, DocCitationKind, DocDescribes } from "@g1t/contracts";
13
14import { projectRef } from "./search.ts";
15
16const KINDS = new Set<DocCitationKind>(["path", "symbol", "endpoint", "env"]);
17const MAX_PATH = 400;
18const MAX_LABEL = 200;
19/** Citations a page keeps from its text, and in its header. */
20export const MAX_CITATIONS = 100;
21export const MAX_DESCRIBES = 20;
22
23/** A path as kept: no leading or trailing slash, no `.`/`..` parts, no doubled slashes; null when empty or unsafe. */
24export function cleanPath(path: unknown): string | null {
25 const parts = String(path ?? "")
26 .trim()
27 .replace(/\\/g, "/")
28 .split("/")
29 .filter((p) => p && p !== ".");
30 if (!parts.length || parts.some((p) => p === ".." || /[\u0000-\u001f]/.test(p))) return null;
31 const out = parts.join("/");
32 return out.length <= MAX_PATH ? out : null;
33}
34
35function isGlob(path: string): boolean {
36 return /[*?]/.test(path);
37}
38
39/** A citation as stored, or null when it names no repository or path. */
40export function cleanCitation(raw: Partial<DocCitation> | null | undefined, source: DocCitation["source"]): DocCitation | null {
41 if (!raw || typeof raw !== "object") return null;
42 const repo = projectRef(String(raw.repo ?? ""));
43 const path = cleanPath(raw.path);
44 if (!repo || !path) return null;
45 const kind = KINDS.has(raw.kind as DocCitationKind) ? (raw.kind as DocCitationKind) : "path";
46 const label =
47 kind === "path"
48 ? null
49 : String(raw.label ?? "")
50 .replace(/\s+/g, " ")
51 .trim()
52 .slice(0, MAX_LABEL) || null;
53 const ref = /^[0-9a-f]{7,64}$/i.test(String(raw.ref ?? ""))
54 ? String(raw.ref).toLowerCase()
55 : /^[A-Za-z0-9._/-]{1,100}$/.test(String(raw.ref ?? ""))
56 ? String(raw.ref)
57 : null;
58 return { repo, path, kind, label, ref, source };
59}
60
61/** The header's "Describes" list as stored: valid entries, once each, at most MAX_DESCRIBES. */
62export function cleanDescribes(list: unknown): DocDescribes[] {
63 if (!Array.isArray(list)) return [];
64 const seen = new Set<string>();
65 const out: DocDescribes[] = [];
66 for (const item of list) {
67 const c = cleanCitation({ ...(item as object), kind: "path" }, "header");
68 if (!c || seen.has(`${c.repo}\n${c.path}`)) continue;
69 seen.add(`${c.repo}\n${c.path}`);
70 out.push({ repo: c.repo, path: c.path });
71 if (out.length >= MAX_DESCRIBES) break;
72 }
73 return out;
74}
75
76/** The folder a glob starts from: its parts before the first with a wildcard. */
77export function globRoot(path: string): string {
78 const parts = path.split("/");
79 const i = parts.findIndex((p) => isGlob(p));
80 return (i < 0 ? parts : parts.slice(0, i)).join("/");
81}
82
83/**
84 * Where a citation links: the file (or folder) in Code at the commit it
85 * was cited at, or the default branch (`HEAD`) when none is known. A
86 * glob links to the folder it starts from.
87 */
88export function citationHref(c: Pick<DocCitation, "repo" | "path" | "ref">): string {
89 const ref = encodeURIComponent(c.ref || "HEAD");
90 const glob = isGlob(c.path);
91 const path = (glob ? globRoot(c.path) : c.path).split("/").filter(Boolean).map(encodeURIComponent).join("/");
92 const kind = glob || !/\.[A-Za-z0-9]{1,10}$/.test(c.path) ? "tree" : "blob";
93 return `/${c.repo}/${kind}/${ref}${path ? `/${path}` : ""}`;
94}
95
96/** A citation's text in a page's Markdown: a link to the code. */
97export function citationMarkdown(c: Pick<DocCitation, "repo" | "path" | "ref" | "kind" | "label">): string {
98 const shown = c.kind !== "path" && c.label ? c.label : c.path;
99 const ticks = shown.includes("`") ? "``" : "`";
100 return `[${ticks}${shown}${ticks}](${citationHref(c)})`;
101}
102
103/**
104 * Links in Markdown to files and folders in a repository on this site:
105 * `/<owner>/<repo>/blob|tree/<ref>/<path>`, absolute or on this origin.
106 * The ref is one segment (a commit or a branch without a slash).
107 */
108export function citationsFromMarkdown(markdown: string): DocCitation[] {
109 const out: DocCitation[] = [];
110 const seen = new Set<string>();
111 const link = /\]\(\s*<?((?:https?:\/\/[^/\s)]+)?\/([A-Za-z0-9][A-Za-z0-9._-]*)\/([A-Za-z0-9._-]+)\/(?:blob|tree)\/([^/\s)#?]+)\/([^\s)#?>]+))[^)]*\)/g;
112 for (const m of String(markdown ?? "").matchAll(link)) {
113 if (m[1]!.startsWith("http") && !/^https?:\/\/([a-z0-9-]+\.)*g1t\.(sh|dev)(:\d+)?\//i.test(m[1]!)) continue;
114 let path: string;
115 let ref: string;
116 try {
117 path = m[5]!
118 .split("/")
119 .map((p) => decodeURIComponent(p))
120 .join("/");
121 ref = decodeURIComponent(m[4]!);
122 } catch {
123 continue;
124 }
125 const c = cleanCitation({ repo: `${m[2]}/${m[3]}`, path, ref: ref === "HEAD" ? null : ref, kind: "path" }, "body");
126 if (!c || seen.has(`${c.repo}\n${c.path}`)) continue;
127 seen.add(`${c.repo}\n${c.path}`);
128 out.push(c);
129 }
130 return out;
131}
132
133/**
134 * A page's citations from its text: the citation chips (`nodes`), then
135 * links to code not already a chip's own. At most MAX_CITATIONS.
136 */
137export function bodyCitations(nodes: Partial<DocCitation>[], markdown: string): DocCitation[] {
138 const out: DocCitation[] = [];
139 const seen = new Set<string>();
140 const key = (c: DocCitation) => `${c.repo}\n${c.path}\n${c.kind}\n${c.label ?? ""}`;
141 const chips = new Set<string>();
142 for (const n of nodes) {
143 const c = cleanCitation(n, "body");
144 if (!c || seen.has(key(c))) continue;
145 seen.add(key(c));
146 chips.add(citationHref(c));
147 out.push(c);
148 }
149 for (const c of citationsFromMarkdown(markdown)) {
150 // A chip's own link is the chip.
151 if (chips.has(citationHref(c)) || out.some((o) => o.repo === c.repo && o.path === c.path)) continue;
152 if (seen.has(key(c))) continue;
153 seen.add(key(c));
154 out.push(c);
155 }
156 return out.slice(0, MAX_CITATIONS);
157}
158
159/** A glob as a regular expression over a whole path. */
160function globExpression(glob: string): RegExp {
161 let out = "";
162 for (let i = 0; i < glob.length; i++) {
163 const ch = glob[i]!;
164 if (ch === "*") {
165 if (glob[i + 1] === "*") {
166 // `**/` is any folders, or none; a trailing `**` anything below.
167 if (glob[i + 2] === "/") {
168 out += "(?:.*/)?";
169 i += 2;
170 } else {
171 out += ".*";
172 i += 1;
173 }
174 } else out += "[^/]*";
175 } else if (ch === "?") out += "[^/]";
176 else out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
177 }
178 return new RegExp(`^${out}$`);
179}
180
181/**
182 * Whether a change to `changed` touches what `cited` names: the same file,
183 * anything under a cited folder, or a path a glob matches.
184 */
185export function touches(cited: string, changed: string): boolean {
186 if (isGlob(cited)) return globExpression(cited).test(changed);
187 return changed === cited || changed.startsWith(`${cited}/`);
188}
189
190/** Of `changed` paths, those any of the citations names; at most `max`. */
191export function touchedPaths(citations: Pick<DocCitation, "path">[], changed: string[], max = 20): string[] {
192 const out: string[] = [];
193 for (const path of changed) {
194 if (citations.some((c) => touches(c.path, path))) out.push(path);
195 if (out.length >= max) break;
196 }
197 return out;
198}