1import { Octokit, RestEndpointMethodTypes } from '@octokit/rest'; 2import parseDiff from 'parse-diff'; 3import path from 'path'; 4 5import { EXPO_DIR } from './Constants'; 6import { GitFileDiff } from './Git'; 7 8const octokit = new Octokit({ 9 auth: process.env.GITHUB_TOKEN, 10}); 11 12// Predefine some params used across almost all requests. 13const owner = 'expo'; 14const repo = 'expo'; 15 16/** 17 * Returns public informations about the currently authenticated (by GitHub API token) user. 18 */ 19export async function getAuthenticatedUserAsync() { 20 const { data } = await octokit.users.getAuthenticated(); 21 return data; 22} 23 24/** 25 * Requests for the pull request object. 26 */ 27export async function getPullRequestAsync(pull_number: number): Promise<PullRequest> { 28 const { data } = await octokit.pulls.get({ 29 owner, 30 repo, 31 pull_number, 32 }); 33 return data; 34} 35 36/** 37 * Requests and parses the diff of the pull request with given number. 38 */ 39export async function getPullRequestDiffAsync( 40 pull_number: number, 41 base_path: string = EXPO_DIR 42): Promise<GitFileDiff[]> { 43 const { data } = await octokit.pulls.get({ 44 owner, 45 repo, 46 pull_number, 47 headers: { 48 accept: 'application/vnd.github.v3.diff', 49 }, 50 }); 51 52 // When the custom accept header is provided the returned data 53 // doesn't match declared type (it's a string). 54 const diff = parseDiff(data as unknown as string); 55 56 return diff.map((entry) => { 57 return { 58 ...entry, 59 path: path.join(base_path, (entry.deleted ? entry.from : entry.to)!), 60 }; 61 }); 62} 63 64/** 65 * Gets a list of reviews left in the pull request with given ID. 66 */ 67export async function listPullRequestReviewsAsync( 68 pull_number: number 69): Promise<PullRequestReview[]> { 70 const { data } = await octokit.pulls.listReviews({ 71 owner, 72 repo, 73 pull_number, 74 }); 75 return data; 76} 77 78/** 79 * Creates pull request review. By default the review is pending which needs to be submitted in order to be visible for other users. 80 * Provide `event` option to create and submit at once. 81 */ 82export async function createPullRequestReviewAsync<T>( 83 pull_number: number, 84 options?: T 85): Promise<PullRequestReview> { 86 const { data } = await octokit.pulls.createReview({ 87 owner, 88 repo, 89 pull_number, 90 ...options, 91 }); 92 return data; 93} 94 95/** 96 * Updates pull request review with a new main comment. 97 */ 98export async function updatePullRequestReviewAsync( 99 pull_number: number, 100 review_id: number, 101 body: string 102) { 103 const { data } = await octokit.pulls.updateReview({ 104 owner, 105 repo, 106 pull_number, 107 review_id, 108 body, 109 }); 110 return data; 111} 112 113/** 114 * Gets a list of comments in review. 115 */ 116export async function listPullRequestReviewCommentsAsync(pull_number: number, review_id: number) { 117 const { data } = await octokit.pulls.listReviewComments({ 118 owner, 119 repo, 120 pull_number, 121 review_id, 122 }); 123 return data; 124} 125 126/** 127 * Deletes a comment left under pull request review. 128 */ 129export async function deletePullRequestReviewCommentAsync(comment_id: number) { 130 const { data } = await octokit.pulls.deleteReviewComment({ 131 owner, 132 repo, 133 comment_id, 134 }); 135 return data; 136} 137 138/** 139 * Deletes all comments from given review. 140 */ 141export async function deleteAllPullRequestReviewCommentsAsync( 142 pull_number: number, 143 review_id: number 144) { 145 const comments = await listPullRequestReviewCommentsAsync(pull_number, review_id); 146 147 await Promise.all( 148 comments 149 .filter((comment) => comment.pull_request_review_id === review_id) 150 .map((comment) => deletePullRequestReviewCommentAsync(comment.id)) 151 ); 152} 153 154/** 155 * Requests given users to review the pull request. 156 * If the user already reviewed the PR, it resets his review state. 157 */ 158export async function requestPullRequestReviewersAsync(pull_number: number, reviewers: string[]) { 159 const { data } = await octokit.pulls.requestReviewers({ 160 owner, 161 repo, 162 pull_number, 163 reviewers, 164 }); 165 return data; 166} 167 168/** 169 * Returns an issue object with given issue number. 170 */ 171export async function getIssueAsync(issue_number: number) { 172 const { data } = await octokit.issues.get({ 173 owner, 174 repo, 175 issue_number, 176 }); 177 return data; 178} 179 180/** 181 * Creates an issue comment with given body. 182 */ 183export async function createCommentAsync(issue_number: number, body: string) { 184 const { data } = await octokit.issues.createComment({ 185 owner, 186 repo, 187 issue_number, 188 body, 189 }); 190 return data; 191} 192 193/** 194 * Lists commits in given issue. 195 */ 196export async function listCommentsAsync( 197 issue_number: number, 198 options: Partial<ListCommentsOptions> 199) { 200 const { data } = await octokit.issues.listComments({ 201 owner, 202 repo, 203 issue_number, 204 ...options, 205 }); 206 return data; 207} 208 209/** 210 * Returns a list of issue comments gathered from all pages. 211 */ 212export async function listAllCommentsAsync(issue_number: number) { 213 const issue = await getIssueAsync(issue_number); 214 const comments = [] as ListCommentsResponse['data']; 215 const pageSize = 100; 216 217 for (let page = 1, maxPage = Math.ceil(issue.comments / pageSize); page <= maxPage; page++) { 218 const commentsPage = await listCommentsAsync(issue_number, { 219 page, 220 per_page: pageSize, 221 }); 222 comments.push(...commentsPage); 223 } 224 return comments; 225} 226 227/** 228 * Deletes an issue comment with given identifier. 229 */ 230export async function deleteCommentAsync(comment_id: number) { 231 const { data } = await octokit.issues.deleteComment({ 232 owner, 233 repo, 234 comment_id, 235 }); 236 return data; 237} 238 239/** 240 * Adds labels to the issue. Throws an error when any of given labels doesn't exist. 241 */ 242export async function addIssueLabelsAsync(issue_number: number, labels: string[]) { 243 const { data } = await octokit.issues.addLabels({ 244 owner, 245 repo, 246 issue_number, 247 labels, 248 }); 249 return data; 250} 251 252/** 253 * Removes single label from the issue. 254 * Throws an error when given label doesn't exist and when the label isn't added. 255 */ 256export async function removeIssueLabelAsync(issue_number: number, name: string) { 257 const { data } = await octokit.issues.removeLabel({ 258 owner, 259 repo, 260 issue_number, 261 name, 262 }); 263 return data; 264} 265 266// Octokit's types are autogenerated and so inconvenient to use if you want to refer to them. 267// We re-export some of them to make it easier. 268export type PullRequestReviewEvent = 'COMMENT' | 'APPROVE' | 'REQUEST_CHANGES'; 269export type PullRequest = RestEndpointMethodTypes['pulls']['get']['response']['data']; 270export type PullRequestReview = RestEndpointMethodTypes['pulls']['getReview']['response']['data']; 271export type IssueComment = RestEndpointMethodTypes['issues']['getComment']['response']['data']; 272export type ListCommentsOptions = RestEndpointMethodTypes['issues']['listComments']['parameters']; 273export type ListCommentsResponse = RestEndpointMethodTypes['issues']['listComments']['response']; 274