summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cgithub/CLAUDE.md1
-rw-r--r--cgithub/src/app.test.ts56
-rw-r--r--cgithub/src/scraper.test.ts33
-rw-r--r--cgithub/views/_nav.eta7
-rw-r--r--cgithub/views/blob.eta3
-rw-r--r--cgithub/views/commits.eta8
6 files changed, 106 insertions, 2 deletions
diff --git a/cgithub/CLAUDE.md b/cgithub/CLAUDE.md
index c0c211d..3f0cd61 100644
--- a/cgithub/CLAUDE.md
+++ b/cgithub/CLAUDE.md
@@ -33,6 +33,7 @@ pnpm test
- `GET /:owner/:repo` - Repository info with directory listing
- `GET /:owner/:repo/tree/:branch/:path*` - Directory listing
- `GET /:owner/:repo/blob/:branch/:path` - File content view
+- `GET /:owner/:repo/commits/:branch/:path*` - Commit history of the repository, a directory, or a file
- `GET /:owner/:repo/wiki` - Wiki home page
- `GET /:owner/:repo/wiki/:page` - Wiki page (also `/:page/:oid` for a revision)
- `GET /:owner/:repo/wiki/_pages` - Wiki page index
diff --git a/cgithub/src/app.test.ts b/cgithub/src/app.test.ts
index 265465e..8519d91 100644
--- a/cgithub/src/app.test.ts
+++ b/cgithub/src/app.test.ts
@@ -155,6 +155,62 @@ describe("blob view", () => {
});
});
+describe("commit history", () => {
+ const repo = { owner: "actions", name: "deploy-pages" };
+ const oid = "b39c421b98f49d83ae50ec502c3ddfc3bf28f2c6";
+ const commitGroups = [
+ {
+ title: "Apr 18, 2023",
+ commits: [
+ { oid, shortMessage: "Remove deprecated input parameters", authors: [{ login: "jmg" }] },
+ ],
+ },
+ ];
+ const history = { repo, branch: "main", path: "src/index.js", commitGroups };
+
+ // A commits payload never says whether its path is a file or a directory, so
+ // every link goes to the tree and GitHub redirects a file's to its blob.
+ it("should browse a file's history at each of its commits", () => {
+ const html = eta.render("commits.eta", history);
+
+ assert.match(html, new RegExp(`href="/actions/deploy-pages/blob/${oid}/src/index.js">Browse`));
+ });
+
+ it("should browse a directory's history the same way", () => {
+ const html = eta.render("commits.eta", { ...history, path: "src" });
+
+ assert.match(html, new RegExp(`href="/actions/deploy-pages/blob/${oid}/src">Browse`));
+ });
+
+ it("should browse the whole repository when the history has no path", () => {
+ const html = eta.render("commits.eta", { ...history, path: "" });
+
+ assert.match(html, new RegExp(`href="/actions/deploy-pages/blob/${oid}">Browse`));
+ });
+
+ // Walking up from a history stays in the history, and its last crumb is the
+ // page you are already on, just as a blob or a tree ends on itself.
+ it("should walk up through the history of each parent directory", () => {
+ const html = eta.render("commits.eta", { ...history, path: "src/lib/index.js" });
+
+ assert.match(html, /href="\/actions\/deploy-pages\/commits\/main">\(top\)</);
+ assert.match(html, /href="\/actions\/deploy-pages\/commits\/main\/src">src</);
+ assert.match(html, /href="\/actions\/deploy-pages\/commits\/main\/src\/lib">lib<\/a>\/index\.js/);
+ });
+
+ it("should say so when a path has no history", () => {
+ const html = eta.render("commits.eta", { ...history, commitGroups: [] });
+
+ assert.match(html, /No commits found\./);
+ });
+
+ it("should link a blob to its own history", () => {
+ const html = eta.render("blob.eta", { ...history, textLines: ["x"] });
+
+ assert.match(html, /href="\/actions\/deploy-pages\/commits\/main\/src\/index.js">History/);
+ });
+});
+
describe("embedded files", () => {
function stubGitHub(t: TestContext, body: string, contentType: string, status = 200) {
t.mock.method(
diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts
index 2690f71..f6be2b6 100644
--- a/cgithub/src/scraper.test.ts
+++ b/cgithub/src/scraper.test.ts
@@ -195,6 +195,39 @@ describe("GitHub scraper", () => {
assert.ok(data.commitGroups.length > 0);
});
+ // GitHub serves a file's history from the same route as a directory's.
+ it("should fetch the history of a blob", async () => {
+ const data = await getGitHubCommits("actions", "deploy-pages", "main", "src/index.js");
+
+ assert.strictEqual(data.path, "src/index.js");
+
+ assert.ok(data.commitGroups.length > 0);
+ const commit = data.commitGroups[0].commits[0];
+ assert.strictEqual(commit.oid.length, 40);
+ assert.ok(commit.shortMessage.length > 0);
+ });
+
+ // What a history page's "Browse" link resolves to: the file as it stood at
+ // one of its commits. Nothing in a commits payload says whether the path is
+ // a file or a directory, so those links all point at the tree and rely on
+ // GitHub redirecting a file's to its blob.
+ it("should redirect a blob browsed at a commit to its blob view", async () => {
+ const oid = "b39c421b98f49d83ae50ec502c3ddfc3bf28f2c6";
+
+ await assert.rejects(getGitHubTree("actions", "deploy-pages", oid, "src/index.js"), (err) => {
+ assert(err instanceof RedirectError, "error should be a RedirectError");
+ assert.strictEqual(err.location, `/actions/deploy-pages/blob/${oid}/src/index.js`);
+ return true;
+ });
+
+ const data = await getGitHubBlob("actions", "deploy-pages", oid, "src/index.js");
+
+ assert.strictEqual(data.branch, oid);
+ assert.strictEqual(data.path, "src/index.js");
+ assert.strictEqual(data.size, "1.37 KB / 51 lines / 40 loc");
+ assert.strictEqual(data.textLines?.length, 51);
+ });
+
it("should fetch releases", async () => {
const data = await getGitHubReleases("actions", "deploy-pages");
diff --git a/cgithub/views/_nav.eta b/cgithub/views/_nav.eta
index a20fbbc..a20741b 100644
--- a/cgithub/views/_nav.eta
+++ b/cgithub/views/_nav.eta
@@ -14,7 +14,10 @@
<div class="ref-selector-list">Loading…</div>
</dialog>
<% if (it.path && it.path !== "/") { %>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/tree/<%= it.branch %>">(top)</a><%
+ <%
+ const crumbHrefBase = `/${it.repo.owner}/${it.repo.name}/${it.view === "blob" ? "tree" : it.view}/${it.branch}`;
+ %>
+ <a href="<%= crumbHrefBase %>">(top)</a><%
const pathParts = it.path.split('/').filter(p => p);
let currentPath = '';
for (let i = 0; i < pathParts.length; i++) {
@@ -24,7 +27,7 @@
if (isLast) {
%>/<%= part %><%= it.view === "tree" ? "/" : "" %><%
} else {
- %>/<a href="/<%= it.repo.owner %>/<%= it.repo.name %>/tree/<%= it.branch %>/<%= currentPath %>"><%= part %></a><%
+ %>/<a href="<%= crumbHrefBase %>/<%= currentPath %>"><%= part %></a><%
}
} %>
<% } %>
diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta
index be9a57e..2014b14 100644
--- a/cgithub/views/blob.eta
+++ b/cgithub/views/blob.eta
@@ -8,6 +8,9 @@
<% } %>
<div><%= it.size %></div>
<div>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/commits/<%= it.branch %>/<%= it.path %>">History</a>
+ </div>
+ <div>
<a href="/<%= it.repo.owner %>/<%= it.repo.name %>/raw/<%= it.branch %>/<%= it.path %>">View raw</a>
</div>
</div>
diff --git a/cgithub/views/commits.eta b/cgithub/views/commits.eta
index 5cf0beb..c29693d 100644
--- a/cgithub/views/commits.eta
+++ b/cgithub/views/commits.eta
@@ -2,6 +2,12 @@
<%~ include("./_nav.eta", {...it, view: "commits"}) %>
+<% const pathSuffix = it.path && it.path !== "/" ? "/" + it.path : "" %>
+
+<% if (it.commitGroups.length === 0) { %>
+ <div class="box">No commits found.</div>
+<% } %>
+
<% for (const group of it.commitGroups) { %>
<h3><%= group.title %></h3>
<% for (const commit of group.commits) { %>
@@ -19,6 +25,8 @@
</div>
<div class="commit-sha">
<code><%= commit.oid.substring(0, 7) %></code>
+ <% /* Like GitHub, blindly link to blob and redirect for trees */ %>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/blob/<%= commit.oid %><%= pathSuffix %>">Browse</a>
</div>
</div>
<% } %>