UNPKG

23.2 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const graphql_1 = require("@octokit/graphql");
5const plugin_enterprise_compatibility_1 = tslib_1.__importDefault(require("@octokit/plugin-enterprise-compatibility"));
6const path_1 = tslib_1.__importDefault(require("path"));
7const plugin_retry_1 = tslib_1.__importDefault(require("@octokit/plugin-retry"));
8const plugin_throttling_1 = tslib_1.__importDefault(require("@octokit/plugin-throttling"));
9const rest_1 = tslib_1.__importDefault(require("@octokit/rest"));
10const gitlog_1 = tslib_1.__importDefault(require("gitlog"));
11const tinycolor2_1 = tslib_1.__importDefault(require("tinycolor2"));
12const util_1 = require("util");
13const endent_1 = tslib_1.__importDefault(require("endent"));
14const typescript_memoize_1 = require("typescript-memoize");
15const exec_promise_1 = tslib_1.__importDefault(require("./utils/exec-promise"));
16const logger_1 = require("./utils/logger");
17const gitlog = util_1.promisify(gitlog_1.default);
18/** An error originating from the GitHub */
19class GitAPIError extends Error {
20 /** Extend the base error */
21 constructor(api, args, origError) {
22 super(`Error calling github: ${api}\n\twith: ${JSON.stringify(args)}.\n\t${origError.message}`);
23 }
24}
25/** Make a comment to build automation in PRs off of. */
26const makeIdentifier = (type, context) => `<!-- GITHUB_RELEASE ${type}: ${context} -->`;
27/** Make an identifier for `auto comment` */
28const makeCommentIdentifier = (context) => makeIdentifier('COMMENT', context);
29/** Make an identifier for `auto pr-body` */
30const makePrBodyIdentifier = (context) => makeIdentifier('PR BODY', context);
31/**
32 * A class to interact with the local git instance and the git remote.
33 * currently it only interfaces with GitHub.
34 */
35class Git {
36 /** Initialize the git interface and auth with GitHub */
37 constructor(options, logger = logger_1.dummyLog()) {
38 this.logger = logger;
39 this.options = options;
40 this.baseUrl = this.options.baseUrl || 'https://api.github.com';
41 this.graphqlBaseUrl = this.options.graphqlBaseUrl || this.baseUrl;
42 this.logger.veryVerbose.info(`Initializing GitHub with: ${this.baseUrl}`);
43 const GitHub = rest_1.default.plugin(plugin_enterprise_compatibility_1.default)
44 .plugin(plugin_retry_1.default)
45 .plugin(plugin_throttling_1.default);
46 this.github = new GitHub({
47 baseUrl: this.baseUrl,
48 agent: this.options.agent,
49 auth: this.options.token,
50 previews: ['symmetra-preview'],
51 throttle: {
52 onRateLimit: (retryAfter, opts) => {
53 this.logger.log.warn(`Request quota exhausted for request ${opts.method} ${opts.url}`);
54 if (opts.request.retryCount < 5) {
55 this.logger.verbose.log(`Retrying after ${retryAfter} seconds!`);
56 return true;
57 }
58 },
59 onAbuseLimit: (retryAfter, opts) => {
60 // does not retry, only logs an error
61 this.logger.log.error(`Went over abuse rate limit ${opts.method} ${opts.url}`);
62 }
63 }
64 });
65 this.github.hook.error('request', error => {
66 var _a, _b;
67 if ((_b = (_a = error) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b.authorization) {
68 delete error.headers.authorization;
69 }
70 throw error;
71 });
72 }
73 /** Get the "Latest Release" from GitHub */
74 async getLatestReleaseInfo() {
75 const latestRelease = await this.github.repos.getLatestRelease({
76 owner: this.options.owner,
77 repo: this.options.repo
78 });
79 return latestRelease.data;
80 }
81 /** Get the "Latest Release" or the first commit SHA as a fallback */
82 async getLatestRelease() {
83 try {
84 const latestRelease = await this.getLatestReleaseInfo();
85 this.logger.veryVerbose.info('Got response for "getLatestRelease":\n', latestRelease);
86 this.logger.verbose.info('Got latest release:\n', latestRelease);
87 return latestRelease.tag_name;
88 }
89 catch (e) {
90 if (e.status === 404) {
91 this.logger.verbose.info("Couldn't find latest release on GitHub, using first commit.");
92 return this.getFirstCommit();
93 }
94 throw e;
95 }
96 }
97 /** Get the date a commit sha was created */
98 async getCommitDate(sha) {
99 const date = await exec_promise_1.default('git', ['show', '-s', '--format=%ci', sha]);
100 const [day, time, timezone] = date.split(' ');
101 return `${day}T${time}${timezone}`;
102 }
103 /** Get the first commit for the repo */
104 async getFirstCommit() {
105 const list = await exec_promise_1.default('git', ['rev-list', 'HEAD']);
106 return list.split('\n').pop();
107 }
108 /** Get the SHA of the latest commit */
109 async getSha(short) {
110 const result = await exec_promise_1.default('git', [
111 'rev-parse',
112 short && '--short',
113 'HEAD'
114 ]);
115 this.logger.verbose.info(`Got commit SHA from HEAD: ${result}`);
116 return result;
117 }
118 /** Get the labels for a PR */
119 async getLabels(prNumber) {
120 this.logger.verbose.info(`Getting labels for PR: ${prNumber}`);
121 const args = {
122 owner: this.options.owner,
123 repo: this.options.repo,
124 issue_number: prNumber
125 };
126 this.logger.verbose.info('Getting issue labels using:', args);
127 try {
128 const labels = await this.github.issues.listLabelsOnIssue(args);
129 this.logger.veryVerbose.info('Got response for "listLabelsOnIssue":\n', labels);
130 this.logger.verbose.info('Found labels on PR:\n', labels.data);
131 return labels.data.map(l => l.name);
132 }
133 catch (e) {
134 throw new GitAPIError('listLabelsOnIssue', args, e);
135 }
136 }
137 /** Get all the information about a PR or issue */
138 async getPr(prNumber) {
139 this.logger.verbose.info(`Getting info for PR: ${prNumber}`);
140 const args = {
141 owner: this.options.owner,
142 repo: this.options.repo,
143 issue_number: prNumber
144 };
145 this.logger.verbose.info('Getting issue info using:', args);
146 try {
147 const info = await this.github.issues.get(args);
148 this.logger.veryVerbose.info('Got response for "issues.get":\n', info);
149 return info;
150 }
151 catch (e) {
152 throw new GitAPIError('getPr', args, e);
153 }
154 }
155 /** Get information about specific commit */
156 async getCommit(sha) {
157 this.logger.verbose.info(`Getting info for commit: ${sha}`);
158 try {
159 const info = await this.github.repos.getCommit({
160 owner: this.options.owner,
161 repo: this.options.repo,
162 ref: sha
163 });
164 this.logger.veryVerbose.info('Got response for "repos.getCommit":\n', info);
165 return info;
166 }
167 catch (e) {
168 throw new GitAPIError('getCommit', [], e);
169 }
170 }
171 /** Get the labels for a the project */
172 async getProjectLabels() {
173 this.logger.verbose.info(`Getting labels for project: ${this.options.repo}`);
174 const args = {
175 owner: this.options.owner,
176 repo: this.options.repo
177 };
178 try {
179 const labels = await this.github.issues.listLabelsForRepo(args);
180 this.logger.veryVerbose.info('Got response for "getProjectLabels":\n', labels);
181 this.logger.verbose.info('Found labels on project:\n', labels.data);
182 return labels.data.map(l => l.name);
183 }
184 catch (e) {
185 throw new GitAPIError('getProjectLabels', args, e);
186 }
187 }
188 /** Get the git log for a range of commits */
189 async getGitLog(start, end = 'HEAD') {
190 try {
191 const log = await gitlog({
192 repo: process.cwd(),
193 number: Number.MAX_SAFE_INTEGER,
194 fields: ['hash', 'authorName', 'authorEmail', 'rawBody'],
195 branch: `${start.trim()}..${end.trim()}`,
196 execOptions: { maxBuffer: 1000 * 1024 }
197 });
198 return log.map(commit => ({
199 hash: commit.hash,
200 authorName: commit.authorName,
201 authorEmail: commit.authorEmail,
202 subject: commit.rawBody,
203 files: (commit.files || []).map(file => path_1.default.resolve(file))
204 }));
205 }
206 catch (error) {
207 const tag = error.match(/ambiguous argument '(\S+)\.\.\S+'/);
208 if (tag) {
209 this.logger.log.error(endent_1.default `
210 Missing tag "${tag[1]}" so the command could not run.
211
212 To fix this run the following command:
213
214 git fetch --tags\n
215 `);
216 process.exit(1);
217 }
218 throw new Error(error);
219 }
220 }
221 /** Get the GitHub user for an email. Will not work if they do not have their email set to "public". */
222 async getUserByEmail(email) {
223 var _a;
224 try {
225 const search = (await this.github.search.users({
226 q: `in:email ${email}`
227 })).data;
228 return ((_a = search) === null || _a === void 0 ? void 0 : _a.items.length) > 0 ? search.items[0] : {};
229 }
230 catch (error) {
231 this.logger.verbose.warn(`Could not find user by email: ${email}`);
232 }
233 }
234 /** Get the GitHub user for a username */
235 async getUserByUsername(username) {
236 try {
237 const user = await this.github.users.getByUsername({
238 username
239 });
240 return user.data;
241 }
242 catch (error) {
243 this.logger.verbose.warn(`Could not find user by username: ${username}`);
244 }
245 }
246 /** Get all the information about a PR or issue */
247 async getPullRequest(pr) {
248 this.logger.verbose.info(`Getting Pull Request: ${pr}`);
249 const args = {
250 owner: this.options.owner,
251 repo: this.options.repo,
252 pull_number: pr
253 };
254 this.logger.verbose.info('Getting pull request info using:', args);
255 const result = await this.github.pulls.get(args);
256 this.logger.veryVerbose.info('Got pull request data\n', result);
257 this.logger.verbose.info('Got pull request info');
258 return result;
259 }
260 /** Search to GitHub project's issue and pull requests */
261 async searchRepo(options) {
262 const repo = `repo:${this.options.owner}/${this.options.repo}`;
263 options.q = `${repo} ${options.q}`;
264 this.logger.verbose.info('Searching repo using:\n', options);
265 const result = await this.github.search.issuesAndPullRequests(options);
266 this.logger.veryVerbose.info('Got response from search\n', result);
267 this.logger.verbose.info('Searched repo on GitHub.');
268 return result.data;
269 }
270 /** Run a graphql query on the GitHub project */
271 async graphql(query) {
272 this.logger.verbose.info('Querying Github using GraphQL:\n', query);
273 const data = await graphql_1.graphql(query, {
274 baseUrl: this.graphqlBaseUrl,
275 headers: {
276 authorization: `token ${this.options.token}`
277 }
278 });
279 this.logger.veryVerbose.info('Got response from query\n', data);
280 return data;
281 }
282 /** Create a status (or checkmark) on a commit */
283 async createStatus(prInfo) {
284 const args = Object.assign(Object.assign({}, prInfo), { owner: this.options.owner, repo: this.options.repo });
285 this.logger.verbose.info('Creating status using:\n', args);
286 const result = await this.github.repos.createStatus(args);
287 this.logger.veryVerbose.info('Got response from createStatues\n', result);
288 this.logger.verbose.info('Created status on GitHub.');
289 return result;
290 }
291 /** Add a label to the project */
292 async createLabel(label) {
293 this.logger.verbose.info(`Creating "${label.releaseType || 'general'}" label :\n${label.name}`);
294 const color = label.color
295 ? tinycolor2_1.default(label.color).toString('hex6')
296 : tinycolor2_1.default.random().toString('hex6');
297 const result = await this.github.issues.createLabel({
298 name: label.name,
299 owner: this.options.owner,
300 repo: this.options.repo,
301 color: color.replace('#', ''),
302 description: label.description
303 });
304 this.logger.veryVerbose.info('Got response from createLabel\n', result);
305 this.logger.verbose.info('Created label on GitHub.');
306 return result;
307 }
308 /** Update a label on the project */
309 async updateLabel(label) {
310 this.logger.verbose.info(`Updating "${label.releaseType || 'generic'}" label :\n${label.name}`);
311 const color = label.color
312 ? tinycolor2_1.default(label.color).toString('hex6')
313 : tinycolor2_1.default.random().toString('hex6');
314 const result = await this.github.issues.updateLabel({
315 current_name: label.name,
316 name: label.name,
317 owner: this.options.owner,
318 repo: this.options.repo,
319 color: color.replace('#', ''),
320 description: label.description
321 });
322 this.logger.veryVerbose.info('Got response from updateLabel\n', result);
323 this.logger.verbose.info('Updated label on GitHub.');
324 return result;
325 }
326 /** Add a label to and issue or pull request */
327 async addLabelToPr(pr, label) {
328 this.logger.verbose.info(`Creating "${label}" label to PR ${pr}`);
329 const result = await this.github.issues.addLabels({
330 issue_number: pr,
331 owner: this.options.owner,
332 repo: this.options.repo,
333 labels: [label]
334 });
335 this.logger.veryVerbose.info('Got response from addLabels\n', result);
336 this.logger.verbose.info('Added labels on Pull Request.');
337 return result;
338 }
339 /** Add a label to and issue or pull request */
340 async removeLabel(pr, label) {
341 this.logger.verbose.info(`Removing "${label}" from #${pr}`);
342 const result = await this.github.issues.removeLabel({
343 issue_number: pr,
344 owner: this.options.owner,
345 repo: this.options.repo,
346 name: label
347 });
348 this.logger.veryVerbose.info('Got response from removeLabel\n', result);
349 this.logger.verbose.info('Removed label on Pull Request.');
350 return result;
351 }
352 /** Lock an issue */
353 async lockIssue(issue) {
354 this.logger.verbose.info(`Locking #${issue} issue...`);
355 const result = await this.github.issues.lock({
356 issue_number: issue,
357 owner: this.options.owner,
358 repo: this.options.repo
359 });
360 this.logger.veryVerbose.info('Got response from lock\n', result);
361 this.logger.verbose.info('Locked issue.');
362 return result;
363 }
364 /** Get information about the GitHub project */
365 async getProject() {
366 this.logger.verbose.info('Getting project from GitHub');
367 const result = (await this.github.repos.get({
368 owner: this.options.owner,
369 repo: this.options.repo
370 })).data;
371 this.logger.veryVerbose.info('Got response from repos\n', result);
372 this.logger.verbose.info('Got project information.');
373 return result;
374 }
375 /** Get all the pull requests for a project */
376 async getPullRequests(options) {
377 this.logger.verbose.info('Getting pull requests...');
378 const result = (await this.github.pulls.list(Object.assign({ owner: this.options.owner.toLowerCase(), repo: this.options.repo.toLowerCase() }, options))).data;
379 this.logger.veryVerbose.info('Got response from pull requests', result);
380 this.logger.verbose.info('Got pull request');
381 return result;
382 }
383 /** Get all the commits for a PR */
384 async getCommitsForPR(pr) {
385 this.logger.verbose.info(`Getting commits for PR #${pr}`);
386 const result = await this.github.paginate(this.github.pulls.listCommits.endpoint({
387 owner: this.options.owner.toLowerCase(),
388 repo: this.options.repo.toLowerCase(),
389 pull_number: pr
390 }));
391 this.logger.veryVerbose.info(`Got response from PR #${pr}\n`, result);
392 this.logger.verbose.info(`Got commits for PR #${pr}.`);
393 return result;
394 }
395 /** Find a comment that is using the context in a PR */
396 async getCommentId(pr, context = 'default') {
397 const commentIdentifier = makeCommentIdentifier(context);
398 this.logger.verbose.info('Getting previous comments on:', pr);
399 const comments = await this.github.issues.listComments({
400 owner: this.options.owner,
401 repo: this.options.repo,
402 issue_number: pr
403 });
404 this.logger.veryVerbose.info('Got PR comments\n', comments);
405 const oldMessage = comments.data.find(comment => comment.body.includes(commentIdentifier));
406 if (!oldMessage) {
407 return -1;
408 }
409 this.logger.verbose.info('Found previous message from same scope.');
410 return oldMessage.id;
411 }
412 /** Delete a comment on an issue or pull request */
413 async deleteComment(pr, context = 'default') {
414 const commentId = await this.getCommentId(pr, context);
415 if (commentId === -1) {
416 return;
417 }
418 this.logger.verbose.info(`Deleting comment: ${commentId}`);
419 await this.github.issues.deleteComment({
420 owner: this.options.owner,
421 repo: this.options.repo,
422 comment_id: commentId
423 });
424 this.logger.verbose.info(`Successfully deleted comment: ${commentId}`);
425 }
426 /** Create a comment on an issue or pull request */
427 async createComment(message, pr, context = 'default') {
428 const commentIdentifier = makeCommentIdentifier(context);
429 this.logger.verbose.info('Using comment identifier:', commentIdentifier);
430 await this.deleteComment(pr, context);
431 this.logger.verbose.info('Creating new comment');
432 const result = await this.github.issues.createComment({
433 owner: this.options.owner,
434 repo: this.options.repo,
435 issue_number: pr,
436 body: `${commentIdentifier}\n${message}`
437 });
438 this.logger.veryVerbose.info('Got response from creating comment\n', result);
439 this.logger.verbose.info('Successfully posted comment to PR');
440 return result;
441 }
442 /** Edit a comment on an issue or pull request */
443 async editComment(message, pr, context = 'default') {
444 const commentIdentifier = makeCommentIdentifier(context);
445 this.logger.verbose.info('Using comment identifier:', commentIdentifier);
446 const commentId = await this.getCommentId(pr, context);
447 if (commentId === -1) {
448 return this.createComment(message, pr, context);
449 }
450 this.logger.verbose.info('Editing comment');
451 const result = await this.github.issues.updateComment({
452 owner: this.options.owner,
453 repo: this.options.repo,
454 comment_id: commentId,
455 body: `${commentIdentifier}\n${message}`
456 });
457 this.logger.veryVerbose.info('Got response from editing comment\n', result);
458 this.logger.verbose.info('Successfully edited comment on PR');
459 return result;
460 }
461 /** Create a comment on a pull request body */
462 async addToPrBody(message, pr, context = 'default') {
463 const id = makePrBodyIdentifier(context);
464 this.logger.verbose.info('Using PR body identifier:', id);
465 this.logger.verbose.info('Getting previous pr body on:', pr);
466 const issue = await this.github.issues.get({
467 owner: this.options.owner,
468 repo: this.options.repo,
469 issue_number: pr
470 });
471 this.logger.veryVerbose.info('Got PR description\n', issue.data.body);
472 const regex = new RegExp(`(${id})\\s*([\\S\\s]*)\\s*(${id})`);
473 let body = issue.data.body;
474 if (body.match(regex)) {
475 this.logger.verbose.info('Found previous message from same scope.');
476 this.logger.verbose.info('Replacing pr body comment');
477 body = body.replace(regex, message ? `$1\n${message}\n$3` : '');
478 }
479 else {
480 body += message ? `\n${id}\n${message}\n${id}\n` : '';
481 }
482 this.logger.verbose.info('Creating new pr body');
483 const result = await this.github.issues.update({
484 owner: this.options.owner,
485 repo: this.options.repo,
486 issue_number: pr,
487 body
488 });
489 this.logger.veryVerbose.info('Got response from updating body\n', result);
490 this.logger.verbose.info(`Successfully updated body of PR #${pr}`);
491 return result;
492 }
493 /** Create a release for the GitHub projecct */
494 async publish(releaseNotes, tag, prerelease = false) {
495 this.logger.verbose.info('Creating release on GitHub for tag:', tag);
496 const result = await this.github.repos.createRelease({
497 owner: this.options.owner,
498 repo: this.options.repo,
499 tag_name: tag,
500 body: releaseNotes,
501 prerelease
502 });
503 this.logger.veryVerbose.info('Got response from createRelease\n', result);
504 this.logger.verbose.info('Created GitHub release.');
505 return result;
506 }
507 /** Get the latest tag in the git tree */
508 async getLatestTagInBranch() {
509 return exec_promise_1.default('git', ['describe', '--tags', '--abbrev=0']);
510 }
511}
512tslib_1.__decorate([
513 typescript_memoize_1.Memoize()
514], Git.prototype, "getLatestReleaseInfo", null);
515tslib_1.__decorate([
516 typescript_memoize_1.Memoize()
517], Git.prototype, "getLatestRelease", null);
518tslib_1.__decorate([
519 typescript_memoize_1.Memoize()
520], Git.prototype, "getLabels", null);
521tslib_1.__decorate([
522 typescript_memoize_1.Memoize()
523], Git.prototype, "getPr", null);
524tslib_1.__decorate([
525 typescript_memoize_1.Memoize()
526], Git.prototype, "getCommit", null);
527tslib_1.__decorate([
528 typescript_memoize_1.Memoize()
529], Git.prototype, "getGitLog", null);
530tslib_1.__decorate([
531 typescript_memoize_1.Memoize()
532], Git.prototype, "getUserByEmail", null);
533tslib_1.__decorate([
534 typescript_memoize_1.Memoize()
535], Git.prototype, "getUserByUsername", null);
536tslib_1.__decorate([
537 typescript_memoize_1.Memoize()
538], Git.prototype, "getPullRequest", null);
539tslib_1.__decorate([
540 typescript_memoize_1.Memoize()
541], Git.prototype, "getProject", null);
542tslib_1.__decorate([
543 typescript_memoize_1.Memoize()
544], Git.prototype, "getCommitsForPR", null);
545exports.default = Git;
546//# sourceMappingURL=git.js.map
\No newline at end of file