aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoe Mou <dev@mou.fo>2026-01-28 22:41:00 -0500
committerJoe Mou <dev@mou.fo>2026-01-28 23:03:09 -0500
commit7416f8b40fd07031c56584bd089f910185d58f89 (patch)
tree7c9fd93dbe03ca69bcfebd9e82c31f9491a5a967 /src
parent8770424ae7a0affbb4b05903d355fe856f853797 (diff)
Commits list
Diffstat (limited to 'src')
-rw-r--r--src/index.ts6
-rw-r--r--src/scraper.test.ts31
-rw-r--r--src/scraper.ts53
3 files changed, 90 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,
+ };
+}