"use strict";
/*
 * Copyright © 2019 Atomist, Inc.
 *
 * 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
 *
 *     http://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.DefaultExtractAuthor = exports.NoOpExtractAuthor = exports.AutofixProgressReporter = exports.AutofixProgressTests = exports.generateCommitMessageForAutofix = exports.filterImmediateAutofixes = exports.executeAutofixes = void 0;
const HandlerResult_1 = require("@atomist/automation-client/lib/HandlerResult");
const editModes_1 = require("@atomist/automation-client/lib/operations/edit/editModes");
const projectEditorOps_1 = require("@atomist/automation-client/lib/operations/edit/projectEditorOps");
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const slack_messages_1 = require("@atomist/slack-messages");
const _ = require("lodash");
const types_1 = require("../../typings/types");
const confirmEditedness_1 = require("../command/transform/confirmEditedness");
const minimalClone_1 = require("../goal/minimalClone");
const progress_1 = require("../goal/progress/progress");
const handlerRegistrations_1 = require("../machine/handlerRegistrations");
const child_process_1 = require("../misc/child_process");
const createPushImpactListenerInvocation_1 = require("./createPushImpactListenerInvocation");
const relevantCodeActions_1 = require("./relevantCodeActions");
/**
 * Execute autofixes against this push
 * Throw an error on failure
 * @param {AutofixRegistration[]} registrations
 * @return ExecuteGoal
 */
function executeAutofixes(registrations, transformPresentation, extractAuthor = exports.NoOpExtractAuthor) {
    return async (goalInvocation) => {
        const { id, configuration, goalEvent, credentials, context, progressLog } = goalInvocation;
        progressLog.write("Evaluating to %d configured autofixes", registrations.length);
        try {
            if (registrations.length === 0) {
                return HandlerResult_1.Success;
            }
            const push = goalEvent.push;
            const appliedAutofixes = [];
            let editMode;
            const editResult = await configuration.sdm.projectLoader.doWithProject({
                credentials,
                id,
                context,
                readOnly: false,
                cloneOptions: minimalClone_1.minimalClone(push),
            }, async (project) => {
                if ((await project.gitStatus()).sha !== id.sha) {
                    return {
                        success: true,
                        edited: false,
                        target: project,
                        description: "Autofixes not executing",
                        phase: "new commit on branch",
                    };
                }
                const cri = await createPushImpactListenerInvocation_1.createPushImpactListenerInvocation(goalInvocation, project);
                if (!!transformPresentation) {
                    editMode = transformPresentation(Object.assign(Object.assign({}, cri), { parameters: {
                            goalInvocation,
                        } }), project);
                    if (editModes_1.isBranchCommit(editMode)) {
                        if (await project.hasBranch(editMode.branch)) {
                            await project.checkout(editMode.branch);
                        }
                        else {
                            await project.createBranch(editMode.branch);
                        }
                    }
                }
                const relevantAutofixes = filterImmediateAutofixes(await relevantCodeActions_1.relevantCodeActions(registrations, cri), goalInvocation);
                progressLog.write("Applying %d relevant autofixes of %d to %s/%s: '%s' of configured '%s'", relevantAutofixes.length, registrations.length, cri.id.owner, cri.id.repo, relevantAutofixes.map(a => a.name).join(", "), registrations.map(a => a.name).join(", "));
                let cumulativeResult = {
                    target: cri.project,
                    success: true,
                    edited: false,
                };
                for (const autofix of _.flatten(relevantAutofixes)) {
                    const thisEdit = await runOne(goalInvocation, cri, autofix, extractAuthor);
                    if (thisEdit.edited) {
                        appliedAutofixes.push(autofix);
                    }
                    cumulativeResult = projectEditorOps_1.combineEditResults(cumulativeResult, thisEdit);
                }
                if (cumulativeResult.edited) {
                    await cri.project.push();
                    if (!!editMode && editModes_1.isPullRequest(editMode)) {
                        const targetBranch = editMode.targetBranch || goalEvent.branch;
                        let body = `${editMode.body}

Applied autofixes:
${appliedAutofixes.map(af => ` * ${slack_messages_1.codeLine(af.name)}`).join("\n")}

[atomist:generated] [atomist:autofix]`.trim();
                        if (editMode.autoMerge) {
                            body = `${body} ${editMode.autoMerge.mode} ${editMode.autoMerge.method ? editMode.autoMerge.method : ""}`.trim();
                        }
                        await cri.project.raisePullRequest(editMode.title, body, targetBranch);
                    }
                }
                return cumulativeResult;
            });
            if (editResult.edited) {
                // Send back a stop state to skip downstream goals
                return {
                    code: 0,
                    message: "Edited",
                    description: goalInvocation.goal.stoppedDescription,
                    state: isNewBranch(editMode, goalEvent.branch) ? types_1.SdmGoalState.success : types_1.SdmGoalState.stopped,
                    phase: detailMessage(appliedAutofixes),
                };
            }
            return {
                code: 0,
                description: editResult.description,
            };
        }
        catch (err) {
            logger_1.logger.warn("Autofixes failed with '%s':\n%s", err.message, err.stack);
            progressLog.write("Autofixes failed with '%s'", err.message);
            if (err.stdout) {
                progressLog.write(err.stdout);
            }
            if (err.stderr) {
                progressLog.write(err.stderr);
            }
            return {
                code: 1,
                message: err.message,
            };
        }
    };
}
exports.executeAutofixes = executeAutofixes;
/**
 * Check if this autofix is going to commit to a new branch
 */
