aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/app.test.ts20
-rw-r--r--src/scraper.test.ts13
-rw-r--r--src/scraper.ts53
-rw-r--r--views/layouts/repo.eta20
4 files changed, 85 insertions, 21 deletions
diff --git a/src/app.test.ts b/src/app.test.ts
index 59a2c61..e870666 100644
--- a/src/app.test.ts
+++ b/src/app.test.ts
@@ -92,6 +92,26 @@ describe("sidebar fragment", () => {
});
});
+describe("repository navigation", () => {
+ const releases = { repo: { owner: "torvalds", name: "linux" }, releases: [] };
+
+ it("should only link to the tabs GitHub shows", () => {
+ const html = eta.render("releases.eta", { ...releases, tabs: ["code", "actions", "wiki"] });
+
+ assert.match(html, /href="\/torvalds\/linux\/wiki"/);
+ assert.doesNotMatch(html, /href="\/torvalds\/linux\/issues"/);
+ assert.doesNotMatch(html, /href="\/torvalds\/linux\/pulls"/);
+ assert.doesNotMatch(html, /href="\/torvalds\/linux\/discussions"/);
+ });
+
+ it("should link to every tab without GitHub's navigation to go by", () => {
+ const html = eta.render("releases.eta", { ...releases, tabs: null });
+
+ assert.match(html, /href="\/torvalds\/linux\/issues"/);
+ assert.match(html, /href="\/torvalds\/linux\/wiki"/);
+ });
+});
+
describe("blob view", () => {
const blob = {
repo: { owner: "justrajdeep", name: "fonts" },
diff --git a/src/scraper.test.ts b/src/scraper.test.ts
index a8debd7..83a82f0 100644
--- a/src/scraper.test.ts
+++ b/src/scraper.test.ts
@@ -45,6 +45,10 @@ describe("GitHub scraper", () => {
assert.ok(data.items.length > 0);
assert.ok(typeof data.overviewHtml?.["README.md"] === "string");
+
+ assert.ok(data.tabs?.includes("issues"));
+ assert.ok(data.tabs?.includes("pull-requests"));
+ assert.ok(!data.tabs?.includes("wiki"));
});
it("should fetch tree", async () => {
@@ -434,6 +438,7 @@ describe("GitHub scraper", () => {
assert.strictEqual(data.repo.owner, "microsoft");
assert.strictEqual(data.repo.name, "vscode");
assert.strictEqual(data.title, "Home");
+ assert.ok(data.tabs?.includes("wiki"));
assert.strictEqual(data.updatedBy, "Greg Van Liew");
assert.strictEqual(data.updatedAt, "2019-11-22T03:08:32Z");
@@ -496,6 +501,14 @@ describe("GitHub scraper", () => {
assert.strictEqual(home.updatedAt, "2018-02-08T18:57:56Z");
});
+ it("should leave out the tabs of features a repository has disabled", async () => {
+ const data = await getGitHubReleases("torvalds", "linux");
+
+ assert.ok(data.tabs?.includes("code"));
+ assert.ok(!data.tabs?.includes("issues"));
+ assert.ok(!data.tabs?.includes("pull-requests"));
+ });
+
it("should redirect to the repository when it has no wiki", async () => {
await assert.rejects(getGitHubWiki("git", "git"), (err) => {
assert(err instanceof InternalRedirectError, "error should be an InternalRedirectError");
diff --git a/src/scraper.ts b/src/scraper.ts
index 19b8318..ac1820a 100644
--- a/src/scraper.ts
+++ b/src/scraper.ts
@@ -297,7 +297,12 @@ interface GitHubCommon {
};
}
-interface GitHubNav extends GitHubCommon {
+interface GitHubRepoPage extends GitHubCommon {
+ // Enabled features, like issues or wiki.
+ tabs: string[] | null;
+}
+
+interface GitHubNav extends GitHubRepoPage {
branch: string;
path: string;
}
@@ -347,12 +352,12 @@ interface Issue {
numPRs?: number;
}
-export interface GitHubIssues extends GitHubCommon {
+export interface GitHubIssues extends GitHubRepoPage {
issues: Issue[];
q?: string;
}
-export interface GitHubPulls extends GitHubCommon {
+export interface GitHubPulls extends GitHubRepoPage {
pulls: Issue[];
q?: string;
}
@@ -384,7 +389,7 @@ export type IssueTimelineItem =
| ({ kind: "comment" } & IssueComment)
| ({ kind: "event" } & IssueEvent);
-export interface GitHubIssue extends GitHubCommon {
+export interface GitHubIssue extends GitHubRepoPage {
number: number;
titleHtml: string;
state: string;
@@ -437,7 +442,7 @@ export interface DiscussionComment {
hiddenReplies: number;
}
-export interface GitHubDiscussions extends GitHubCommon {
+export interface GitHubDiscussions extends GitHubRepoPage {
discussions: DiscussionSummary[];
categories: DiscussionCategory[];
// The category the list is narrowed to, if any.
@@ -445,7 +450,7 @@ export interface GitHubDiscussions extends GitHubCommon {
q?: string;
}
-export interface GitHubDiscussion extends GitHubCommon {
+export interface GitHubDiscussion extends GitHubRepoPage {
number: number;
titleHtml: string;
author: string | null;
@@ -520,11 +525,11 @@ export interface ReleaseAsset {
publishedAt?: string;
}
-export interface GitHubReleases extends GitHubCommon {
+export interface GitHubReleases extends GitHubRepoPage {
releases: Release[];
}
-export interface GitHubRelease extends GitHubCommon {
+export interface GitHubRelease extends GitHubRepoPage {
release: Release;
}
@@ -534,7 +539,7 @@ export interface WikiPage {
updatedAt?: string;
}
-export interface GitHubWiki extends GitHubCommon {
+export interface GitHubWiki extends GitHubRepoPage {
title: string;
updatedBy: string | null;
updatedAt: string | null;
@@ -544,7 +549,7 @@ export interface GitHubWiki extends GitHubCommon {
pages: WikiPage[];
}
-export interface GitHubWikiPages extends GitHubCommon {
+export interface GitHubWikiPages extends GitHubRepoPage {
pages: WikiPage[];
}
@@ -657,7 +662,13 @@ function extractOverviewHtml(
return Object.keys(result).length > 0 ? result : undefined;
}
-function extractGitHub<T>(payload: AppPayload | null, extra: T): GitHubNav & T {
+function parseRepoTabs(html: string): string[] | null {
+ const nav = /<nav\b[^>]*\baria-label="Repository"[^>]*>([\s\S]*?)<\/nav>/.exec(html);
+ if (!nav) return null;
+ return Array.from(nav[1].matchAll(/\bid="([a-z-]+)-tab"/g), (match) => match[1]);
+}
+
+function extractGitHub<T>(html: string, payload: AppPayload | null, extra: T): GitHubNav & T {
const layoutRoute = payload?.codeViewLayoutRoute;
if (layoutRoute === undefined) {
throw new Error("Missing codeViewLayoutRoute");
@@ -668,6 +679,7 @@ function extractGitHub<T>(payload: AppPayload | null, extra: T): GitHubNav & T {
owner: layoutRoute.repo.ownerLogin,
name: layoutRoute.repo.name,
},
+ tabs: parseRepoTabs(html),
branch: layoutRoute.refInfo.name,
path: layoutRoute.path,
...extra,
@@ -695,7 +707,7 @@ export async function getGitHubRepo(owner: string, repo: string): Promise<GitHub
};
const overviewHtml = extractOverviewHtml(repoRoute.overview?.overviewFiles);
- return extractGitHub(payload, { items: repoRoute.tree.items, info, overviewHtml });
+ return extractGitHub(html, payload, { items: repoRoute.tree.items, info, overviewHtml });
}
export async function getGitHubTree(
@@ -715,7 +727,7 @@ export async function getGitHubTree(
const overviewHtml = extractOverviewHtml(treeRoute.tree.readme ? [treeRoute.tree.readme] : []);
- return extractGitHub(payload, { items: treeRoute.tree.items, overviewHtml });
+ return extractGitHub(html, payload, { items: treeRoute.tree.items, overviewHtml });
}
export async function getGitHubBlob(
@@ -748,7 +760,7 @@ export async function getGitHubBlob(
size += ` / ${blob.headerInfo.lineInfo.truncatedSloc} loc`;
}
- return extractGitHub(payload, {
+ return extractGitHub(html, payload, {
language: blob.language || null,
size,
image: blob.image,
@@ -833,7 +845,7 @@ export async function getGitHubIssues(
};
});
- return { repo: { owner, name: repo }, issues, q };
+ return { repo: { owner, name: repo }, tabs: parseRepoTabs(html), issues, q };
}
// GitHub's payloads carry absolute URLs; the same paths on our host are ours.
@@ -988,6 +1000,7 @@ export async function getGitHubIssue(
return {
repo: { owner, name: repo },
+ tabs: parseRepoTabs(html),
number: issue.number,
titleHtml: issue.titleHTML,
state: issue.state,
@@ -1051,7 +1064,7 @@ export async function getGitHubPulls(
pulls.push({ number, title, state, createdAt, labels });
}
- return { repo: { owner, name: repo }, pulls, q };
+ return { repo: { owner, name: repo }, tabs: parseRepoTabs(html), pulls, q };
}
// A category link names the category on its own in a discussion's header and
@@ -1128,6 +1141,7 @@ export async function getGitHubDiscussions(
return {
repo: { owner, name: repo },
+ tabs: parseRepoTabs(html),
discussions,
categories,
category: categories.find((c) => c.slug === category) ?? null,
@@ -1220,6 +1234,7 @@ export async function getGitHubDiscussion(
return {
repo: { owner, name: repo },
+ tabs: parseRepoTabs(html),
number,
titleHtml: title ? getInnerHTML(title).trim() : "",
author: meta ? selectText("a[href]", meta) : null,
@@ -1255,6 +1270,7 @@ export async function getGitHubCommits(
owner,
name: repo,
},
+ tabs: parseRepoTabs(html),
branch,
path,
commitGroups: commitsRoute.commitGroups,
@@ -1467,6 +1483,7 @@ export async function getGitHubReleases(owner: string, repo: string): Promise<Gi
owner,
name: repo,
},
+ tabs: parseRepoTabs(html),
releases,
};
}
@@ -1493,6 +1510,7 @@ export async function getGitHubReleaseBase(
owner,
name: repo,
},
+ tabs: parseRepoTabs(html),
release,
};
}
@@ -1763,6 +1781,7 @@ export async function getGitHubWiki(
return {
repo: { owner, name: repo },
+ tabs: parseRepoTabs(html),
title: directText(heading),
updatedBy,
updatedAt: time ? getAttributeValue(time, "datetime") || null : null,
@@ -1792,5 +1811,5 @@ export async function getGitHubWikiPages(owner: string, repo: string): Promise<G
pages.push({ name, href, updatedAt: time ? getAttributeValue(time, "datetime") : undefined });
}
- return { repo: { owner, name: repo }, pages };
+ return { repo: { owner, name: repo }, tabs: parseRepoTabs(html), pages };
}
diff --git a/views/layouts/repo.eta b/views/layouts/repo.eta
index 6381196..cab9324 100644
--- a/views/layouts/repo.eta
+++ b/views/layouts/repo.eta
@@ -11,13 +11,25 @@ layout("/layouts/base.eta", {
<code>git clone https://github.com/<%= it.repo.owner %>/<%= it.repo.name %>.git</code>
<code>git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git</code>
</div>
+ <%
+ /* Without GitHub's navigation to go by, offer every tab. */
+ const hasTab = (tab) => !it.tabs || it.tabs.includes(tab)
+ %>
<nav>
<a href="/<%= it.repo.owner %>/<%= it.repo.name %>">Code</a>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/issues">Issues</a>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/pulls">Pull requests</a>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/discussions">Discussions</a>
+ <% if (hasTab("issues")) { %>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/issues">Issues</a>
+ <% } %>
+ <% if (hasTab("pull-requests")) { %>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/pulls">Pull requests</a>
+ <% } %>
+ <% if (hasTab("discussions")) { %>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/discussions">Discussions</a>
+ <% } %>
<a href="/<%= it.repo.owner %>/<%= it.repo.name %>/releases">Releases</a>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/wiki">Wiki</a>
+ <% if (hasTab("wiki")) { %>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/wiki">Wiki</a>
+ <% } %>
<% if (it.branch) { %>
<a href="/<%= it.repo.owner %>/<%= it.repo.name %>/commits/<%= it.branch %><%= it.path && it.path !== '/' ? '/' + it.path : '' %>">Commits</a>
<% } %>