diff options
| author | Joe Mou <dev@mou.fo> | 2026-01-28 22:41:00 -0500 |
|---|---|---|
| committer | Joe Mou <dev@mou.fo> | 2026-01-28 23:03:09 -0500 |
| commit | 7416f8b40fd07031c56584bd089f910185d58f89 (patch) | |
| tree | 7c9fd93dbe03ca69bcfebd9e82c31f9491a5a967 | |
| parent | 8770424ae7a0affbb4b05903d355fe856f853797 (diff) | |
Commits list
| -rw-r--r-- | src/index.ts | 6 | ||||
| -rw-r--r-- | src/scraper.test.ts | 31 | ||||
| -rw-r--r-- | src/scraper.ts | 53 | ||||
| -rw-r--r-- | static/style.css | 20 | ||||
| -rw-r--r-- | views/commits.eta | 25 | ||||
| -rw-r--r-- | views/layout.eta | 3 |
6 files changed, 138 insertions, 0 deletions
diff --git a/src/index.ts b/src/index.ts index d4229fd..4486dff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import path from "node:path"; import url from "node:url"; import { getGitHubBlob, + getGitHubCommits, getGitHubIssues, getGitHubRepo, getGitHubTree, @@ -67,6 +68,11 @@ app.get("/:owner/:repo/issues", async (c) => { return tryRender(c, "issues.eta", getGitHubIssues(owner, repo)); }); +app.get("/:owner/:repo/commits/:branch/:path{.*}?", async (c) => { + const { owner, repo, branch, path = "" } = c.req.param(); + return tryRender(c, "commits.eta", getGitHubCommits(owner, repo, branch, path)); +}); + app.all("*", async (c) => { const path = c.req.path; c.header("Referrer-Policy", "no-referrer"); diff --git a/src/scraper.test.ts b/src/scraper.test.ts index ae38c44..7cafd9c 100644 --- a/src/scraper.test.ts +++ b/src/scraper.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert"; import { describe, it } from "node:test"; import { getGitHubBlob, + getGitHubCommits, getGitHubIssues, getGitHubRepo, getGitHubTree, @@ -128,6 +129,36 @@ describe("GitHub scraper", () => { assert.strictEqual(issue402.state, "OPEN"); assert.strictEqual(issue402.createdAt, "2025-07-03T19:07:08Z"); }); + + it("should fetch commits", async () => { + const data = await getGitHubCommits("actions", "deploy-pages", "main", ""); + + assert.strictEqual(data.repo.owner, "actions"); + assert.strictEqual(data.repo.name, "deploy-pages"); + assert.strictEqual(data.branch, "main"); + assert.strictEqual(data.path, ""); + + assert.ok(data.commitGroups.length > 0); + const firstGroup = data.commitGroups[0]; + assert.ok(firstGroup.title.length > 0); + assert.ok(firstGroup.commits.length > 0); + + const firstCommit = firstGroup.commits[0]; + assert.ok(firstCommit.oid.length === 40); + assert.ok(firstCommit.shortMessage.length > 0); + assert.ok(firstCommit.authors.length > 0); + }); + + it("should fetch commits with path", async () => { + const data = await getGitHubCommits("actions", "deploy-pages", "main", "src"); + + assert.strictEqual(data.repo.owner, "actions"); + assert.strictEqual(data.repo.name, "deploy-pages"); + assert.strictEqual(data.branch, "main"); + assert.strictEqual(data.path, "src"); + + assert.ok(data.commitGroups.length > 0); + }); }); describe("rich file types", () => { diff --git a/src/scraper.ts b/src/scraper.ts index e58402a..71d0e3e 100644 --- a/src/scraper.ts +++ b/src/scraper.ts @@ -54,6 +54,26 @@ interface IssueIndexPageQuery { type StylingDirective = [number, number, string]; +interface Commit { + oid: string; + url: string; + authoredDate: string; + committedDate: string; + shortMessage: string; + bodyMessageHtml: string; + authors: { + login: string; + displayName: string; + avatarUrl: string; + path: string; + }[]; +} + +export interface CommitGroup { + title: string; + commits: Commit[]; +} + interface RawPayload { repo: { ownerLogin: string; @@ -82,6 +102,7 @@ interface RawPayload { overview?: { overviewFiles?: OverviewFile[]; }; + commitGroups?: CommitGroup[]; // Actually there may be other query types. preloadedQueries?: IssueIndexPageQuery[]; } @@ -159,6 +180,10 @@ export interface GitHubIssues extends GitHubCommon { issues: Issue[]; } +export interface GitHubCommits extends GitHubNav { + commitGroups: CommitGroup[]; +} + async function fetchGitHubPage(path: string): Promise<string> { // GitHub throttles/blocks requests without realistic browser headers. // These headers make the request appear as a standard browser visit. @@ -350,3 +375,31 @@ export async function getGitHubIssues(owner: string, repo: string): Promise<GitH issues, }; } + +export async function getGitHubCommits( + owner: string, + repo: string, + branch: string, + path: string, +): Promise<GitHubCommits> { + const urlPath = path + ? `${owner}/${repo}/commits/${branch}/${path}` + : `${owner}/${repo}/commits/${branch}`; + const html = await fetchGitHubPage(urlPath); + + const payload = parsePayload(html, "react-app.embeddedData", ["payload"]); + + if (payload?.commitGroups === undefined) { + throw new Error("Could not find commit history in embedded JSON"); + } + + return { + repo: { + owner, + name: repo, + }, + branch, + path, + commitGroups: payload.commitGroups, + }; +} diff --git a/static/style.css b/static/style.css index 2020dc2..5673aa7 100644 --- a/static/style.css +++ b/static/style.css @@ -178,6 +178,26 @@ pre.box { font-size: .8rem; } +.commit { + display: flex; + justify-content: space-between; + padding: 0.5rem; + border: 1px solid var(--border-color); + + .commit { + border-top: none; + } +} + +.commit-author { + font-size: 0.8rem; + margin-top: 0.2rem; +} + +.commit-sha { + align-self: center; + font-size: 0.8rem; +} + /* TODO Strip permalinks? */ svg.octicon-link { display: none; diff --git a/views/commits.eta b/views/commits.eta new file mode 100644 index 0000000..2ddcba3 --- /dev/null +++ b/views/commits.eta @@ -0,0 +1,25 @@ +<% layout("./layout.eta") %> + +<%~ include("./_nav.eta", {...it, isTree: false}) %> + +<% for (const group of it.commitGroups) { %> + <h3><%= group.title %></h3> + <% for (const commit of group.commits) { %> + <div class="commit"> + <div> + <div> + <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/commit/<%= commit.oid %>"><%= commit.shortMessage %></a> + </div> + <div class="commit-author"> + by + <% for (const author of commit.authors) { %> + <a href="/<%= author.login %>"><%= author.login %></a> + <% } %> + </div> + </div> + <div class="commit-sha"> + <code><%= commit.oid.substring(0, 7) %></code> + </div> + </div> + <% } %> +<% } %> diff --git a/views/layout.eta b/views/layout.eta index f4535df..a404437 100644 --- a/views/layout.eta +++ b/views/layout.eta @@ -13,6 +13,9 @@ git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git</code></pr <nav> <a href="/<%= it.repo.owner %>/<%= it.repo.name %>">Code</a> <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/issues">Issues</a> + <% if (it.branch) { %> + <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/commits/<%= it.branch %><%= it.path && it.path !== '/' ? '/' + it.path : '' %>">Commits</a> + <% } %> </nav> </div> |
