Skip to content

Commit 83a0ae2

Browse files
authored
chore: add gh comments api (#229)
1 parent f4dab75 commit 83a0ae2

File tree

1 file changed

+182
-0
lines changed
  • apps/array/src/main/services

1 file changed

+182
-0
lines changed

apps/array/src/main/services/git.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,45 @@ export const parseGitHubUrl = (url: string): GitHubRepo | null => {
9090
};
9191
};
9292

93+
const getRepositoryFromRemoteUrl = async (
94+
directoryPath: string,
95+
): Promise<string> => {
96+
const remoteUrl = await getRemoteUrl(directoryPath);
97+
if (!remoteUrl) {
98+
throw new Error("No remote URL found");
99+
}
100+
101+
// Parse repo from URL (handles both HTTPS and SSH formats)
102+
const repoMatch = remoteUrl.match(
103+
/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/,
104+
);
105+
if (!repoMatch) {
106+
throw new Error(`Cannot parse repository from URL: ${remoteUrl}`);
107+
}
108+
109+
return repoMatch[1];
110+
};
111+
112+
const validatePullRequestNumber = (prNumber: number): void => {
113+
if (
114+
typeof prNumber !== "number" ||
115+
!Number.isInteger(prNumber) ||
116+
prNumber < 1
117+
) {
118+
throw new Error(`Invalid pull request number: ${prNumber}`);
119+
}
120+
};
121+
122+
const validateCommentId = (commentId: number): void => {
123+
if (
124+
typeof commentId !== "number" ||
125+
!Number.isInteger(commentId) ||
126+
commentId < 1
127+
) {
128+
throw new Error(`Invalid comment ID: ${commentId}`);
129+
}
130+
};
131+
93132
export const isGitRepository = async (
94133
directoryPath: string,
95134
): Promise<boolean> => {
@@ -517,6 +556,104 @@ export const detectSSHError = (output: string): string | undefined => {
517556
return `SSH test failed: ${output.substring(0, 200)}`;
518557
};
519558

559+
const getAllPullRequestComments = async (
560+
directoryPath: string,
561+
prNumber: number,
562+
): Promise<any> => {
563+
validatePullRequestNumber(prNumber);
564+
565+
try {
566+
const { stdout } = await execAsync(
567+
`gh pr view ${prNumber} --json comments`,
568+
{ cwd: directoryPath },
569+
);
570+
return JSON.parse(stdout);
571+
} catch (error) {
572+
throw new Error(`Failed to fetch PR comments: ${error}`);
573+
}
574+
};
575+
576+
const getPullRequestReviewComments = async (
577+
directoryPath: string,
578+
prNumber: number,
579+
): Promise<any> => {
580+
validatePullRequestNumber(prNumber);
581+
582+
try {
583+
const repo = await getRepositoryFromRemoteUrl(directoryPath);
584+
585+
// TODO: Paginate if many comments
586+
const { stdout } = await execAsync(
587+
`gh api repos/${repo}/pulls/${prNumber}/comments`,
588+
{ cwd: directoryPath },
589+
);
590+
return JSON.parse(stdout);
591+
} catch (error) {
592+
throw new Error(`Failed to fetch PR review comments: ${error}`);
593+
}
594+
};
595+
596+
interface AddPullRequestCommentOptions {
597+
body: string;
598+
commitId: string;
599+
path: string;
600+
line: number;
601+
side?: "LEFT" | "RIGHT";
602+
}
603+
604+
const addPullRequestComment = async (
605+
directoryPath: string,
606+
prNumber: number,
607+
options: AddPullRequestCommentOptions,
608+
): Promise<any> => {
609+
validatePullRequestNumber(prNumber);
610+
611+
// Validate required options
612+
if (!options.body || !options.commitId || !options.path) {
613+
throw new Error("body, commitId, and path are required");
614+
}
615+
616+
if (typeof options.line !== "number" || options.line < 1) {
617+
throw new Error("line must be a positive number");
618+
}
619+
620+
try {
621+
const repo = await getRepositoryFromRemoteUrl(directoryPath);
622+
const side = options.side || "RIGHT";
623+
624+
const { stdout } = await execAsync(
625+
`gh api repos/${repo}/pulls/${prNumber}/comments ` +
626+
`-f body="${options.body.replace(/"/g, '\\"')}" ` +
627+
`-f commit_id="${options.commitId}" ` +
628+
`-f path="${options.path}" ` +
629+
`-f line=${options.line} ` +
630+
`-f side="${side}"`,
631+
{ cwd: directoryPath },
632+
);
633+
return JSON.parse(stdout);
634+
} catch (error) {
635+
throw new Error(`Failed to add PR comment: ${error}`);
636+
}
637+
};
638+
639+
const deletePullRequestComment = async (
640+
directoryPath: string,
641+
commentId: number,
642+
): Promise<void> => {
643+
validateCommentId(commentId);
644+
645+
try {
646+
const repo = await getRepositoryFromRemoteUrl(directoryPath);
647+
648+
await execAsync(
649+
`gh api repos/${repo}/pulls/comments/${commentId} -X DELETE`,
650+
{ cwd: directoryPath },
651+
);
652+
} catch (error) {
653+
throw new Error(`Failed to delete PR comment: ${error}`);
654+
}
655+
};
656+
520657
export function registerGitIpc(
521658
getMainWindow: () => BrowserWindow | null,
522659
): void {
@@ -796,4 +933,49 @@ export function registerGitIpc(
796933
return discardFileChanges(directoryPath, filePath, fileStatus);
797934
},
798935
);
936+
937+
ipcMain.handle(
938+
"get-pr-comments",
939+
async (
940+
_event: IpcMainInvokeEvent,
941+
directoryPath: string,
942+
prNumber: number,
943+
): Promise<any> => {
944+
return getAllPullRequestComments(directoryPath, prNumber);
945+
},
946+
);
947+
948+
ipcMain.handle(
949+
"get-pr-review-comments",
950+
async (
951+
_event: IpcMainInvokeEvent,
952+
directoryPath: string,
953+
prNumber: number,
954+
): Promise<any> => {
955+
return getPullRequestReviewComments(directoryPath, prNumber);
956+
},
957+
);
958+
959+
ipcMain.handle(
960+
"add-pr-comment",
961+
async (
962+
_event: IpcMainInvokeEvent,
963+
directoryPath: string,
964+
prNumber: number,
965+
options: AddPullRequestCommentOptions,
966+
): Promise<any> => {
967+
return addPullRequestComment(directoryPath, prNumber, options);
968+
},
969+
);
970+
971+
ipcMain.handle(
972+
"delete-pr-comment",
973+
async (
974+
_event: IpcMainInvokeEvent,
975+
directoryPath: string,
976+
commentId: number,
977+
): Promise<void> => {
978+
return deletePullRequestComment(directoryPath, commentId);
979+
},
980+
);
799981
}

0 commit comments

Comments
 (0)