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