From c57123139794c3ef174b62c6bd2a54a60bbf3327 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Wed, 27 May 2026 14:02:36 -0400 Subject: Add repo-scoped search support for issues and PRs We do not support hybrid issue/PR search. Any searches on the issues endpoint will redirect to an explicit issue-only search. --- cgithub/src/index.ts | 26 +++++++++++++++++++++++++- cgithub/src/scraper.test.ts | 15 +++++++++++++++ cgithub/src/scraper.ts | 20 ++++++++++---------- cgithub/static/style.css | 13 ++++++++++--- cgithub/views/blob.eta | 2 +- cgithub/views/issues.eta | 2 +- cgithub/views/layout.eta | 8 ++++++++ cgithub/views/tree.eta | 2 +- 8 files changed, 71 insertions(+), 17 deletions(-) diff --git a/cgithub/src/index.ts b/cgithub/src/index.ts index eb3a0ee..db9915f 100644 --- a/cgithub/src/index.ts +++ b/cgithub/src/index.ts @@ -70,7 +70,31 @@ app.get("/:owner/:repo/raw/:branch/:path{.*}", async (c) => { app.get("/:owner/:repo/issues", async (c) => { const { owner, repo } = c.req.param(); - return tryRender(c, "issues.eta", getGitHubIssues(owner, repo)); + const q = c.req.query("q"); + + // GitHub supports combined issue/PR search, apparently for legacy. We just + // redirect to issue-only search. + if (q && !/\bis:issue\b/.test(q)) { + return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(`is:issue ${q}`)}`); + } + + return tryRender(c, "issues.eta", getGitHubIssues(owner, repo, { q })); +}); + +app.get("/:owner/:repo/search", async (c) => { + const { owner, repo } = c.req.param(); + const q = c.req.query("q") ?? ""; + const type = c.req.query("type") ?? "issues"; + + if (type === "issues") { + return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(q)}`); + } + + // Redirect unhandled search types (like code, which requires sign in anyway). + c.header("Referrer-Policy", "no-referrer"); + return c.redirect( + `https://github.com/${owner}/${repo}/search?${new URLSearchParams(c.req.query()).toString()}`, + ); }); app.get("/:owner/:repo/commits/:branch/:path{.*}?", async (c) => { diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts index 42ddd6e..236b0f5 100644 --- a/cgithub/src/scraper.test.ts +++ b/cgithub/src/scraper.test.ts @@ -127,6 +127,7 @@ describe("GitHub scraper", () => { assert.strictEqual(data.repo.owner, "actions"); assert.strictEqual(data.repo.name, "deploy-pages"); + assert.strictEqual(data.q, undefined); const issue402 = data.issues.find((issue) => issue.number === 402); assert.ok(issue402); @@ -135,6 +136,20 @@ describe("GitHub scraper", () => { assert.strictEqual(issue402.createdAt, "2025-07-03T19:07:08Z"); }); + it("should fetch filtered issues with q", async () => { + const data = await getGitHubIssues("actions", "deploy-pages", { + q: "is:issue state:closed", + }); + + assert.strictEqual(data.repo.owner, "actions"); + assert.strictEqual(data.repo.name, "deploy-pages"); + assert.strictEqual(data.q, "is:issue state:closed"); + const issue417 = data.issues.find((issue) => issue.number === 417); + assert.ok(issue417); + assert.strictEqual(issue417.title, "Chats"); + assert.ok(data.issues.every((issue) => issue.title !== "")); + }); + 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 04a685b..2412e49 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/src/scraper.ts @@ -258,6 +258,7 @@ interface Issue { export interface GitHubIssues extends GitHubCommon { issues: Issue[]; + q?: string; } export interface GitHubCommits extends GitHubNav { @@ -513,9 +514,14 @@ export async function getGitHubBlob( }); } -export async function getGitHubIssues(owner: string, repo: string): Promise { - const html = await fetchGitHubPage(`${owner}/${repo}/issues`); - +export async function getGitHubIssues( + owner: string, + repo: string, + // We only support is:issue searches (no PR results). + { q }: { q?: string } = {}, +): Promise { + const suffix = q ? `?q=${encodeURIComponent(q)}` : ""; + const html = await fetchGitHubPage(`${owner}/${repo}/issues${suffix}`); const payload = parsePayload(html); let issuesQuery: IssueIndexPageQuery | undefined; @@ -534,13 +540,7 @@ export async function getGitHubIssues(owner: string, repo: string): Promise a { display: block; padding-right: .5rem; diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta index ea07059..50f92a9 100644 --- a/cgithub/views/blob.eta +++ b/cgithub/views/blob.eta @@ -1,4 +1,4 @@ -<% layout("./layout.eta") %> +<% layout("./layout.eta"); it.searchType = "code" %> <%~ include("./_nav.eta", it) %> diff --git a/cgithub/views/issues.eta b/cgithub/views/issues.eta index 6e4c9ba..646da46 100644 --- a/cgithub/views/issues.eta +++ b/cgithub/views/issues.eta @@ -1,4 +1,4 @@ -<% layout("./layout") %> +<% layout("./layout"); it.searchType = "issues" %> <% for (const issue of it.issues) { %> diff --git a/cgithub/views/layout.eta b/cgithub/views/layout.eta index ec3c7b8..f31c6e0 100644 --- a/cgithub/views/layout.eta +++ b/cgithub/views/layout.eta @@ -19,6 +19,14 @@ git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.gitCommits <% } %> + + "> + + + <%~ it.body %> diff --git a/cgithub/views/tree.eta b/cgithub/views/tree.eta index 6e675e1..41ded06 100644 --- a/cgithub/views/tree.eta +++ b/cgithub/views/tree.eta @@ -1,3 +1,3 @@ -<% layout("./layout.eta") %> +<% layout("./layout.eta"); it.searchType = "code" %> <%~ include("./_tree.eta", it) %> -- cgit v1.3.1