From 6240cf1c6b73b7ed34ff5816b53333382c229f5c Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Tue, 21 Jul 2026 17:46:58 -0400 Subject: Add async branch/tag dropdown and latest-commit bar Adds two lazily-fetched UI pieces to match vanilla GitHub while keeping initial page loads plain server-rendered HTML: a branch/tag selector (native , CSS anchor-positioned to its button, light-dismissed via closedby="any") and a latest-commit bar above the file listing. Both are populated by new /api/:owner/:repo/refs/:type and /api/:owner/:repo/latest-commit/:branch/:path routes; the latter calls GitHub's own /latest-commit JSON endpoint (the same one the React UI uses) rather than scraping the full commit history page. --- cgithub/views/_refs.eta | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 cgithub/views/_refs.eta (limited to 'cgithub/views/_refs.eta') diff --git a/cgithub/views/_refs.eta b/cgithub/views/_refs.eta new file mode 100644 index 0000000..2450c4b --- /dev/null +++ b/cgithub/views/_refs.eta @@ -0,0 +1,17 @@ +<% if (it.refs.length === 0) { %> +
No <%= it.type %> found.
+<% } else { %> + +<% } %> +<% if (it.hasMore) { %> + +<% } %> -- cgit v1.3.1 From 866f99c414cb3d102483ce21c68a0e62b394baaa Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Tue, 11 Aug 2026 13:32:44 -0400 Subject: Keep the ref picker on the current view and path --- cgithub/public/static/refs.js | 6 +++-- cgithub/src/app.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++++- cgithub/src/app.ts | 12 +++++++--- cgithub/views/_nav.eta | 4 ++-- cgithub/views/_refs.eta | 6 ++++- cgithub/views/_tree.eta | 2 +- cgithub/views/blob.eta | 2 +- cgithub/views/commits.eta | 2 +- 8 files changed, 74 insertions(+), 12 deletions(-) (limited to 'cgithub/views/_refs.eta') diff --git a/cgithub/public/static/refs.js b/cgithub/public/static/refs.js index ffff505..f3e07e8 100644 --- a/cgithub/public/static/refs.js +++ b/cgithub/public/static/refs.js @@ -7,7 +7,9 @@ document.addEventListener("DOMContentLoaded", () => { const dialog = document.querySelector(".ref-selector-menu"); if (dialog) { const list = dialog.querySelector(".ref-selector-list"); - const { owner, repo } = dialog.dataset; + // The view and path let the picker link to the same place on another ref + // rather than to the repository root. + const { owner, repo, view, path = "" } = dialog.dataset; const cache = {}; async function load(type) { @@ -17,7 +19,7 @@ document.addEventListener("DOMContentLoaded", () => { } list.textContent = "Loading…"; try { - const res = await fetch(`/api/${owner}/${repo}/refs/${type}`); + const res = await fetch(`/api/${owner}/${repo}/refs/${type}/${view}/${path}`); const html = res.ok ? await res.text() : ""; cache[type] = html || "Failed to load."; list.innerHTML = cache[type]; diff --git a/cgithub/src/app.test.ts b/cgithub/src/app.test.ts index 498d8a6..6544f03 100644 --- a/cgithub/src/app.test.ts +++ b/cgithub/src/app.test.ts @@ -8,6 +8,56 @@ import { createApp } from "./app.ts"; const eta = new Eta({ views: path.join(import.meta.dirname, "..", "views") }); const app = createApp(eta); +describe("branch picker", () => { + const repo = { owner: "actions", name: "deploy-pages" }; + const refs = [{ name: "main", isDefault: true }, { name: "v4" }]; + + it("should keep the current view and path", () => { + const blob = eta.render("_refs.eta", { + repo, + refs, + type: "branches", + view: "blob", + path: "src/main.ts", + }); + const tree = eta.render("_refs.eta", { + repo, + refs, + type: "branches", + view: "tree", + path: "src", + }); + + assert.match(blob, /href="\/actions\/deploy-pages\/blob\/main\/src\/main.ts"/); + assert.match(blob, /href="\/actions\/deploy-pages\/blob\/v4\/src\/main.ts"/); + assert.match(tree, /href="\/actions\/deploy-pages\/tree\/v4\/src"/); + }); + + it("should link to the root tree without a path", () => { + const html = eta.render("_refs.eta", { repo, refs, type: "branches", view: "tree", path: "" }); + + assert.match(html, /href="\/actions\/deploy-pages\/tree\/main"/); + }); + + it("should not build ref links for views it does not serve", async () => { + const res = await app.request("http://cgithub.example/api/a/b/refs/branches/raw/README.md"); + + assert.strictEqual(res.status, 404); + }); + + it("should tell the refs endpoint where the picker was opened", () => { + const nav = { repo, branch: "main", path: "src" }; + + const blob = eta.render("_nav.eta", { ...nav, view: "blob", path: "src/main.ts" }); + assert.match(blob, /data-view="blob" data-path="src\/main.ts"/); + assert.match( + eta.render("_nav.eta", { ...nav, view: "tree" }), + /data-view="tree" data-path="src"/, + ); + assert.doesNotMatch(eta.render("_nav.eta", { ...nav, view: "tree", path: "" }), /data-path=/); + }); +}); + describe("redirects to GitHub", () => { // An HTTP redirect would be caught by the extension's declarativeNetRequest // rules and bounced straight back here. @@ -21,7 +71,7 @@ describe("redirects to GitHub", () => { const body = await res.text(); assert.match( body, - //, + //, ); assert.doesNotMatch(body, /