char/sorcery

static-files based git repo viewer

git clone https://git.t4t.associates/char/sorcery

Charlotte Somfeat: give commits absolute <time> timestamps, relativized while recentc166af0

main
2.9 KiB84 linesraw
1import type { Commit, LocatedObject } from "./git/types.ts";
2import { log, type PathChange, pathHistory } from "./git/walk.ts";
3import { paginate } from "./pagination.tsx";
4import { canonical, type Site, type View } from "./route.ts";
5import { kindAtTip, topbar } from "./tree.tsx";
6import { commitTime, plural } from "./util.ts";
7
8const TREE = 0o040000;
9const HISTORY_PREFETCH = 25;
10
11async function* commitHistory(
12  site: Site,
13  start: string,
14  scanned: (count: number) => void,
15): AsyncGenerator<PathChange> {
16  for await (const commit of log(site.repo, [start], HISTORY_PREFETCH)) {
17    scanned(1);
18    yield { commit, object: { oid: commit.tree, mode: TREE } };
19  }
20}
21
22const snapshotKind = (object: LocatedObject): "tree" | "blob" =>
23  (object.mode & 0o170000) === TREE ? "tree" : "blob";
24
25export async function historyView(site: Site, view: View): Promise<Node[]> {
26  const { oid, path } = view;
27  const commitList = path.length === 0;
28  const atTip = await kindAtTip(site, path);
29  const list = (<ol class="log history-log" />) as HTMLOListElement;
30  const status = (<p class="meta history-status" />) as HTMLElement;
31  const control = (<p class="meta log-pagination" />) as HTMLElement;
32  let scanned = 0;
33  const scannedCommits = (count: number) => {
34    scanned += count;
35    status.textContent = `scanned ${plural(scanned, "commit")}`;
36  };
37  paginate<PathChange>({
38    list,
39    control,
40    open: () => {
41      scanned = 0;
42      return commitList
43        ? commitHistory(site, oid, scannedCommits)
44        : pathHistory(site.repo, [oid], path, scannedCommits);
45    },
46    key: change => change.commit.oid,
47    render: change => {
48      // rows of a file's history link to the file at that commit, not the commit's diff
49      const target = !commitList && change.object
50        ? canonical(site.page, { kind: snapshotKind(change.object), oid: change.commit.oid, path }, atTip)
51        : `#commit/${change.commit.oid}`;
52      const who = (
53        <span class="who">
54          <span>{change.commit.author.name}</span>
55          {commitTime(change.commit.author)}
56        </span>
57      ) as HTMLElement;
58      if (!change.object) who.append(<span class="deleted">deleted</span>);
59      const item = (<li />) as HTMLLIElement;
60      if (change.commit.changeId) {
61        item.append(<a class="cid" href={target}>{change.commit.changeId.slice(0, 8)}</a>, " ");
62      }
63      item.append(
64        <a class="sha" href={target}>{change.commit.oid.slice(0, 7)}</a>,
65        who,
66        <span class="msg">{change.commit.message.split("\n")[0]}</span>,
67      );
68      return item;
69    },
70    onShow: count => {
71      status.replaceChildren(<span>{plural(count, commitList ? "commit" : "change")}</span>);
72    },
73    onError: err => {
74      status.textContent = `history failed: ${String(err)}`;
75    },
76  });
77
78  return [
79    await topbar(site, view, [commitList ? "commit history" : "change history"]),
80    status,
81    list,
82    control,
83  ];
84}