char/sorcery

static-files based git repo viewer

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

Charlotte Somexperiment: support sha-256 oids in git repos42f80d8

main
3.8 KiB101 linesraw
1import type { Commit, GitObject, Identity, ObjectType, Tag, TreeEntry } from "./types.ts";
2
3export async function inflate(data: Uint8Array): Promise<Uint8Array> {
4  const stream = new Blob([data as BlobPart]).stream().pipeThrough(new DecompressionStream("deflate"));
5  return new Uint8Array(await new Response(stream).arrayBuffer());
6}
7
8const utf8 = new TextDecoder();
9
10export function toHex(bytes: Uint8Array): string {
11  return Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
12}
13
14export function oidBytes(oid: string): Uint8Array {
15  if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(oid)) throw new Error(`invalid object id ${oid}`);
16  return new Uint8Array(oid.match(/../g)!.map(byte => parseInt(byte, 16)));
17}
18
19/** `<type> <size>\0<content>`, as stored zlib-compressed in `objects/xx/` */
20export function parseLoose(raw: Uint8Array): GitObject {
21  const nul = raw.indexOf(0);
22  if (nul === -1) throw new Error("malformed loose object");
23  const [type, size] = utf8.decode(raw.subarray(0, nul)).split(" ");
24  const data = raw.subarray(nul + 1);
25  if (data.length !== Number(size)) throw new Error("loose object size mismatch");
26  return { type: type as ObjectType, data };
27}
28
29function parseIdentity(line: string): Identity {
30  const lt = line.indexOf(" <");
31  const gt = line.indexOf("> ", lt);
32  const [time, tz] = line.slice(gt + 2).split(" ");
33  return {
34    name: line.slice(0, lt),
35    email: line.slice(lt + 2, gt),
36    time: Number(time),
37    tz: tz ?? "+0000",
38  };
39}
40
41export function parseCommit(oid: string, data: Uint8Array): Commit {
42  const text = utf8.decode(data);
43  const blank = text.indexOf("\n\n");
44  const head = blank === -1 ? text : text.slice(0, blank);
45  const message = blank === -1 ? "" : text.slice(blank + 2);
46
47  const commit: Commit = {
48    oid,
49    tree: "",
50    parents: [],
51    author: { name: "", email: "", time: 0, tz: "+0000" },
52    committer: { name: "", email: "", time: 0, tz: "+0000" },
53    message,
54  };
55  // continuation lines (multi-line headers like gpgsig) start with a space
56  for (const line of head.split("\n").filter(l => !l.startsWith(" "))) {
57    const sp = line.indexOf(" ");
58    const key = line.slice(0, sp);
59    const value = line.slice(sp + 1);
60    if (key === "tree") commit.tree = value;
61    else if (key === "parent") commit.parents.push(value);
62    else if (key === "author") commit.author = parseIdentity(value);
63    else if (key === "committer") commit.committer = parseIdentity(value);
64    else if (key === "change-id") commit.changeId = value;
65  }
66  return commit;
67}
68
69/** entries of `<octal mode> <name>\0<raw oid>` */
70export function parseTree(data: Uint8Array, hashBytes: number): TreeEntry[] {
71  if (hashBytes !== 20 && hashBytes !== 32) throw new Error("unsupported object format");
72  const entries: TreeEntry[] = [];
73  let pos = 0;
74  while (pos < data.length) {
75    const sp = data.indexOf(0x20, pos);
76    if (sp === -1) throw new Error("malformed tree object");
77    const nul = data.indexOf(0, sp);
78    if (nul === -1 || nul + 1 + hashBytes > data.length) throw new Error("malformed tree object");
79    entries.push({
80      mode: parseInt(utf8.decode(data.subarray(pos, sp)), 8),
81      name: utf8.decode(data.subarray(sp + 1, nul)),
82      oid: toHex(data.subarray(nul + 1, nul + 1 + hashBytes)),
83    });
84    pos = nul + 1 + hashBytes;
85  }
86  return entries;
87}
88
89export function parseTag(data: Uint8Array): Tag {
90  const text = utf8.decode(data);
91  const blank = text.indexOf("\n\n");
92  const tag: Tag = { object: "", targetType: "commit", name: "", message: text.slice(blank + 2) };
93  for (const line of text.slice(0, blank).split("\n")) {
94    const sp = line.indexOf(" ");
95    const [key, value] = [line.slice(0, sp), line.slice(sp + 1)];
96    if (key === "object") tag.object = value;
97    else if (key === "type") tag.targetType = value as ObjectType;
98    else if (key === "tag") tag.name = value;
99  }
100  return tag;
101}