Last active
December 17, 2024 00:59
-
-
Save yne/f683005540d3d618d04c36c282a44035 to your computer and use it in GitHub Desktop.
Github API based appraise
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <meta charset="utf-8"> | |
| <meta name="color-scheme" content="dark light"> | |
| <title>Peer Review Framework</title> | |
| <link rel="stylesheet" href="style.css"> | |
| <link rel="icon" | |
| href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'><path d='M4,5C2,7,-1.5,4,1,1C4,-1.5,7,2,5,4L8,7L7,8M1.5,1.5C0,3,2,6,4,4C6,2,3,0,1.5,1.5'></path></svg>"> | |
| <nav> | |
| <a href="#/">Home</a> | |
| <a href="#/orgs">My Orgs</a> | |
| <a href="#/repo">My Repos</a> | |
| <a href="#/repo/google/git-appraise">Google/Appraise</a> | |
| </nav> | |
| <main></main> | |
| <progress id="quota" value="1"></progress> | |
| <script> | |
| // @ts-check | |
| const forgeBase = "https://github.com"; | |
| const refReviews = "notes/devtools/reviews"; | |
| const refDiscuss = "notes/devtools/discuss"; | |
| const cachePrefix = 'cache:' | |
| /** | |
| * @template {keyof HTMLElementTagNameMap} T | |
| * @param {T} tagName | |
| * @param {Partial<HTMLElementTagNameMap[T]>} props | |
| * @param {(Node|String)[]} ch | |
| * @returns {HTMLElementTagNameMap[T]} | |
| */ | |
| function el(tagName, props = {}, ch = [], attr = {}) { | |
| const e = Object.assign(document.createElement(tagName), props); | |
| for (const c of ch) typeof c == "string" ? e.appendChild(new Text(c)) : e.appendChild(c); | |
| for (const a in attr) e.setAttribute(a, attr[a]) | |
| return e; | |
| } | |
| function tryParse(jsonOrBlob) { try { return JSON.parse(jsonOrBlob) } catch (_) { return jsonOrBlob } } | |
| const api = async (url, opt) => { | |
| const headers = { ...opt?.headers, Authorization: `Bearer ${localStorage.gheToken}` } | |
| const res = await fetch(url.startsWith('http') ? url : `https://api.github.com/${url}`, { ...opt, headers }); | |
| Object.assign(document.getElementById("quota") || {}, { | |
| ariaDescription: res.headers.get(`github-authentication-token-expiration`), | |
| max: +(res.headers.get(`x-ratelimit-limit`) || 0), | |
| value: +(res.headers.get(`x-ratelimit-remaining`) || 0), // q("used") | |
| title: `Refill in ${(+(res.headers.get(`x-ratelimit-reset`) || 0) - (+new Date() / 1000)) / 60 | 0}min` | |
| }); | |
| if (res.status == 401) throw (res); | |
| const t = res.headers.get('link')?.match(/<(.*?)>; rel="next"/) | |
| if (t) return [...tryParse(await res.text()), ...await api(t[1])] | |
| return tryParse(await res.text()) | |
| }; | |
| api.auth = (token = "") => token ? localStorage.gheToken = token : localStorage.gheToken; | |
| api.flush = () => Object.keys(localStorage).filter(e => e.startsWith(cachePrefix)).forEach(k => delete localStorage[k]) | |
| api.cached = async (url, opt) => tryParse(localStorage[cachePrefix + url] ? localStorage[cachePrefix + url] : tryParse(localStorage[cachePrefix + url] = JSON.stringify(await api(url, opt)))) | |
| async function findBlobInTree(tree_url, sha) { | |
| const next = (await api.cached(tree_url))?.tree?.find(t => sha.startsWith(t.path)); | |
| return (next?.type == "tree") ? findBlobInTree(next?.url, sha.slice(next.path.length)) : next; | |
| } | |
| const loc2url = (l, com = "", pre = (pre, str) => str ? pre + str : '') => (com || l.commit)?.slice(0, 7) + pre("/", l.path) + pre("#", pre("L", l.range?.startLine) + pre('C', l.range?.startColumn) + pre("-L", l.range?.endLine) + pre("C", l.range?.endColumn)) | |
| const profile = (name) => name?.includes('@') ? `mailto:${name}` : `${forgeBase}/${name}`; | |
| const prevent = (e, /**@type {function(HTMLFormElement)}*/cb) => { | |
| const form = /** @type {HTMLFormElement} */(e.target); | |
| e.preventDefault(); | |
| form.ariaBusy = ""; | |
| cb(form).then(e => dispatchEvent(new Event('hashchange'))).catch(e => form.ariaBusy = null); | |
| } | |
| const sha1 = async (text) => [...new Uint8Array(await crypto.subtle.digest('SHA-1', new TextEncoder().encode(text)))].map(n => n.toString(16).padStart(2, "0")).join('') | |
| const input = (/**@type {Partial<HTMLInputElement>}*/props) => el("label", {}, [`${props.name}`, el("input", props)]); | |
| const routes = { | |
| "/": async () => { | |
| const u = await api("user"); | |
| return [el("nav", {}, [ | |
| el("h1", {}, [el("a", { href: u.html_url, innerText: `${u.login}` }), ` (${u.name})`]), | |
| el("img", { src: u.avatar_url, width: 0xFF }), | |
| el("button", { type: "button", innerText: "Flush Cache", onclick: api.flush }), | |
| el("button", { type: "button", innerText: "Flush Token", onclick() { api.auth(" "); dispatchEvent(new Event('hashchange')) } }) | |
| ])] | |
| }, | |
| "/repo": async () => [el('nav', {}, (await api("user/repos")).map(r => el("a", { href: `#/repo/${r.full_name}`, innerText: r.full_name })))], | |
| "/orgs": async () => [el('nav', {}, (await api.cached("user/orgs")).map(o => el("a", { href: `#/orgs/${o.login}`, innerText: o.login })))], | |
| "/orgs/(?<orgName>[^/]+)": async ({ orgName }) => [el('nav', {}, (await api.cached(`orgs/${orgName}/repos`)).map(e => el("a", { href: `#/repo/${e.full_name}`, innerText: e.full_name })))], | |
| "/repo/(?<owner>[^/]+)/(?<repo>[^/]+)/(?<sha>[0-9a-fA-F]+)": async ({ owner, repo, sha }) => { | |
| const discuss_ref = await api(`repos/${owner}/${repo}/git/refs/${refDiscuss}`); | |
| const discuss_com = discuss_ref?.object ? await api.cached(discuss_ref.object.url) : undefined; | |
| const form = (prev = "") => el("article", { className: "create" }, [el("form", { | |
| onsubmit: (e) => prevent(e, async (form) => { // TODO: parse "path" into appraise {location} | |
| const { description, path } = Object.fromEntries([...new FormData(form)]); | |
| const user = await api("user"); | |
| const content = (prev ? prev + "\n" : "") + JSON.stringify({ timestamp: `${+new Date() / 1000 | 0}`, author: user.login, description, location: { path }, v: 0 }) | |
| const tree = await api(`repos/${owner}/${repo}/git/trees`, { method: "POST", body: JSON.stringify({ base_tree: discuss_com?.tree.sha, tree: [{ type: "blob", path: `${sha.slice(0, 2)}/${sha.slice(2)}`, mode: "100644", content }] }) }); | |
| const comm = await api(`repos/${owner}/${repo}/git/commits`, { method: "POST", body: JSON.stringify({ tree: tree.sha, message: "comment added from webapp", parents: discuss_com ? [discuss_com.sha] : [] }) }); | |
| const refs = await api(`repos/${owner}/${repo}/git/refs${discuss_com ? '/' + refDiscuss : ''}`, { method: discuss_com ? "PATCH" : "POST", body: JSON.stringify({ ref: `refs/${refDiscuss}`, sha: comm.sha }) }); | |
| }) | |
| }, [ | |
| input({ name: "path", size: 50, placeholder: `${forgeBase}/${owner}/${repo}/blob/0000000/path/to/file#L4C5-L5C6` }), | |
| input({ name: "description", placeholder: "description", required: true }), | |
| el("button", {}, ["Comment"]) | |
| ])]) | |
| if (!discuss_com) return [el("nav", {}, [`Empty ${refDiscuss}`, form()])] | |
| const commitInfo = el('article', { className: 'reviewCommit' }, [ | |
| el("a", { href: `#/repo/${owner}/${repo}` }, [`${owner}/${repo}`]), | |
| `Last comment:`, el("a", { href: discuss_com.html_url, innerText: discuss_com.sha, ariaColCount: "7" }), | |
| `at`, el('time', { innerText: discuss_com.committer.date, ariaColCount: "10" }), | |
| `by`, el("a", { href: `${discuss_com.tree.url}`, innerText: discuss_com.committer.name }) | |
| ]) | |
| const blob = await api.cached((await findBlobInTree(discuss_com.tree.url, sha)).url); | |
| const md2dom = await import("./md2dom.mjs").then(mod => new mod.default()).catch(e => ({ parse: (md) => [el("code", { innerText: md })] })); | |
| const discusses = await Promise.all(atob(blob.content).split('\n').filter(String).map(async line => ({ d: tryParse(line), sha1: await sha1(line) }))); | |
| return [commitInfo, form(atob(blob.content)), ...discusses.sort(({ d }) => d.timestamp).map(({ d, sha1 }) => el("article", { className: "discuss", id: sha1 }, [ | |
| el("header", {}, [ | |
| ...d.timestamp ? [el("time", { innerText: new Date(1000 * (+d.timestamp || 0)).toISOString(), ariaColCount: "10" })] : [], | |
| ...d.author ? [el("a", { innerText: d.author, href: profile(d.author) })] : [], | |
| ...d.parent ? [el("a", { innerText: d.parent, href: d.parent, ariaColCount: "7" })] : [], | |
| ...d.original ? [el("a", { innerText: d.original, href: d.original, ariaColCount: "7" })] : [], | |
| ...d.location ? ["on", el("a", { href: `${forgeBase}/${owner}/${repo}/blob/${loc2url(d.location, sha)}`, title: JSON.stringify(d.location), innerText: loc2url(d.location, sha) })] : [], | |
| ...d.resolved ? [el("span", { innerText: '✔️', title: 'Resolved' })] : [], | |
| ]), | |
| el("div", {}, [...md2dom.parse(d.description || '')]), | |
| el("footer", {}, [ | |
| el("a", { href: "", innerText: "Comment" }), | |
| el("a", { href: "", innerText: "Resolve" }) | |
| ]), | |
| ]))] | |
| }, | |
| "/repo/(?<owner>[^/]+)/(?<repo>[^/]+)": async ({ owner, repo }) => { | |
| const reviews_ref = await api(`repos/${owner}/${repo}/git/refs/${refReviews}`); | |
| const reviews_com = reviews_ref?.object ? await api.cached(reviews_ref.object.url) : undefined; | |
| const form = (prevs) => el("article", { className: "create" }, [el("form", { | |
| onsubmit: (e) => prevent(e, async (form) => { | |
| const { baseCommit, reviewRef, targetRef, reviewers, description } = Object.fromEntries([...new FormData(form)]); | |
| const prevReview = prevs?.find(prev => prev.blob.path == baseCommit); | |
| const user = await api("user"); | |
| const content = (prevReview ? atob(prevReview.blob.content) + "\n" : "") + JSON.stringify({ timestamp: `${+new Date() / 1000 | 0}`, requester: user.login, baseCommit, reviewRef: reviewRef, targetRef, reviewers: new String(reviewers).split(','), description, v: 0 }) | |
| const tree = await api(`repos/${owner}/${repo}/git/trees`, { method: "POST", body: JSON.stringify({ base_tree: reviews_com?.tree.sha, tree: [{ type: "blob", path: `${baseCommit.slice(0, 2)}/${baseCommit.slice(2)}`, mode: "100644", content }] }) }); | |
| const comm = await api(`repos/${owner}/${repo}/git/commits`, { method: "POST", body: JSON.stringify({ tree: tree.sha, message: "review-request added from webapp", parents: reviews_com ? [reviews_com.sha] : [] }) }); | |
| const refs = await api(`repos/${owner}/${repo}/git/refs${reviews_com ? '/' + refReviews : ''}`, { method: reviews_com ? "PATCH" : "POST", body: JSON.stringify({ ref: `refs/${refReviews}`, sha: comm.sha }) }); | |
| }) | |
| }, [ | |
| input({ name: "baseCommit", pattern: "[0-9a-zA-Z]{40}", placeholder: "base commit SHA1", required: true, value: "17c37fd8532230ffdb3579b4c40bc6df76e2c784" }), | |
| input({ name: "reviewRef", placeholder: "review branch", required: true }), | |
| input({ name: "targetRef", placeholder: "target branch", required: true, value: `develop` }), | |
| input({ name: "reviewers", placeholder: "comma (,) separated reviewer email", required: true }), | |
| input({ name: "description", placeholder: "description", required: true }), | |
| el("button", {}, ["Request a Review"]) | |
| ])]) | |
| if (!reviews_com) return [el("nav", {}, [`Empty ${refReviews}`, form()])] | |
| const commitInfo = el('article', { className: 'reviewCommit' }, [ | |
| el("a", { href: `#/repo/${owner}/${repo}` }, [`${owner}/${repo}`]), | |
| `Last review:`, el("a", { href: reviews_com.html_url, innerText: reviews_com.sha, ariaColCount: "7" }), | |
| `at`, el('time', { innerText: reviews_com.committer.date, ariaColCount: "10" }), | |
| `by`, el("a", { href: `${reviews_com.tree.url}`, innerText: reviews_com.committer.name }) | |
| ]) | |
| const reviews_tree = (await api.cached(`${reviews_com.tree.url}?recursive=1`)).tree.filter(obj => obj.type == 'blob').map(blob => ({ ...blob, path: blob.path.replaceAll(/\//g, '') }));//listBlobs(await fetchTree(reviews_com.tree.url)); | |
| const blobs = (await Promise.all(reviews_tree.map(async b => ({ ...b, ...await api.cached(b.url) })))).map(blob => atob(blob.content).split('\n').filter(String).map(tryParse).map(review => ({ review, blob }))).flat(); | |
| const md2dom = await import("./md2dom.mjs").then(mod => new mod.default()).catch(e => ({ parse: (md) => [el("code", {}, [md||''])] })); | |
| const reviewsSorted = blobs | |
| .sort((a, b) => (+b.review.timestamp || 0) - (+a.review.timestamp || 0)) | |
| .map(({ blob, review }) => el('article', { className: 'review' }, [ | |
| el("header", {}, [ | |
| el("time", { innerText: new Date(1000 * (+review.timestamp || 0)).toISOString(), ariaColCount: "10" }), | |
| `on`, el("a", { href: `${forgeBase}/${owner}/${repo}/commit/${blob.path}`, ariaColCount: "7" }, [blob.path]), | |
| el("a", { href: profile(review.requester), innerText: review.requester }), | |
| `ask `, ...(review.reviewers || []).map(reviewer => el("a", { href: profile(reviewer), innerText: reviewer })), | |
| `to review`, el("a", { href: `${forgeBase}/${owner}/${repo}/compare/${review.baseCommit || review.targetRef}..${review.reviewRef}` }, [`${review.baseCommit?.slice(0, 7) || review.targetRef?.replace(/^refs\/heads\//, '')}..${review.reviewRef?.replace(/^refs\//, '').replace(/\/head$/, '')}`]), | |
| "🢥", el("a", { href: `${forgeBase}/${owner}/${repo}/tree/${review.targetRef}`, innerText: review.targetRef?.replace(/^refs\/heads\//, '') }), | |
| ]), | |
| el("div", {}, [...md2dom.parse(review.description)]), | |
| el("footer", {}, [ | |
| el("a", { href: `#/repo/${owner}/${repo}/${blob.path}` }, ["Open Review"]) | |
| ]) | |
| ])) | |
| return [commitInfo, form(blobs), ...reviewsSorted]//8f3d6b67820f9a0ba1fa6d91cb31317660aff355 fd4109dbd9e1c239d8dde559bad523c6afeed5fb | |
| } | |
| }; | |
| const [main] = document.getElementsByTagName('main'); | |
| window.onhashchange = async function () { | |
| for (const rule in routes) { | |
| const match = new RegExp(`^${rule}$`).exec(location.hash.slice(1) || '/'); | |
| if (!match) continue; | |
| document.querySelectorAll("a").forEach(e => { | |
| e.ariaCurrent = new RegExp(`^${rule}$`).exec(new URL(e.href).hash.slice(1)) ? "page" : null | |
| }); | |
| try { | |
| return main.replaceChildren(...await routes[rule](match.groups)); | |
| } catch (e) { | |
| if (e instanceof Response && e.status == 401) { | |
| return main.replaceChildren(el("nav", {}, [ | |
| el("h1", { innerText: "Github Token" }), "Generate a token", | |
| el("a", { href: `https://github.com/settings/tokens/new?scopes=repo,user:email&description=prf-${(new Date()).toISOString().slice(0, 7)}`, target: "_blank", innerText: "here" }), | |
| "and, past it here:", | |
| el("input", { placeholder: "ghp_xxxx", size: 40, value: api.auth(), oninput() { api.auth((/**@type {HTMLInputElement}*/(this)).value) } }), | |
| el("button", { type: "button", innerText: "save", onclick() { dispatchEvent(new Event('hashchange')) } }) | |
| ])); | |
| } | |
| console.error(e) | |
| return main.replaceChildren(el("pre", { innerText: JSON.stringify(e, null, '\t') })); | |
| } | |
| } | |
| main.replaceChildren(el("h1", { innerText: "404" })); | |
| }; | |
| dispatchEvent(new Event('hashchange')); | |
| </script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment