forks/sorcery

git clone https://git.gae.moe/forks/sorcery

Charlotte Somshow absolute dates in commit diffsbfa33be

typst_support
5.8 KiB153 linesraw
1import { treeDiff, type FileChange } from "./git/walk.ts";
2import { diffLines } from "./git/diff.ts";
3import type { Site, View } from "./route.ts";
4import { href, topbar } from "./tree.tsx";
5import { identityDate, looksBinary, looksGenerated, plural, utf8 } from "./util.ts";
6
7const highlightedLines = (path: string, source: string) =>
8  import("./highlight.ts").then(module => module.highlightedLines(path, source));
9
10export async function commitView(site: Site, view: View): Promise<Node[]> {
11  const { repo } = site;
12  const { oid } = view;
13  await repo.prefetch([oid], { smart: "commit-diff" });
14  const commit = await repo.commit(oid);
15  const parentTree = commit.parents.length > 0
16    ? (await repo.commit(commit.parents[0])).tree
17    : null;
18  const changes = await treeDiff(repo, parentTree, commit.tree);
19  const blobs = changes.flatMap(change =>
20    [change.oldOid, change.newOid].filter(oid => oid !== null)
21  );
22  await repo.prefetch(blobs, { depth: 0 });
23  const files = await Promise.all(changes.map(change => fileSection(site, oid, change)));
24  const adds = files.reduce((n, f) => n + f.adds, 0);
25  const dels = files.reduce((n, f) => n + f.dels, 0);
26
27  const [subject, ...rest] = commit.message.trimEnd().split("\n");
28  const body = rest.join("\n").trim();
29
30  const meta = (<table class="commit-meta" />) as HTMLTableElement;
31  const row = (key: string, value: Node | string) =>
32    meta.append(
33      <tr>
34        <th>{key}</th>
35        <td _also={el => el.append(typeof value === "string" ? document.createTextNode(value) : value)} />
36      </tr>,
37    );
38  row("author", `${commit.author.name} <${commit.author.email}>`);
39  row("date", (
40    <time dateTime={new Date(commit.author.time * 1000).toISOString()}>
41      {identityDate(commit.author)}
42    </time>
43  ));
44  row("commit", <span class="sha">{oid}</span>);
45  for (const parent of commit.parents) {
46    row("parent", <a class="sha" href={`#commit/${parent}`}>{parent}</a>);
47  }
48  if (commit.changeId) row("change-id", <span class="cid">{commit.changeId}</span>);
49
50  const browse = <a href={await href(site, { kind: "tree", oid, path: [] })}>browse files</a>;
51  return [
52    await topbar(site, view, [], [browse]),
53    <h2 class="commit-subject">{subject}</h2>,
54    ...(body ? [<pre class="commit-body">{body}</pre>] : []),
55    meta,
56    <p class="meta diffstat">
57      <span class="add-count">+{String(adds)}</span>
58      <span class="del-count">-{String(dels)}</span>
59      <span>{plural(changes.length, "changed file")}</span>
60    </p>,
61    ...files.map(f => f.element),
62  ];
63}
64
65interface FileSection {
66  element: HTMLElement;
67  adds: number;
68  dels: number;
69}
70
71async function fileSection(site: Site, revision: string, change: FileChange): Promise<FileSection> {
72  const { repo } = site;
73  const generated = looksGenerated(change.path);
74  const actions = (<span class="file-actions" />) as HTMLElement;
75  if (change.status !== "modified") {
76    actions.append(<span class={`status ${change.status}`}>{change.status}</span>);
77  }
78  if (change.newOid) {
79    const view = await href(site, { kind: "blob", oid: revision, path: change.path.split("/") });
80    actions.append(<a class="status" href={view}>view</a>);
81  }
82  if (generated) actions.append(<span class="generated-note">generated</span>);
83  const header = (
84    <summary>
85      <span class="file-summary-content">
86        <span class="path">{change.path}</span>
87        {actions}
88      </span>
89    </summary>
90  ) as HTMLElement;
91  const section = (<details class="file-diff">{header}</details>) as HTMLDetailsElement;
92  section.open = !generated;
93
94  const [oldData, newData] = await Promise.all([
95    change.oldOid ? repo.blob(change.oldOid) : Promise.resolve(new Uint8Array()),
96    change.newOid ? repo.blob(change.newOid) : Promise.resolve(new Uint8Array()),
97  ]);
98  if (looksBinary(oldData) || looksBinary(newData)) {
99    section.append(<p class="meta binary-note">binary file</p>);
100    return { element: section, adds: 0, dels: 0 };
101  }
102
103  const oldText = utf8.decode(oldData);
104  const newText = utf8.decode(newData);
105  const hunks = diffLines(oldText, newText);
106  const cells: Array<{ cell: HTMLTableCellElement; old: boolean; line: number }> = [];
107  let adds = 0;
108  let dels = 0;
109  const table = (<table class="diff" />) as HTMLTableElement;
110  for (const hunk of hunks) {
111    table.append(
112      <tr class="hunk">
113        <td class="ln" />
114        <td class="ln" />
115        <td>{`@@ -${hunk.aStart},${hunk.aLines} +${hunk.bStart},${hunk.bLines} @@`}</td>
116      </tr>,
117    );
118    let aLine = hunk.aStart;
119    let bLine = hunk.bStart;
120    for (const line of hunk.lines) {
121      const kind = line.sign === "+" ? "add" : line.sign === "-" ? "del" : "ctx";
122      const old = line.sign === "-";
123      const sourceLine = (old ? aLine : bLine) - 1;
124      if (line.sign === "+") adds++;
125      if (line.sign === "-") dels++;
126      const cell = (<td class="text">{line.text}</td>) as HTMLTableCellElement;
127      cells.push({ cell, old, line: sourceLine });
128      table.append(
129        <tr class={kind}>
130          <td class="ln">{line.sign === "+" ? "" : String(aLine++)}</td>
131          <td class="ln">{line.sign === "-" ? "" : String(bLine++)}</td>
132          {/* the +/- marker is CSS ::before so copied text stays clean */}
133          {cell}
134        </tr>,
135      );
136    }
137  }
138  actions.prepend(
139    <span class="add-count">+{String(adds)}</span>,
140    <span class="del-count">-{String(dels)}</span>,
141  );
142  section.append(table);
143  void Promise.all([
144    change.oldOid ? highlightedLines(change.path, oldText) : Promise.resolve(null),
145    change.newOid ? highlightedLines(change.path, newText) : Promise.resolve(null),
146  ]).then(([oldLines, newLines]) => {
147    for (const target of cells) {
148      const line = (target.old ? oldLines : newLines)?.[target.line];
149      if (line) target.cell.replaceChildren(...line.map(node => node.cloneNode(true)));
150    }
151  }).catch(err => console.warn("arborium:", err));
152  return { element: section, adds, dels };
153}