diff options
| author | Joe Mou <dev@mou.fo> | 2026-05-27 14:02:36 -0400 |
|---|---|---|
| committer | Joe Mou <dev@mou.fo> | 2026-05-27 23:37:23 -0400 |
| commit | c57123139794c3ef174b62c6bd2a54a60bbf3327 (patch) | |
| tree | dfa27f2843efd30b2fab6ae65abd233ee488e69d | |
| parent | 123da65350888dc0fbe04003c5ffc36fdded3a46 (diff) | |
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.
| -rw-r--r-- | cgithub/src/index.ts | 26 | ||||
| -rw-r--r-- | cgithub/src/scraper.test.ts | 15 | ||||
| -rw-r--r-- | cgithub/src/scraper.ts | 20 | ||||
| -rw-r--r-- | cgithub/static/style.css | 13 | ||||
| -rw-r--r-- | cgithub/views/blob.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/issues.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/layout.eta | 8 | ||||
| -rw-r--r-- | 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<GitHubIssues> { - 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<GitHubIssues> { + 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<GitH return { number, title: finalTitle, state, createdAt }; }); - return { - repo: { - owner, - name: repo, - }, - issues, - }; + return { repo: { owner, name: repo }, issues, q }; } export async function getGitHubCommits( diff --git a/cgithub/static/style.css b/cgithub/static/style.css index b3ebb46..86a67d8 100644 --- a/cgithub/static/style.css +++ b/cgithub/static/style.css @@ -43,14 +43,20 @@ body { color: var(--text-color); } +a { + text-decoration: none; + color: var(--link-color); +} + h1, h2, h3, h4, h5, h6, p, pre, ul, ol { margin: .5em 0; } -a { - text-decoration: none; - color: var(--link-color); +input, select, button { + background-color: var(--body-background); + color: var(--text-color); + border: 1px solid var(--border-color); } table { @@ -75,6 +81,7 @@ td { nav { display: flex; + margin: .5em 0; > 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" %> <table> <% 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 %>.git</code></pr <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/commits/<%= it.branch %><%= it.path && it.path !== '/' ? '/' + it.path : '' %>">Commits</a> <% } %> </nav> + <form action="/<%= it.repo.owner %>/<%= it.repo.name %>/search" method="get"> + <input type="text" name="q" placeholder="Search…" value="<%= it.q ?? "" %>"> + <select name="type"> + <option value="issues" <%= it.searchType === "issues" ? "selected" : "" %>>Issues</option> + <option value="code" <%= it.searchType === "code" || it.searchType == null ? "selected" : "" %>>Code</option> + </select> + <button type="submit">Search</button> + </form> </div> <%~ 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) %> |
