Skip to content
Merged
25 changes: 24 additions & 1 deletion src/lib/components/GHBadge.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
GitPullRequestArrow,
GitPullRequestClosed,
GitPullRequestDraft,
type Icon
type Icon,
MessageSquare,
MessageSquareX
} from "@lucide/svelte";

type CommonStatus = "open" | "closed";
Expand All @@ -20,6 +22,10 @@
| {
type: "issue";
status: CommonStatus | "solved";
}
| {
type: "discussion";
status: CommonStatus;
};

type Props = {
Expand Down Expand Up @@ -86,6 +92,23 @@
bgColor = "bg-purple-500";
break;
}
break;
case "discussion":
switch (status) {
case "open":
icon = MessageSquare;
label = "Open";
textColor = "text-green-600";
bgColor = "bg-green-600";
break;
case "closed":
icon = MessageSquareX;
label = "Closed";
textColor = "text-purple-500";
bgColor = "bg-purple-500";
break;
}
break;
}
</script>

Expand Down
141 changes: 138 additions & 3 deletions src/lib/server/github-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ export type Member = Awaited<

type OwnerKeyType = "members";

type RepoKeyType = "releases" | "descriptions" | "issue" | "issues" | "pr" | "prs";
type RepoKeyType =
| "releases"
| "descriptions"
| "issue"
| "issues"
| "pr"
| "prs"
| "discussion"
| "discussions";

export type ItemDetails = {
comments: Awaited<ReturnType<Issues["listComments"]>>["data"];
Expand All @@ -44,6 +52,61 @@ export type PullRequestDetails = ItemDetails & {
linkedIssues: LinkedItem[];
};

export type DiscussionDetails = {
info: Discussion;
comments: DiscussionComment[];
};

type TeamDiscussion = Awaited<
ReturnType<InstanceType<typeof Octokit>["rest"]["teams"]["listDiscussionsInOrg"]>
>["data"][number];
export type Discussion = {
repository_url: string;
category: {
id: number;
node_id: string;
repository_id: number;
emoji: `:${string}:`;
name: string;
description: string;
created_at: string;
updated_at: string;
slug: string;
is_answerable: boolean;
};
answer_html_url: string | null;
answer_chosen_at: string | null;
answer_chosen_by: TeamDiscussion["author"] | null;
id: number;
user: TeamDiscussion["author"];
labels: never[];
state: "open" | "closed";
state_reason: "resolved" | null;
locked: boolean;
comments: TeamDiscussion["comments_count"];
author_association: "MEMBER" | "CONTRIBUTOR" | "NONE";
active_lock_reason: null;
timeline_url: string;
} & Pick<
TeamDiscussion,
"html_url" | "node_id" | "number" | "title" | "created_at" | "updated_at" | "body" | "reactions"
>;
type TeamDiscussionComment = Awaited<
ReturnType<InstanceType<typeof Octokit>["rest"]["teams"]["listDiscussionCommentsInOrg"]>
>["data"][number];
export type DiscussionComment = {
id: number;
parent_id: number | null;
child_comment_count: number;
repository_url: `${string}/${string}`;
discussion_id: number;
author_association: "MEMBER" | "CONTRIBUTOR" | "NONE";
user: TeamDiscussion["author"];
} & Pick<
TeamDiscussionComment,
"node_id" | "html_url" | "created_at" | "updated_at" | "body" | "reactions"
>;

export type LinkedItem = {
number: number;
title: string;
Expand Down Expand Up @@ -169,6 +232,7 @@ export class GitHubCache {
* 2. calls the promise to get new data if no value is found in cache
* 3. store this new value back in the cache with an optional TTL before returning the value.
*
* @returns a currying promise than handles everything needed for requests
* @private
*/
#processCached<RType extends Parameters<InstanceType<typeof Redis>["json"]["set"]>[2]>() {
Expand Down Expand Up @@ -229,14 +293,16 @@ export class GitHubCache {
owner: string,
repo: string,
id: number,
type: ExtractStrict<RepoKeyType, "issue" | "pr"> | undefined = undefined
type: ExtractStrict<RepoKeyType, "issue" | "pr" | "discussions"> | undefined = undefined
) {
// Known type we assume the existence of
switch (type) {
case "issue":
return await this.getIssueDetails(owner, repo, id);
case "pr":
return await this.getPullRequestDetails(owner, repo, id);
case "discussions":
return await this.getDiscussionDetails(owner, repo, id);
}

// Unknown type, try to find or null otherwise
Expand All @@ -247,12 +313,18 @@ export class GitHubCache {
}

try {
// comes last because issues will also resolve for prs
// doesn't come first because issues will also resolve for prs
return await this.getIssueDetails(owner, repo, id);
} catch (err: unknown) {
console.error(`Error trying to get issue details for ${owner}/${repo}: ${err}`);
}

try {
return await this.getDiscussionDetails(owner, repo, id);
} catch (err: unknown) {
console.error(`Error trying to get discussion details for ${owner}/${repo}: ${err}`);
}

return null;
}

Expand Down Expand Up @@ -314,6 +386,43 @@ export class GitHubCache {
);
}

/**
* Get the discussion from the specified info.
*
* @param owner the GitHub repository owner
* @param repo the GitHub repository name
* @param id the discussion number
* @returns the matching discussion
* @throws Error if the discussion is not found
*/
async getDiscussionDetails(owner: string, repo: string, id: number) {
return await this.#processCached<DiscussionDetails>()(
this.#getRepoKey(owner, repo, "discussion", id),
() =>
Promise.all([
this.#octokit.request("GET /repos/{owner}/{repo}/discussions/{number}", {
owner,
repo,
number: id
}),
this.#octokit.paginate<DiscussionComment>(
"GET /repos/{owner}/{repo}/discussions/{number}/comments",
{
owner,
repo,
number: id,
per_page
}
)
]),
([{ data: discussion }, comments]) => ({
info: discussion,
comments
}),
FULL_DETAILS_TTL
);
}

