summaryrefslogtreecommitdiff
path: root/cgithub/src
diff options
context:
space:
mode:
authorJoe Mou <dev@mou.fo>2026-05-27 14:02:36 -0400
committerJoe Mou <dev@mou.fo>2026-05-27 23:38:34 -0400
commit745ac9d83234cb060bf802527efd90d301ecc491 (patch)
tree63ff86be00a976e5a45c8905e06b51b37f53eded /cgithub/src
parentc57123139794c3ef174b62c6bd2a54a60bbf3327 (diff)
Pull request index
Diffstat (limited to 'cgithub/src')
-rw-r--r--cgithub/src/index.ts9
-rw-r--r--cgithub/src/scraper.test.ts11
-rw-r--r--cgithub/src/scraper.ts40
3 files changed, 60 insertions, 0 deletions
diff --git a/cgithub/src/index.ts b/cgithub/src/index.ts
index db9915f..514f2e2 100644
--- a/cgithub/src/index.ts
+++ b/cgithub/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/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts
index 236b0f5..a4b834e 100644
--- a/cgithub/src/scraper.test.ts
+++ b/cgithub/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/cgithub/src/scraper.ts b/cgithub/src/scraper.ts
index 2412e49..f32a8da 100644
--- a/cgithub/src/scraper.ts
+++ b/cgithub/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<GitHubPulls> {
+ 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,