From 94fcda59a064b5e8952d1bccb6b9a153c39acda1 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Wed, 28 Jan 2026 22:41:00 -0500 Subject: Commits list --- cgithub/src/index.ts | 6 +++++ cgithub/src/scraper.test.ts | 31 ++++++++++++++++++++++++++ cgithub/src/scraper.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) (limited to 'cgithub/src') diff --git a/cgithub/src/index.ts b/cgithub/src/index.ts index d4229fd..4486dff 100644 --- a/cgithub/src/index.ts +++ b/cgithub/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/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts index ae38c44..7cafd9c 100644 --- a/cgithub/src/scraper.test.ts +++ b/cgithub/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/cgithub/src/scraper.ts b/cgithub/src/scraper.ts index e58402a..71d0e3e 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/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 { // 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 { + 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, + }; +} -- cgit v1.3.1