"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const graphql_1 = require("@octokit/graphql");
const plugin_enterprise_compatibility_1 = tslib_1.__importDefault(require("@octokit/plugin-enterprise-compatibility"));
const path_1 = tslib_1.__importDefault(require("path"));
const plugin_retry_1 = tslib_1.__importDefault(require("@octokit/plugin-retry"));
const plugin_throttling_1 = tslib_1.__importDefault(require("@octokit/plugin-throttling"));
const rest_1 = tslib_1.__importDefault(require("@octokit/rest"));
const gitlog_1 = tslib_1.__importDefault(require("gitlog"));
const tinycolor2_1 = tslib_1.__importDefault(require("tinycolor2"));
const util_1 = require("util");
const dedent_1 = tslib_1.__importDefault(require("dedent"));
const typescript_memoize_1 = require("typescript-memoize");
const exec_promise_1 = tslib_1.__importDefault(require("./utils/exec-promise"));
const logger_1 = require("./utils/logger");
const gitlog = util_1.promisify(gitlog_1.default);
/** An error originating from the GitHub */
class GitAPIError extends Error {
    /** Extend the base error */
    constructor(api, args, origError) {
        super(`Error calling github: ${api}\n\twith: ${JSON.stringify(args)}.\n\t${origError.message}`);
    }
}
/** Make a comment to build automation in PRs off of. */
const makeIdentifier = (type, context) => `<!-- GITHUB_RELEASE ${type}: ${context} -->`;
/** Make an identifier for `auto comment` */
const makeCommentIdentifier = (context) => makeIdentifier('COMMENT', context);
/** Make an identifier for `auto pr-body` */
const makePrBodyIdentifier = (context) => makeIdentifier('PR BODY', context);
/**
 * A class to interact with the local git instance and the git remote.
 * currently it only interfaces with GitHub.
 */
