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