Skip to content

Commit 3c2f4c1

Browse files
committed
Add keybinds
1 parent 506bd26 commit 3c2f4c1

File tree

10 files changed

+2026
-146
lines changed

10 files changed

+2026
-146
lines changed

src/api/api.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import {
55
getPullRequestComments,
66
createReviewComment,
77
replyToComment,
8+
getReviews,
9+
createReview,
10+
getCheckRuns,
11+
getCombinedStatus,
12+
mergePullRequest,
13+
getIssueComments,
14+
createIssueComment,
815
} from "./github";
916
import { parseDiffWithHighlighting } from "./diff";
1017

@@ -105,6 +112,13 @@ api.post("/parse-diff", async (c) => {
105112
try {
106113
// Use SHA as cache key for immutable caching
107114
const parsed = parseDiffWithHighlighting(patch, filename, previousFilename, sha);
115+
116+
// Set cache headers for immutable content (SHA-based)
117+
if (sha) {
118+
c.header("Cache-Control", "public, max-age=31536000, immutable");
119+
c.header("ETag", `"${sha}"`);
120+
}
121+
108122
return c.json(parsed);
109123
} catch (error) {
110124
return c.json(
@@ -114,4 +128,131 @@ api.post("/parse-diff", async (c) => {
114128
}
115129
});
116130

131+
// ============================================================================
132+
// Review APIs
133+
// ============================================================================
134+
135+
// Get PR reviews
136+
api.get("/pr/:owner/:repo/:number/reviews", async (c) => {
137+
const { owner, repo, number } = c.req.param();
138+
try {
139+
const reviews = await getReviews(owner, repo, parseInt(number, 10));
140+
return c.json(reviews);
141+
} catch (error) {
142+
return c.json(
143+
{ error: error instanceof Error ? error.message : "Failed to fetch reviews" },
144+
500
145+
);
146+
}
147+
});
148+
149+
// Submit a review
150+
api.post("/pr/:owner/:repo/:number/reviews", async (c) => {
151+
const { owner, repo, number } = c.req.param();
152+
const body = await c.req.json();
153+
154+
try {
155+
const review = await createReview(
156+
owner,
157+
repo,
158+
parseInt(number, 10),
159+
body.commit_id,
160+
body.event,
161+
body.body || "",
162+
body.comments || []
163+
);
164+
return c.json(review);
165+
} catch (error) {
166+
return c.json(
167+
{ error: error instanceof Error ? error.message : "Failed to submit review" },
168+
500
169+
);
170+
}
171+
});
172+
173+
// ============================================================================
174+
// Checks & Status
175+
// ============================================================================
176+
177+
// Get check runs
178+
api.get("/pr/:owner/:repo/:number/checks", async (c) => {
179+
const { owner, repo, number } = c.req.param();
180+
try {
181+
const pr = await getPullRequest(owner, repo, parseInt(number, 10));
182+
const [checkRuns, status] = await Promise.all([
183+
getCheckRuns(owner, repo, pr.head.sha),
184+
getCombinedStatus(owner, repo, pr.head.sha),
185+
]);
186+
return c.json({ checkRuns: checkRuns.check_runs, status });
187+
} catch (error) {
188+
return c.json(
189+
{ error: error instanceof Error ? error.message : "Failed to fetch checks" },
190+
500
191+
);
192+
}
193+
});
194+
195+
// ============================================================================
196+
// Merge
197+
// ============================================================================
198+
199+
api.post("/pr/:owner/:repo/:number/merge", async (c) => {
200+
const { owner, repo, number } = c.req.param();
201+
const body = await c.req.json();
202+
203+
try {
204+
const result = await mergePullRequest(
205+
owner,
206+
repo,
207+
parseInt(number, 10),
208+
body.merge_method || "squash",
209+
body.commit_title,
210+
body.commit_message
211+
);
212+
return c.json(result);
213+
} catch (error) {
214+
return c.json(
215+
{ error: error instanceof Error ? error.message : "Failed to merge PR" },
216+
500
217+
);
218+
}
219+
});
220+
221+
// ============================================================================
222+
// Issue Comments (PR conversation)
223+
// ============================================================================
224+
225+
api.get("/pr/:owner/:repo/:number/conversation", async (c) => {
226+
const { owner, repo, number } = c.req.param();
227+
try {
228+
const comments = await getIssueComments(owner, repo, parseInt(number, 10));
229+
return c.json(comments);
230+
} catch (error) {
231+
return c.json(
232+
{ error: error instanceof Error ? error.message : "Failed to fetch conversation" },
233+
500
234+
);
235+
}
236+
});
237+
238+
api.post("/pr/:owner/:repo/:number/conversation", async (c) => {
239+
const { owner, repo, number } = c.req.param();
240+
const body = await c.req.json();
241+
242+
try {
243+
const comment = await createIssueComment(
244+
owner,
245+
repo,
246+
parseInt(number, 10),
247+
body.body
248+
);
249+
return c.json(comment);
250+
} catch (error) {
251+
return c.json(
252+
{ error: error instanceof Error ? error.message : "Failed to add comment" },
253+
500
254+
);
255+
}
256+
});
257+
117258
export default api;

src/api/github.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface PullRequest {
2020
title: string;
2121
body: string | null;
2222
state: string;
23+
html_url: string;
2324
user: { login: string; avatar_url: string };
2425
head: { ref: string; sha: string };
2526
base: { ref: string; sha: string };
@@ -29,7 +30,50 @@ export interface PullRequest {
2930
deletions: number;
3031
changed_files: number;
3132
mergeable: boolean | null;
33+
mergeable_state: string;
34+
merged: boolean;
35+
merge_commit_sha: string | null;
3236
draft: boolean;
37+
labels: Array<{ name: string; color: string }>;
38+
requested_reviewers: Array<{ login: string; avatar_url: string }>;
39+
assignees: Array<{ login: string; avatar_url: string }>;
40+
}
41+
42+
export interface CheckRun {
43+
id: number;
44+
name: string;
45+
status: "queued" | "in_progress" | "completed";
46+
conclusion: "success" | "failure" | "neutral" | "cancelled" | "skipped" | "timed_out" | "action_required" | null;
47+
html_url: string;
48+
started_at: string;
49+
completed_at: string | null;
50+
}
51+
52+
export interface CombinedStatus {
53+
state: "success" | "failure" | "pending";
54+
statuses: Array<{
55+
state: "success" | "failure" | "pending" | "error";
56+
context: string;
57+
description: string | null;
58+
target_url: string | null;
59+
}>;
60+
}
61+
62+
export interface Review {
63+
id: number;
64+
user: { login: string; avatar_url: string };
65+
body: string;
66+
state: "APPROVED" | "CHANGES_REQUESTED" | "COMMENTED" | "PENDING" | "DISMISSED";
67+
submitted_at: string;
68+
}
69+
70+
export interface PendingReviewComment {
71+
path: string;
72+
line: number;
73+
start_line?: number;
74+
body: string;
75+
side: "LEFT" | "RIGHT";
76+
start_side?: "LEFT" | "RIGHT";
3377
}
3478

3579
export interface PullRequestFile {
@@ -182,3 +226,126 @@ export async function replyToComment(
182226
);
183227
}
184228

229+
// ============================================================================
230+
// Review APIs
231+
// ============================================================================
232+
233+
export async function getReviews(
234+
owner: string,
235+
repo: string,
236+
number: number
237+
): Promise<Review[]> {
238+
return githubFetch(
239+
`https://api.github.com/repos/${owner}/${repo}/pulls/${number}/reviews`
240+
);
241+
}
242+
243+
export async function createReview(
244+
owner: string,
245+
repo: string,
246+
number: number,
247+
commitId: string,
248+
event: "APPROVE" | "REQUEST_CHANGES" | "COMMENT",
249+
body: string,
250+
comments: PendingReviewComment[]
251+
): Promise<Review> {
252+
return githubFetch(
253+
`https://api.github.com/repos/${owner}/${repo}/pulls/${number}/reviews`,
254+
{
255+
method: "POST",
256+
body: JSON.stringify({
257+
commit_id: commitId,
258+
event,
259+
body,
260+
comments,
261+
}),
262+
}
263+
);
264+
}
265+
266+
// ============================================================================
267+
// Check Runs & Status
268+
// ============================================================================
269+
270+
export async function getCheckRuns(
271+
owner: string,
272+
repo: string,
273+
ref: string
274+
): Promise<{ check_runs: CheckRun[] }> {
275+
return githubFetch(
276+
`https://api.github.com/repos/${owner}/${repo}/commits/${ref}/check-runs`
277+
);
278+
}
279+
280+
export async function getCombinedStatus(
281+
owner: string,
282+
repo: string,
283+
ref: string
284+
): Promise<CombinedStatus> {
285+
return githubFetch(
286+
`https://api.github.com/repos/${owner}/${repo}/commits/${ref}/status`
287+
);
288+
}
289+
290+
// ============================================================================
291+
// Merge Operations
292+
// ============================================================================
293+
294+
export async function mergePullRequest(
295+
owner: string,
296+
repo: string,
297+
number: number,
298+
mergeMethod: "merge" | "squash" | "rebase" = "squash",
299+
commitTitle?: string,
300+
commitMessage?: string
301+
): Promise<{ sha: string; merged: boolean; message: string }> {
302+
return githubFetch(
303+
`https://api.github.com/repos/${owner}/${repo}/pulls/${number}/merge`,
304+
{
305+
method: "PUT",
306+
body: JSON.stringify({
307+
merge_method: mergeMethod,
308+
commit_title: commitTitle,
309+
commit_message: commitMessage,
310+
}),
311+
}
312+
);
313+
}
314+
315+
// ============================================================================
316+
// Issue Comments (for PR body/conversation)
317+
// ============================================================================
318+
319+
export interface IssueComment {
320+
id: number;
321+
body: string;
322+
user: { login: string; avatar_url: string };
323+
created_at: string;
324+
updated_at: string;
325+
}
326+
327+
export async function getIssueComments(
328+
owner: string,
329+
repo: string,
330+
number: number
331+
): Promise<IssueComment[]> {
332+
return githubFetch(
333+
`https://api.github.com/repos/${owner}/${repo}/issues/${number}/comments`
334+
);
335+
}
336+
337+
export async function createIssueComment(
338+
owner: string,
339+
repo: string,
340+
number: number,
341+
body: string
342+
): Promise<IssueComment> {
343+
return githubFetch(
344+
`https://api.github.com/repos/${owner}/${repo}/issues/${number}/comments`,
345+
{
346+
method: "POST",
347+
body: JSON.stringify({ body }),
348+
}
349+
);
350+
}
351+

0 commit comments

Comments
 (0)