class Git {
    /** Initialize the git interface and auth with GitHub */
    constructor(options, logger = logger_1.dummyLog()) {
        this.logger = logger;
        this.options = options;
        this.baseUrl = this.options.baseUrl || 'https://api.github.com';
        this.graphqlBaseUrl = this.options.graphqlBaseUrl || this.baseUrl;
        this.logger.veryVerbose.info(`Initializing GitHub with: ${this.baseUrl}`);
        const GitHub = rest_1.default.plugin(plugin_enterprise_compatibility_1.default)
            .plugin(plugin_retry_1.default)
            .plugin(plugin_throttling_1.default);
        this.github = new GitHub({
            baseUrl: this.baseUrl,
            agent: this.options.agent,
            auth: this.options.token,
            previews: ['symmetra-preview'],
            throttle: {
                onRateLimit: (retryAfter, opts) => {
                    this.logger.log.warn(`Request quota exhausted for request ${opts.method} ${opts.url}`);
                    if (opts.request.retryCount < 5) {
                        this.logger.verbose.log(`Retrying after ${retryAfter} seconds!`);
                        return true;
                    }
                },
                onAbuseLimit: (retryAfter, opts) => {
                    // does not retry, only logs an error
                    this.logger.log.error(`Went over abuse rate limit ${opts.method} ${opts.url}`);
                }
            }
        });
        this.github.hook.error('request', error => {
            var _a, _b;
            if ((_b = (_a = error) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b.authorization) {
                delete error.headers.authorization;
            }
            throw error;
        });
    }
    /** Get the "Latest Release" from GitHub */
    async getLatestReleaseInfo() {
        const latestRelease = await this.github.repos.getLatestRelease({
            owner: this.options.owner,
            repo: this.options.repo
        });
        return latestRelease.data;
    }
    /** Get the "Latest Release" or the first commit SHA as a fallback */
    async getLatestRelease() {
        try {
            const latestRelease = await this.getLatestReleaseInfo();
            this.logger.veryVerbose.info('Got response for "getLatestRelease":\n', latestRelease);
            this.logger.verbose.info('Got latest release:\n', latestRelease);
            return latestRelease.tag_name;
        }
        catch (e) {
            if (e.status === 404) {
                this.logger.verbose.info("Couldn't find latest release on GitHub, using first commit.");
                return this.getFirstCommit();
            }
            throw e;
        }
    }
    /** Get the date a commit sha was created */
    async getCommitDate(sha) {
        const date = await exec_promise_1.default('git', ['show', '-s', '--format=%ci', sha]);
        const [day, time, timezone] = date.split(' ');
        return `${day}T${time}${timezone}`;
    }
    /** Get the first commit for the repo */
    async getFirstCommit() {
        const list = await exec_promise_1.default('git', ['rev-list', 'HEAD']);
        return list.split('\n').pop();
    }
    /** Get the SHA of the latest commit */
    async getSha(short) {
        const result = await exec_promise_1.default('git', [
            'rev-parse',
            short && '--short',
            'HEAD'
        ]);
        this.logger.verbose.info(`Got commit SHA from HEAD: ${result}`);
        return result;
    }
    /** Get the labels for a PR */
    async getLabels(prNumber) {
        this.logger.verbose.info(`Getting labels for PR: ${prNumber}`);
        const args = {
            owner: this.options.owner,
            repo: this.options.repo,
            issue_number: prNumber
        };
        this.logger.verbose.info('Getting issue labels using:', args);
        try {
            const labels = await this.github.issues.listLabelsOnIssue(args);
            this.logger.veryVerbose.info('Got response for "listLabelsOnIssue":\n', labels);
            this.logger.verbose.info('Found labels on PR:\n', labels.data);
            return labels.data.map(l => l.name);
        }
        catch (e) {
            throw new GitAPIError('listLabelsOnIssue', args, e);
        }
    }
    /** Get all the information about a PR or issue */
    async getPr(prNumber) {
        this.logger.verbose.info(`Getting info for PR: ${prNumber}`);
        const args = {
            owner: this.options.owner,
            repo: this.options.repo,
            issue_number: prNumber
        };
        this.logger.verbose.info('Getting issue info using:', args);
        try {
            const info = await this.github.issues.get(args);
            this.logger.veryVerbose.info('Got response for "issues.get":\n', info);
            return info;
        }
        catch (e) {
            throw new GitAPIError('getPr', args, e);
        }
    }
    /** Get the labels for a the project */
    async getProjectLabels() {
        this.logger.verbose.info(`Getting labels for project: ${this.options.repo}`);
        const args = {
            owner: this.options.owner,
            repo: this.options.repo
        };
        try {
            const labels = await this.github.issues.listLabelsForRepo(args);
            this.logger.veryVerbose.info('Got response for "getProjectLabels":\n', labels);
            this.logger.verbose.info('Found labels on project:\n', labels.data);
            return labels.data.map(l => l.name);
        }
        catch (e) {
            throw new GitAPIError('getProjectLabels', args, e);
        }
    }
    /** Get the git log for a range of commits */
    async getGitLog(start, end = 'HEAD') {
        try {
            const log = await gitlog({
                repo: process.cwd(),
                number: Number.MAX_SAFE_INTEGER,
                fields: ['hash', 'authorName', 'authorEmail', 'rawBody'],
                branch: `${start.trim()}..${end.trim()}`,
                execOptions: { maxBuffer: 1000 * 1024 }
            });
            return log.map(commit => ({
                hash: commit.hash,
                authorName: commit.authorName,
                authorEmail: commit.authorEmail,
                subject: commit.rawBody,
                files: (commit.files || []).map(file => path_1.default.resolve(file))
            }));
        }
        catch (error) {
            const tag = error.match(/ambiguous argument '(\S+)\.\.HEAD'/);
            if (tag) {
                this.logger.log.error(dedent_1.default `
            Missing tag "${tag[1]}" so the command could not run.

            To fix this run the following command:

            git fetch --tags\n
          `);
                process.exit(1);
            }
            throw new Error(error);
        }
    }
    /** Get the GitHub user for an email. Will not work if they do not have their email set to "public". */
    async getUserByEmail(email) {
        var _a;
        try {
            const search = (await this.github.search.users({
                q: `in:email ${email}`
            })).data;
            return ((_a = search) === null || _a === void 0 ? void 0 : _a.items.length) > 0 ? search.items[0] : {};
        }
        catch (error) {
            this.logger.verbose.warn(`Could not find user by email: ${email}`);
        }
    }
    /** Get the GitHub user for a username */
    async getUserByUsername(username) {
        try {
            const user = await this.github.users.getByUsername({
                username
            });
            return user.data;
        }
        catch (error) {
            this.logger.verbose.warn(`Could not find user by username: ${username}`);
        }
    }
    /** Get all the information about a PR or issue */
    async getPullRequest(pr) {
        this.logger.verbose.info(`Getting Pull Request: ${pr}`);
        const args = {
            owner: this.options.owner,
            repo: this.options.repo,
            pull_number: pr
        };
        this.logger.verbose.info('Getting pull request info using:', args);
        const result = await this.github.pulls.get(args);
        this.logger.veryVerbose.info('Got pull request data\n', result);
        this.logger.verbose.info('Got pull request info');
        return result;
    }
    /** Search to GitHub project's issue and pull requests */
    async searchRepo(options) {
        const repo = `repo:${this.options.owner}/${this.options.repo}`;
        options.q = `${repo} ${options.q}`;
        this.logger.verbose.info('Searching repo using:\n', options);
        const result = await this.github.search.issuesAndPullRequests(options);
        this.logger.veryVerbose.info('Got response from search\n', result);
        this.logger.verbose.info('Searched repo on GitHub.');
        return result.data;
    }
    /** Run a graphql query on the GitHub project */
    async graphql(query) {
        this.logger.verbose.info('Querying Github using GraphQL:\n', query);
        const data = await graphql_1.graphql(query, {
            baseUrl: this.graphqlBaseUrl,
            headers: {
                authorization: `token ${this.options.token}`
            }
        });
        this.logger.veryVerbose.info('Got response from query\n', data);
        return data;
    }
    /** Create a status (or checkmark) on a commit */
    async createStatus(prInfo) {
        const args = Object.assign(Object.assign({}, prInfo), { owner: this.options.owner, repo: this.options.repo });
        this.logger.verbose.info('Creating status using:\n', args);
        const result = await this.github.repos.createStatus(args);
        this.logger.veryVerbose.info('Got response from createStatues\n', result);
        this.logger.verbose.info('Created status on GitHub.');
        return result;
    }
    /** Add a label to the project */
    async createLabel(name, label) {
        this.logger.verbose.info(`Creating "${name}" label :\n${label.name}`);
        const color = label.color
            ? tinycolor2_1.default(label.color).toString('hex6')
            : tinycolor2_1.default.random().toString('hex6');
        const result = await this.github.issues.createLabel({
            name: label.name,
            owner: this.options.owner,
            repo: this.options.repo,
            color: color.replace('#', ''),
            description: label.description
        });
        this.logger.veryVerbose.info('Got response from createLabel\n', result);
        this.logger.verbose.info('Created label on GitHub.');
        return result;
    }
    /** Update a label on the project */
    async updateLabel(name, label) {
        this.logger.verbose.info(`Updating "${name}" label :\n${label.name}`);
        const color = label.color
            ? tinycolor2_1.default(label.color).toString('hex6')
            : tinycolor2_1.default.random().toString('hex6');
        const result = await this.github.issues.updateLabel({
            current_name: label.name,
            name: label.name,
            owner: this.options.owner,
            repo: this.options.repo,
            color: color.replace('#', ''),
            description: label.description
        });
        this.logger.veryVerbose.info('Got response from updateLabel\n', result);
        this.logger.verbose.info('Updated label on GitHub.');
        return result;
    }
    /** Add a label to and issue or pull request */
    async addLabelToPr(pr, label) {
        this.logger.verbose.info(`Creating "${label}" label to PR ${pr}`);
        const result = await this.github.issues.addLabels({
            issue_number: pr,
            owner: this.options.owner,
            repo: this.options.repo,
            labels: [label]
        });
        this.logger.veryVerbose.info('Got response from addLabels\n', result);
        this.logger.verbose.info('Added labels on Pull Request.');
        return result;
    }
    /** Lock an issue */
    async lockIssue(issue) {
        this.logger.verbose.info(`Locking #${issue} issue...`);
        const result = await this.github.issues.lock({
            issue_number: issue,
            owner: this.options.owner,
            repo: this.options.repo
        });
        this.logger.veryVerbose.info('Got response from lock\n', result);
        this.logger.verbose.info('Locked issue.');
        return result;
    }
    /** Get information about the GitHub project */
    async getProject() {
        this.logger.verbose.info('Getting project from GitHub');
        const result = (await this.github.repos.get({
            owner: this.options.owner,
            repo: this.options.repo
        })).data;
        this.logger.veryVerbose.info('Got response from repos\n', result);
        this.logger.verbose.info('Got project information.');
        return result;
    }
    /** Get all the pull requests for a project */
    async getPullRequests(options) {
        this.logger.verbose.info('Getting pull requests...');
        const result = (await this.github.pulls.list(Object.assign({ owner: this.options.owner.toLowerCase(), repo: this.options.repo.toLowerCase() }, options))).data;
        this.logger.veryVerbose.info('Got response from pull requests', result);
        this.logger.verbose.info('Got pull request');
        return result;
    }
    /** Get all the commits for a PR */
    async getCommitsForPR(pr) {
        this.logger.verbose.info(`Getting commits for PR #${pr}`);
        const result = await this.github.paginate(this.github.pulls.listCommits.endpoint({
            owner: this.options.owner.toLowerCase(),
            repo: this.options.repo.toLowerCase(),
            pull_number: pr
        }));
        this.logger.veryVerbose.info(`Got response from PR #${pr}\n`, result);
        this.logger.verbose.info(`Got commits for PR #${pr}.`);
        return result;
    }
    /** Find a comment that is using the context in a PR */
    async getCommentId(pr, context = 'default') {
        const commentIdentifier = makeCommentIdentifier(context);
        this.logger.verbose.info('Getting previous comments on:', pr);
        const comments = await this.github.issues.listComments({
            owner: this.options.owner,
            repo: this.options.repo,
            issue_number: pr
        });
        this.logger.veryVerbose.info('Got PR comments\n', comments);
        const oldMessage = comments.data.find(comment => comment.body.includes(commentIdentifier));
        if (!oldMessage) {
            return -1;
        }
        this.logger.verbose.info('Found previous message from same scope.');
        return oldMessage.id;
    }
    /** Delete a comment on an issue or pull request */
    async deleteComment(pr, context = 'default') {
        const commentId = await this.getCommentId(pr, context);
        if (commentId === -1) {
            return;
        }
        this.logger.verbose.info(`Deleting comment: ${commentId}`);
        await this.github.issues.deleteComment({
            owner: this.options.owner,
            repo: this.options.repo,
            comment_id: commentId
        });
        this.logger.verbose.info(`Successfully deleted comment: ${commentId}`);
    }
    /** Create a comment on an issue or pull request */
    async createComment(message, pr, context = 'default') {
        const commentIdentifier = makeCommentIdentifier(context);
        this.logger.verbose.info('Using comment identifier:', commentIdentifier);
        await this.deleteComment(pr, context);
        this.logger.verbose.info('Creating new comment');
        const result = await this.github.issues.createComment({
            owner: this.options.owner,
            repo: this.options.repo,
            issue_number: pr,
            body: `${commentIdentifier}\n${message}`
        });
        this.logger.veryVerbose.info('Got response from creating comment\n', result);
        this.logger.verbose.info('Successfully posted comment to PR');
        return result;
    }
    /** Edit a comment on an issue or pull request */
    async editComment(message, pr, context = 'default') {
        const commentIdentifier = makeCommentIdentifier(context);
        this.logger.verbose.info('Using comment identifier:', commentIdentifier);
        const commentId = await this.getCommentId(pr, context);
        if (commentId === -1) {
            return this.createComment(message, pr, context);
        }
        this.logger.verbose.info('Editing comment');
        const result = await this.github.issues.updateComment({
            owner: this.options.owner,
            repo: this.options.repo,
            comment_id: commentId,
            body: `${commentIdentifier}\n${message}`
        });
        this.logger.veryVerbose.info('Got response from editing comment\n', result);
        this.logger.verbose.info('Successfully edited comment on PR');
        return result;
    }
    /** Create a comment on a pull request body */
    async addToPrBody(message, pr, context = 'default') {
        const id = makePrBodyIdentifier(context);
        this.logger.verbose.info('Using PR body identifier:', id);
        this.logger.verbose.info('Getting previous pr body on:', pr);
        const issue = await this.github.issues.get({
            owner: this.options.owner,
            repo: this.options.repo,
            issue_number: pr
        });
        this.logger.veryVerbose.info('Got PR description\n', issue.data.body);
        const regex = new RegExp(`(${id})\\s*([\\S\\s]*)\\s*(${id})`);
        let body = issue.data.body;
        if (body.match(regex)) {
            this.logger.verbose.info('Found previous message from same scope.');
            this.logger.verbose.info('Replacing pr body comment');
            body = body.replace(regex, message ? `$1\n${message}\n$3` : '');
        }
        else {
            body += message ? `\n${id}\n${message}\n${id}\n` : '';
        }
        this.logger.verbose.info('Creating new pr body');
        const result = await this.github.issues.update({
            owner: this.options.owner,
            repo: this.options.repo,
            issue_number: pr,
            body
        });
        this.logger.veryVerbose.info('Got response from updating body\n', result);
        this.logger.verbose.info(`Successfully updated body of PR #${pr}`);
        return result;
    }
    /** Create a release for the GitHub projecct */
    async publish(releaseNotes, tag) {
        this.logger.verbose.info('Creating release on GitHub for tag:', tag);
        const result = await this.github.repos.createRelease({
            owner: this.options.owner,
            repo: this.options.repo,
            tag_name: tag,
            body: releaseNotes
        });
        this.logger.veryVerbose.info('Got response from createRelease\n', result);
        this.logger.verbose.info('Created GitHub release.');
        return result;
    }
    /** Get the latest tag in the git tree */
    async getLatestTagInBranch() {
        return exec_promise_1.default('git', ['describe', '--tags', '--abbrev=0']);
    }
}
tslib_1.__decorate([
    typescript_memoize_1.Memoize()
], Git.prototype, "getLatestReleaseInfo", null);
tslib_1.__decorate([
    typescript_memoize_1.Memoize()
], Git.prototype, "getLatestRelease", null);
tslib_1.__decorate([
    typescript_memoize_1.Memoize()
], Git.prototype, "getLabels", null);
tslib_1.__decorate([
    typescript_memoize_1.Memoize()
], Git.prototype, "getPr", null);
tslib_1.__decorate([
    typescript_memoize_1.Memoize()
], Git.prototype, "getGitLog", null);
tslib_1.__decorate([
    typescript_memoize_1.Memoize()
], Git.prototype, "getUserByEmail", null);
tslib_1.__decorate([
    typescript_memoize_1.Memoize()
], Git.prototype, "getUserByUsername", null);
tslib_1.__decorate([
    typescript_memoize_1.Memoize()
], Git.prototype, "getPullRequest", null);
tslib_1.__decorate([
    typescript_memoize_1.Memoize()
], Git.prototype, "getProject", null);
tslib_1.__decorate([
    typescript_memoize_1.Memoize()
], Git.prototype, "getCommitsForPR", null);
exports.default = Git;
//# sourceMappingURL=git.js.map