code-suggester
Version:
Library to propose code changes
181 lines • 9.7 kB
JavaScript
;
// 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.parseTextFiles = exports.createPullRequest = exports.reviewPullRequest = exports.CommitError = exports.getDiffString = exports.getChanges = void 0;
const types_1 = require("./types");
const logger_1 = require("./logger");
const default_options_handler_1 = require("./default-options-handler");
const retry = require("async-retry");
const review_pull_request_1 = require("./github/review-pull-request");
const branch_1 = require("./github/branch");
const fork_1 = require("./github/fork");
const commit_and_push_1 = require("./github/commit-and-push");
const open_pull_request_1 = require("./github/open-pull-request");
const labels_1 = require("./github/labels");
var handle_git_dir_change_1 = require("./bin/handle-git-dir-change");
Object.defineProperty(exports, "getChanges", { enumerable: true, get: function () { return handle_git_dir_change_1.getChanges; } });
Object.defineProperty(exports, "getDiffString", { enumerable: true, get: function () { return handle_git_dir_change_1.getDiffString; } });
var errors_1 = require("./errors");
Object.defineProperty(exports, "CommitError", { enumerable: true, get: function () { return errors_1.CommitError; } });
/**
* Given a set of suggestions, make all the multiline inline review comments on a given pull request given
* that they are in scope of the pull request. Outof scope suggestions are not made.
*
* In-scope suggestions are specifically: the suggestion for a file must correspond to a file in the remote pull request
* and the diff hunk computed for a file's contents must produce a range that is a subset of the pull request's files hunks.
*
* If a file is too large to load in the review, it is skipped in the suggestion phase.
*
* If changes are empty then the workflow will not run.
* Rethrows an HttpError if Octokit GitHub API returns an error. HttpError Octokit access_token and client_secret headers redact all sensitive information.
* @param octokit The authenticated octokit instance, instantiated with an access token having permissiong to create a fork on the target repository.
* @param diffContents A set of changes. The changes may be empty.
* @param options The configuration for interacting with GitHub provided by the user.
* @returns the created review's id number, or null if there are no changes to be made.
*/
async function reviewPullRequest(octokit, diffContents, options) {
(0, logger_1.setupLogger)(options.logger);
// if null undefined, or the empty map then no changes have been provided.
// Do not execute GitHub workflow
if (diffContents === null ||
diffContents === undefined ||
(typeof diffContents !== 'string' && diffContents.size === 0)) {
logger_1.logger.info('Empty changes provided. No suggestions to be made. Cancelling workflow.');
return null;
}
const gitHubConfigs = (0, default_options_handler_1.addReviewCommentsDefaults)(options);
const remote = {
owner: gitHubConfigs.owner,
repo: gitHubConfigs.repo,
};
const reviewNumber = await (0, review_pull_request_1.createPullRequestReview)(octokit, remote, gitHubConfigs.pullNumber, gitHubConfigs.pageSize, diffContents);
return reviewNumber;
}
exports.reviewPullRequest = reviewPullRequest;
/**
* Make a new GitHub Pull Request with a set of changes applied on top of primary branch HEAD.
* The changes are committed into a new branch based on the upstream repository options using the authenticated Octokit account.
* Then a Pull Request is made from that branch.
*
* Also throws error if git data from the fork is not ready in 5 minutes.
*
* From the docs
* https://developer.github.com/v3/repos/forks/#create-a-fork
* """
* Forking a Repository happens asynchronously.
* You may have to wait a short period of time before you can access the git objects.
* If this takes longer than 5 minutes, be sure to contact GitHub Support or GitHub Premium Support.
* """
*
* If changes are empty then the workflow will not run.
* Rethrows an HttpError if Octokit GitHub API returns an error. HttpError Octokit access_token and client_secret headers redact all sensitive information.
* @param {Octokit} octokit The authenticated octokit instance, instantiated with an access token having permissiong to create a fork on the target repository
* @param {Changes | null | undefined} changes A set of changes. The changes may be empty
* @param {CreatePullRequestUserOptions} options The configuration for interacting with GitHub provided by the user.
* @returns {Promise<number>} the pull request number. Returns 0 if unsuccessful.
* @throws {CommitError} on failure during commit process
*/
async function createPullRequest(octokit, changes, options) {
(0, logger_1.setupLogger)(options.logger);
// if null undefined, or the empty map then no changes have been provided.
// Do not execute GitHub workflow
if (changes === null || changes === undefined || changes.size === 0) {
logger_1.logger.info('Empty change set provided. No changes need to be made. Cancelling workflow.');
return 0;
}
const gitHubConfigs = (0, default_options_handler_1.addPullRequestDefaults)(options);
logger_1.logger.info('Starting GitHub PR workflow...');
const upstream = {
owner: gitHubConfigs.upstreamOwner,
repo: gitHubConfigs.upstreamRepo,
};
const origin = options.fork === false ? upstream : await (0, fork_1.fork)(octokit, upstream);
if (options.fork) {
// try to sync the fork
await retry(async () => await octokit.repos.mergeUpstream({
owner: origin.owner,
repo: origin.repo,
branch: gitHubConfigs.primary,
}), {
retries: options.retry,
factor: 2.8411,
minTimeout: 3000,
randomize: false,
onRetry: (e, attempt) => {
e.message = `Error creating syncing upstream: ${e.message}`;
logger_1.logger.error(e);
logger_1.logger.info(`Retry attempt #${attempt}...`);
},
});
}
const originBranch = {
...origin,
branch: gitHubConfigs.branch,
};
// The `retry` flag defaults to `5` to maintain compatibility
options.retry = options.retry === undefined ? 5 : options.retry;
const refHeadSha = await retry(async () => await (0, branch_1.branch)(octokit, origin, upstream, originBranch.branch, gitHubConfigs.primary), {
retries: options.retry,
factor: 2.8411,
minTimeout: 3000,
randomize: false,
onRetry: (e, attempt) => {
e.message = `Error creating Pull Request: ${e.message}`;
logger_1.logger.error(e);
logger_1.logger.info(`Retry attempt #${attempt}...`);
},
});
await (0, commit_and_push_1.commitAndPush)(octokit, refHeadSha, changes, originBranch, gitHubConfigs.message, gitHubConfigs.force, options);
const description = {
body: gitHubConfigs.description,
title: gitHubConfigs.title,
};
const prNumber = await (0, open_pull_request_1.openPullRequest)(octokit, upstream, originBranch, description, gitHubConfigs.maintainersCanModify, gitHubConfigs.primary, options.draft);
logger_1.logger.info(`Successfully opened pull request: ${prNumber}.`);
// addLabels will no-op if options.labels is undefined or empty.
await (0, labels_1.addLabels)(octokit, upstream, originBranch, prNumber, options.labels);
return prNumber;
}
exports.createPullRequest = createPullRequest;
/**
* Convert a Map<string,string> or {[path: string]: string}, where the key is the relative file path in the repository,
* and the value is the text content. The files will be converted to a Map also containing the file mode information '100644'
* @param {Object<string, string | null> | Map<string, string | null>} textFiles a map/object where the key is the relative file path and the value is the text file content
* @returns {Changes} Map of the file path to the string file content and the file mode '100644'
*/
function parseTextFiles(textFiles) {
const changes = new Map();
if (textFiles instanceof Map) {
textFiles.forEach((content, path) => {
if (typeof path !== 'string' ||
(content !== null && typeof content !== 'string')) {
throw TypeError('The file changeset provided must have a string key and a string/null value');
}
changes.set(path, new types_1.FileData(content));
});
}
else {
for (const [path, content] of Object.entries(textFiles)) {
if (typeof path !== 'string' ||
(content !== null && typeof content !== 'string')) {
throw TypeError('The file changeset provided must have a string key and a string/null value');
}
changes.set(path, new types_1.FileData(content));
}
}
return changes;
}
exports.parseTextFiles = parseTextFiles;
//# sourceMappingURL=index.js.map