UNPKG

code-suggester

Version:
226 lines 10.1 kB
"use strict"; // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. Object.defineProperty(exports, "__esModule", { value: true }); exports.getPullRequestHunks = exports.getCurrentPullRequestPatches = exports.createPullRequestReview = exports.makeInlineSuggestions = exports.buildReviewComments = exports.buildSummaryComment = void 0; const logger_1 = require("../logger"); const diff_utils_1 = require("../utils/diff-utils"); const hunk_utils_1 = require("../utils/hunk-utils"); function hunkErrorMessage(hunk) { return ` * lines ${hunk.oldStart}-${hunk.oldEnd}`; } function fileErrorMessage(filename, hunks) { return `* ${filename}\n` + hunks.map(hunkErrorMessage).join('\n'); } /** * Build an error message based on invalid hunks. * Returns an empty string if the provided hunks are empty. * @param invalidHunks a map of filename to hunks that are not suggestable */ function buildSummaryComment(invalidHunks) { if (invalidHunks.size === 0) { return ''; } return ('Some suggestions could not be made:\n' + Array.from(invalidHunks, ([filename, hunks]) => fileErrorMessage(filename, hunks)).join('\n')); } exports.buildSummaryComment = buildSummaryComment; const COMFORT_PREVIEW_HEADER = 'application/vnd.github.comfort-fade-preview+json'; /** * Convert the patch suggestions into GitHub parameter objects. * Use this to generate review comments * For information see: * https://developer.github.com/v3/pulls/comments/#create-a-review-comment-for-a-pull-request * @param suggestions */ function buildReviewComments(suggestions) { const fileComments = []; suggestions.forEach((hunks, fileName) => { hunks.forEach(hunk => { const newContent = hunk.newContent.join('\n'); if (hunk.oldStart === hunk.oldEnd) { const singleComment = { path: fileName, body: `\`\`\`suggestion\n${newContent}\n\`\`\``, line: hunk.oldEnd, side: 'RIGHT', }; fileComments.push(singleComment); } else { const comment = { path: fileName, body: `\`\`\`suggestion\n${newContent}\n\`\`\``, start_line: hunk.oldStart, line: hunk.oldEnd, side: 'RIGHT', start_side: 'RIGHT', }; fileComments.push(comment); } }); }); return fileComments; } exports.buildReviewComments = buildReviewComments; /** * Make a request to GitHub to make review comments * @param octokit an authenticated octokit instance * @param suggestions code suggestions patches * @param remote the repository domain * @param pullNumber the pull request number to make a review on */ async function makeInlineSuggestions(octokit, suggestions, outOfScopeSuggestions, remote, pullNumber) { const comments = buildReviewComments(suggestions); if (!comments.length) { logger_1.logger.info('No valid suggestions to make'); } if (!comments.length && !outOfScopeSuggestions.size) { logger_1.logger.info('No suggestions were generated. Exiting...'); return null; } const summaryComment = buildSummaryComment(outOfScopeSuggestions); if (summaryComment) { logger_1.logger.warn('Some suggestions could not be made'); } // apply the suggestions to the latest sha // the latest Pull Request hunk range includes // all previous commit valid hunk ranges const headSha = (await octokit.pulls.get({ owner: remote.owner, repo: remote.repo, pull_number: pullNumber, })).data.head.sha; const reviewNumber = (await octokit.pulls.createReview({ owner: remote.owner, repo: remote.repo, pull_number: pullNumber, commit_id: headSha, event: 'COMMENT', body: summaryComment, headers: { accept: COMFORT_PREVIEW_HEADER }, // Octokit type definitions doesn't support mulitiline comments, but the GitHub API does comments: comments, })).data.id; logger_1.logger.info(`Successfully created a review on pull request: ${pullNumber}.`); return reviewNumber; } exports.makeInlineSuggestions = makeInlineSuggestions; /** * Comment on a Pull Request * @param {Octokit} octokit authenticated octokit isntance * @param {RepoDomain} remote the Pull Request repository * @param {number} pullNumber the Pull Request number * @param {number} pageSize the number of files to comment on // TODO pagination * @param {Map<string, FileDiffContent>} diffContents the old and new contents of the files to suggest * @returns the created review's id, or null if no review was made */ async function createPullRequestReview(octokit, remote, pullNumber, pageSize, diffContents) { try { // get the hunks from the pull request const pullRequestHunks = await exports.getPullRequestHunks(octokit, remote, pullNumber, pageSize); // get the hunks from the suggested change const allSuggestedHunks = typeof diffContents === 'string' ? (0, diff_utils_1.parseAllHunks)(diffContents) : (0, hunk_utils_1.getRawSuggestionHunks)(diffContents); // split hunks by commentable and uncommentable const { validHunks, invalidHunks } = (0, hunk_utils_1.partitionSuggestedHunksByScope)(pullRequestHunks, allSuggestedHunks); // create pull request review const reviewNumber = await exports.makeInlineSuggestions(octokit, validHunks, invalidHunks, remote, pullNumber); return reviewNumber; } catch (err) { logger_1.logger.error('Failed to suggest'); throw err; } } exports.createPullRequestReview = createPullRequestReview; /** * For a pull request, get each remote file's patch text asynchronously * Also get the list of files whose patch data could not be returned * @param {Octokit} octokit the authenticated octokit instance * @param {RepoDomain} remote the remote repository domain information * @param {number} pullNumber the pull request number * @param {number} pageSize the number of results to return per page * @returns {Promise<Object<PatchText, string[]>>} the stringified patch data for each file and the list of files whose patch data could not be resolved */ async function getCurrentPullRequestPatches(octokit, remote, pullNumber, pageSize) { // TODO: support pagination const filesMissingPatch = []; const files = (await octokit.pulls.listFiles({ owner: remote.owner, repo: remote.repo, pull_number: pullNumber, per_page: pageSize, })).data; const patches = new Map(); if (files.length === 0) { logger_1.logger.error(`0 file results have returned from list files query for Pull Request #${pullNumber}. Cannot make suggestions on an empty Pull Request`); throw Error('Empty Pull Request'); } files.forEach(file => { if (file.patch === undefined) { // files whose patch is too large do not return the patch text by default // TODO handle file patches that are too large logger_1.logger.warn(`File ${file.filename} may have a patch that is too large to display patch object.`); filesMissingPatch.push(file.filename); } else { patches.set(file.filename, file.patch); } }); if (patches.size === 0) { logger_1.logger.warn('0 patches have been returned. This could be because the patch results were too large to return.'); } return { patches, filesMissingPatch }; } exports.getCurrentPullRequestPatches = getCurrentPullRequestPatches; /** * For a pull request, get each remote file's current patch range to identify the scope of each patch as a Map. * @param {Octokit} octokit the authenticated octokit instance * @param {RepoDomain} remote the remote repository domain information * @param {number} pullNumber the pull request number * @param {number} pageSize the number of files to return per pull request list files query * @returns {Promise<Map<string, Hunk[]>>} the scope of each file in the pull request */ async function getPullRequestHunks(octokit, remote, pullNumber, pageSize) { const files = (await octokit.pulls.listFiles({ owner: remote.owner, repo: remote.repo, pull_number: pullNumber, per_page: pageSize, })).data; const pullRequestHunks = new Map(); if (files.length === 0) { logger_1.logger.error(`0 file results have returned from list files query for Pull Request #${pullNumber}. Cannot make suggestions on an empty Pull Request`); throw Error('Empty Pull Request'); } files.forEach(file => { if (file.patch === undefined) { // files whose patch is too large do not return the patch text by default // TODO handle file patches that are too large logger_1.logger.warn(`File ${file.filename} may have a patch that is too large to display patch object.`); } else { const hunks = (0, diff_utils_1.parsePatch)(file.patch); pullRequestHunks.set(file.filename, hunks); } }); if (pullRequestHunks.size === 0) { logger_1.logger.warn('0 patches have been returned. This could be because the patch results were too large to return.'); } return pullRequestHunks; } exports.getPullRequestHunks = getPullRequestHunks; //# sourceMappingURL=review-pull-request.js.map