function isNewBranch(editMode, branch) {
    if (!!editMode && editModes_1.isBranchCommit(editMode)) {
        return editMode.branch !== branch;
    }
    return false;
}
function detailMessage(appliedAutofixes) {
    // We show only two autofixes by name here as otherwise the message is going to get too long
    if (appliedAutofixes.length <= 2) {
        return `${appliedAutofixes.map(af => af.name).join(", ")}`;
    }
    else {
        return `${appliedAutofixes.length} autofixes`;
    }
}
async function runOne(gi, cri, autofix, extractAuthor) {
    const { progressLog, configuration } = gi;
    const project = cri.project;
    progressLog.write("About to transform %s with autofix '%s'", project.id.url, autofix.name);
    try {
        const arg2 = Object.assign(Object.assign(Object.assign({}, cri.context), cri), { push: cri, progressLog });
        const tentativeEditResult = await handlerRegistrations_1.toScalarProjectEditor(autofix.transform, configuration.sdm)(project, arg2, autofix.parametersInstance);
        const editResult = await confirmEditedness_1.confirmEditedness(tentativeEditResult);
        if (!editResult.success) {
            await project.revert();
            logger_1.logger.warn("Edited %s with autofix %s and success=false, edited=%d", project.id.url, autofix.name, editResult.edited);
            progressLog.write("Edited %s with autofix %s and success=false, edited=%d", project.id.url, autofix.name, editResult.edited);
            if (!!autofix.options && autofix.options.ignoreFailure) {
                // Say we didn't edit and can keep going
                return { target: project, edited: false, success: false };
            }
        }
        else if (editResult.edited) {
            progressLog.write("Autofix '%s' made changes", autofix.name);
            await project.commit(generateCommitMessageForAutofix(autofix));
            const author = await extractAuthor(gi);
            if (!!author && !!author.name && !!author.email) {
                await child_process_1.spawnLog("git", ["commit", "--amend", `--author="${author.name} <${author.email}>"`, "--no-edit"], {
                    cwd: project.baseDir,
                    log: progressLog,
                });
            }
        }
        else {
            progressLog.write("Autofix '%s' made no changes", autofix.name);
            logger_1.logger.debug("No changes were made by autofix %s", autofix.name);
        }
        return editResult;
    }
    catch (err) {
        if (!autofix.options || !autofix.options.ignoreFailure) {
            throw err;
        }
        await project.revert();
        logger_1.logger.warn("Ignoring autofix failure %s on %s with autofix %s", err.message, project.id.url, autofix.name);
        progressLog.write("Ignoring autofix failure %s on %s with autofix %s", err.message, project.id.url, autofix.name);
        return { target: project, success: false, edited: false };
    }
}
/**
 * Filter any provided autofixes whose results were included in the commits of the current push.
 * @param {AutofixRegistration[]} autofixes
 * @param {GoalInvocation} gi
 * @returns {AutofixRegistration[]}
 */
function filterImmediateAutofixes(autofixes, gi) {
    return autofixes.filter(af => !(gi.goalEvent.push.commits || [])
        .some(c => c.message === generateCommitMessageForAutofix(af)));
}
exports.filterImmediateAutofixes = filterImmediateAutofixes;
/**
 * Generate a commit message for the provided autofix.
 * @param {AutofixRegistration} autofix
 * @returns {string}
 */
function generateCommitMessageForAutofix(autofix) {
    const name = autofix.name.toLowerCase().replace(/ /g, "_");
    return `Autofix: ${autofix.name}\n\n[atomist:generated] [atomist:autofix=${name}]`;
}
exports.generateCommitMessageForAutofix = generateCommitMessageForAutofix;
exports.AutofixProgressTests = [{
        test: /About to transform .* autofix '(.*)'/i,
        phase: "$1",
    }];
/**
 * Default ReportProgress for running autofixes
 */
exports.AutofixProgressReporter = progress_1.testProgressReporter(...exports.AutofixProgressTests);
const NoOpExtractAuthor = async () => {
    return undefined;
};
exports.NoOpExtractAuthor = NoOpExtractAuthor;
const DefaultExtractAuthor = async (gi) => {
    const { goalEvent } = gi;
    const name = _.get(goalEvent, "push.after.author.name");
    const email = _.get(goalEvent, "push.after.author.emails[0].address");
    if (!!name && !!email) {
        return {
            name,
            email,
        };
    }
    else {
        return undefined;
    }
};
exports.DefaultExtractAuthor = DefaultExtractAuthor;
//# sourceMappingURL=executeAutofixes.js.map