aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/index.ts26
-rw-r--r--src/scraper.test.ts15
-rw-r--r--src/scraper.ts20
3 files changed, 50 insertions, 11 deletions
diff --git a/src/index.ts b/src/index.ts
index eb3a0ee..db9915f 100644
--- a/src/index.ts
+++ b/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/src/scraper.test.ts b/src/scraper.test.ts
index 42ddd6e..236b0f5 100644
--- a/src/scraper.test.ts
+++ b/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/src/scraper.ts b/src/scraper.ts
index 04a685b..2412e49 100644
--- a/src/scraper.ts
+++ b/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(