From 15206d169c89b8a337b3f929a23ff3f71cdd5a89 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Wed, 27 May 2026 14:02:36 -0400 Subject: Pull request index --- src/index.ts | 9 +++++++++ src/scraper.test.ts | 11 +++++++++++ src/scraper.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) (limited to 'src') diff --git a/src/index.ts b/src/index.ts index db9915f..514f2e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { getGitHubBlob, getGitHubCommits, getGitHubIssues, + getGitHubPulls, getGitHubRelease, getGitHubReleases, getGitHubRepo, @@ -81,6 +82,12 @@ app.get("/:owner/:repo/issues", async (c) => { return tryRender(c, "issues.eta", getGitHubIssues(owner, repo, { q })); }); +app.get("/:owner/:repo/pulls", async (c) => { + const { owner, repo } = c.req.param(); + const q = c.req.query("q"); + return tryRender(c, "pulls.eta", getGitHubPulls(owner, repo, { q })); +}); + app.get("/:owner/:repo/search", async (c) => { const { owner, repo } = c.req.param(); const q = c.req.query("q") ?? ""; @@ -88,6 +95,8 @@ app.get("/:owner/:repo/search", async (c) => { if (type === "issues") { return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(q)}`); + } else if (type === "pullrequests") { + return c.redirect(`/${owner}/${repo}/pulls?q=${encodeURIComponent(q)}`); } // Redirect unhandled search types (like code, which requires sign in anyway). diff --git a/src/scraper.test.ts b/src/scraper.test.ts index 236b0f5..a4b834e 100644 --- a/src/scraper.test.ts +++ b/src/scraper.test.ts @@ -4,6 +4,7 @@ import { getGitHubBlob, getGitHubCommits, getGitHubIssues, + getGitHubPulls, getGitHubRelease, getGitHubReleases, getGitHubRepo, @@ -150,6 +151,16 @@ describe("GitHub scraper", () => { assert.ok(data.issues.every((issue) => issue.title !== "")); }); + it("should fetch pull requests", async () => { + const data = await getGitHubPulls("actions", "deploy-pages", { q: "state:closed" }); + + assert.strictEqual(data.repo.owner, "actions"); + assert.strictEqual(data.repo.name, "deploy-pages"); + const pr411 = data.pulls.find((pull) => pull.number === 411); + assert.ok(pr411); + assert.strictEqual(pr411.title, "update node version"); + }); + it("should fetch commits", async () => { const data = await getGitHubCommits("actions", "deploy-pages", "main", ""); diff --git a/src/scraper.ts b/src/scraper.ts index 2412e49..f32a8da 100644 --- a/src/scraper.ts +++ b/src/scraper.ts @@ -261,6 +261,11 @@ export interface GitHubIssues extends GitHubCommon { q?: string; } +export interface GitHubPulls extends GitHubCommon { + pulls: Issue[]; + q?: string; +} + export interface GitHubCommits extends GitHubNav { commitGroups: CommitGroup[]; } @@ -543,6 +548,41 @@ export async function getGitHubIssues( return { repo: { owner, name: repo }, issues, q }; } +export async function getGitHubPulls( + owner: string, + repo: string, + { q }: { q?: string } = {}, +): Promise { + const suffix = q ? `?q=${encodeURIComponent(q)}` : ""; + const html = await fetchGitHubPage(`${owner}/${repo}/pulls${suffix}`); + const document = parseDocument(html); + + const pulls: Issue[] = []; + for (const row of cssSelect.selectAll(".js-issue-row", document) as unknown as Element[]) { + const id = getAttributeValue(row, "id"); // "issue_NNN" + const number = id ? parseInt(id.replace("issue_", ""), 10) : 0; + if (!number) continue; + + const titleLink = cssSelect.selectOne("a.markdown-title", row); + const title = titleLink ? textContent(titleLink).trim() : ""; + + const timeElem = cssSelect.selectOne("relative-time", row); + const createdAt = timeElem ? getAttributeValue(timeElem, "datetime") || "" : ""; + + const stateSpan = cssSelect.selectOne(".tooltipped[aria-label]", row) as Element | null; + const ariaLabel = stateSpan ? (getAttributeValue(stateSpan, "aria-label") ?? "") : ""; + const state = ariaLabel.includes("Merged") + ? "MERGED" + : ariaLabel.includes("Closed") + ? "CLOSED" + : "OPEN"; + + pulls.push({ number, title, state, createdAt }); + } + + return { repo: { owner, name: repo }, pulls, q }; +} + export async function getGitHubCommits( owner: string, repo: string, -- cgit v1.3.1