/**
* Get the pull requests linked to the given issue number.
*
Expand Down Expand Up @@ -720,6 +829,32 @@ export class GitHubCache {
);
}

/**
* Get all the discussions for a given GitHub repository.
*
* @param owner the GitHub repository owner
* @param repo the GitHub repository name
* @returns a list of discussions, empty if not existing
*/
async getAllDiscussions(owner: string, repo: string) {
return await this.#processCached<Discussion[]>()(
this.#getRepoKey(owner, repo, "discussions"),
async () => {
try {
return await this.#octokit.paginate<Discussion>("GET /repos/{owner}/{repo}/discussions", {
owner,
repo,
per_page
});
} catch {
return [] as Discussion[];
}
},
discussions => discussions,
FULL_DETAILS_TTL
);
}

/**
* Get the deprecation state of a package from its name.
*
Expand Down
3 changes: 3 additions & 0 deletions src/params/pid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function match(param: string) {
return param === "pull" || param === "issues" || param === "discussions";
}
3 changes: 0 additions & 3 deletions src/params/poi.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@ import { error, redirect } from "@sveltejs/kit";
import { gitHubCache } from "$lib/server/github-cache";

export async function load({ params }) {
const { pullOrIssue: type, org, repo, id } = params;
const { pid: type, org, repo, id } = params;
const numId = +id; // id is already validated by the route matcher

const item = await gitHubCache.getItemDetails(org, repo, numId);
if (!item) {
error(404, `${type} #${id} doesn't exist in repo ${org}/${repo}`);
}

const realType = "commits" in item ? "pull" : "issues";
const realType = "commits" in item ? "pull" : "category" in item.info ? "discussions" : "issues";
if (type !== realType) {
redirect(303, `/${realType}/${org}/${repo}/${id}`);
redirect(307, `/${realType}/${org}/${repo}/${id}`);
}

return {
itemMetadata: {
org,
repo,
id: numId,
type: type === "issues" ? ("issue" as const) : ("pull" as const)
type:
type === "issues"
? ("issue" as const)
: type === "discussions"
? ("discussion" as const)
: ("pull" as const)
},
item
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
let commits = $derived("commits" in data.item ? data.item.commits : []);
let files = $derived("files" in data.item ? data.item.files : []);
let linkedEntities = $derived(
"linkedPrs" in data.item ? data.item.linkedPrs : data.item.linkedIssues
"linkedPrs" in data.item
? data.item.linkedPrs
: "linkedIssues" in data.item
? data.item.linkedIssues
: []
);
</script>

Expand Down
Loading
Loading