Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"clsx": "^2.1.1",
"eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-svelte": "^3.5.1",
"eslint-plugin-svelte": "^3.7.0",
"feed": "^5.0.0",
"globals": "^16.0.0",
"marked": "^15.0.11",
Expand All @@ -49,14 +49,14 @@
"posthog-js": "^1.240.0",
"posthog-node": "^4.17.1",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"rehype-raw": "^7.0.0",
"runed": "^0.26.0",
"semver": "^7.7.1",
"shiki": "^3.4.0",
"svelte": "^5.28.2",
"svelte-check": "^4.1.7",
"svelte": "^5.30.1",
"svelte-check": "^4.2.1",
"svelte-exmarkdown": "^5.0.1",
"svelte-meta-tags": "^4.2.0",
"svelte-sonner": "https://pkg.pr.new/wobsoriano/svelte-sonner@126",
Expand All @@ -78,5 +78,5 @@
]
},
"type": "module",
"packageManager": "pnpm@10.10.0"
"packageManager": "pnpm@10.11.0"
}
194 changes: 97 additions & 97 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

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
150 changes: 147 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,70 @@ export type PullRequestDetails = ItemDetails & {
linkedIssues: LinkedItem[];
};

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

type AuthorAssociation =
| "OWNER"
| "MEMBER"
| "COLLABORATOR"
| "CONTRIBUTOR"
| "FIRST_TIMER"
| "FIRST_TIME_CONTRIBUTOR"
| "MANNEQUIN"
| "NONE";
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: AuthorAssociation;
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: AuthorAssociation;
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 +241,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 +302,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 +322,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 +395,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 +838,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