"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChangeAction = void 0;
const path = __importStar(require("node:path"));
const child_process = __importStar(require("node:child_process"));
const node_core_library_1 = require("@rushstack/node-core-library");
const terminal_1 = require("@rushstack/terminal");
const package_deps_hash_1 = require("@rushstack/package-deps-hash");
const ChangeManagement_1 = require("../../api/ChangeManagement");
const ChangeFile_1 = require("../../api/ChangeFile");
const BaseRushAction_1 = require("./BaseRushAction");
const ChangeFiles_1 = require("../../logic/ChangeFiles");
const VersionPolicy_1 = require("../../api/VersionPolicy");
const ProjectChangeAnalyzer_1 = require("../../logic/ProjectChangeAnalyzer");
const Git_1 = require("../../logic/Git");
const RushConstants_1 = require("../../logic/RushConstants");
const BULK_LONG_NAME = '--bulk';
const BULK_MESSAGE_LONG_NAME = '--message';
const BULK_BUMP_TYPE_LONG_NAME = '--bump-type';
class ChangeAction extends BaseRushAction_1.BaseRushAction {
    constructor(parser) {
        const documentation = [
            'Asks a series of questions and then generates a <branchname>-<timestamp>.json file ' +
                'in the common folder. The `publish` command will consume these files and perform the proper ' +
                'version bumps. Note these changes will eventually be published in a changelog.md file in each package.',
            '',
            'The possible types of changes are: ',
            '',
            'MAJOR - these are breaking changes that are not backwards compatible. ' +
                'Examples are: renaming a public class, adding/removing a non-optional ' +
                'parameter from a public API, or renaming an variable or function that ' +
                'is exported.',
            '',
            'MINOR - these are changes that are backwards compatible (but not ' +
                'forwards compatible). Examples are: adding a new public API or adding an ' +
                'optional parameter to a public API',
            '',
            'PATCH - these are changes that are backwards and forwards compatible. ' +
                'Examples are: Modifying a private API or fixing a bug in the logic ' +
                'of how an existing API works.',
            '',
            "NONE - these are changes that are backwards and forwards compatible and don't require an immediate release. " +
                'Examples are: Modifying dev tooling configuration like eslint.',
            '',
            'HOTFIX (EXPERIMENTAL) - these are changes that are hotfixes targeting a ' +
                'specific older version of the package. When a hotfix change is added, ' +
                'other changes will not be able to increment the version number. ' +
                `Enable this feature by setting 'hotfixChangeEnabled' in your ${RushConstants_1.RushConstants.rushJsonFilename}.`,
            ''
        ].join('\n');
        super({
            actionName: 'change',
            summary: 'Records changes made to projects, indicating how the package version number should be bumped ' +
                'for the next publish.',
            documentation,
            safeForSimultaneousRushProcesses: true,
            parser
        });
        this._git = new Git_1.Git(this.rushConfiguration);
        this._verifyParameter = this.defineFlagParameter({
            parameterLongName: '--verify',
            parameterShortName: '-v',
            description: 'Verify the change file has been generated and that it is a valid JSON file'
        });
        this._noFetchParameter = this.defineFlagParameter({
            parameterLongName: '--no-fetch',
            description: 'Skips fetching the baseline branch before running "git diff" to detect changes.'
        });
        this._targetBranchParameter = this.defineStringParameter({
            parameterLongName: '--target-branch',
            parameterShortName: '-b',
            argumentName: 'BRANCH',
            description: 'If this parameter is specified, compare the checked out branch with the specified branch to ' +
                'determine which projects were changed. If this parameter is not specified, the checked out branch ' +
                'is compared against the "main" branch.'
        });
        this._overwriteFlagParameter = this.defineFlagParameter({
            parameterLongName: '--overwrite',
            description: `If a changefile already exists, overwrite without prompting ` +
                `(or erroring in ${BULK_LONG_NAME} mode).`
        });
        this._commitChangesFlagParameter = this.defineFlagParameter({
            parameterLongName: '--commit',
            parameterShortName: '-c',
            description: `If this flag is specified generated changefiles will be commited automatically.`
        });
        this._commitChangesMessageStringParameter = this.defineStringParameter({
            parameterLongName: '--commit-message',
            argumentName: 'COMMIT_MESSAGE',
            description: `If this parameter is specified generated changefiles will be commited automatically with the specified commit message.`
        });
        this._changeEmailParameter = this.defineStringParameter({
            parameterLongName: '--email',
            argumentName: 'EMAIL',
            description: 'The email address to use in changefiles. If this parameter is not provided, the email address ' +
                'will be detected or prompted for in interactive mode.'
        });
        this._bulkChangeParameter = this.defineFlagParameter({
            parameterLongName: BULK_LONG_NAME,
            description: 'If this flag is specified, apply the same change message and bump type to all changed projects. ' +
                `The ${BULK_MESSAGE_LONG_NAME} and the ${BULK_BUMP_TYPE_LONG_NAME} parameters must be specified if the ` +
                `${BULK_LONG_NAME} parameter is specified`
        });
        this._bulkChangeMessageParameter = this.defineStringParameter({
            parameterLongName: BULK_MESSAGE_LONG_NAME,
            argumentName: 'MESSAGE',
            description: `The message to apply to all changed projects if the ${BULK_LONG_NAME} flag is provided.`
        });
        this._bulkChangeBumpTypeParameter = this.defineChoiceParameter({
            parameterLongName: BULK_BUMP_TYPE_LONG_NAME,
            alternatives: [...Object.keys(this._getBumpOptions())],
            description: `The bump type to apply to all changed projects if the ${BULK_LONG_NAME} flag is provided.`
        });
    }
    async runAsync() {
        const targetBranch = await this._getTargetBranchAsync();
        // eslint-disable-next-line no-console
        console.log(`The target branch is ${targetBranch}`);
        if (this._verifyParameter.value) {
            const errors = [
                this._bulkChangeParameter,
                this._bulkChangeMessageParameter,
                this._bulkChangeBumpTypeParameter,
                this._overwriteFlagParameter,
                this._commitChangesFlagParameter
            ]
                .map((parameter) => {
                return parameter.value
                    ? `The {${this._bulkChangeParameter.longName} parameter cannot be provided with the ` +
                        `${this._verifyParameter.longName} parameter`
                    : '';
            })
                .filter((error) => error !== '');
            if (errors.length > 0) {
                errors.forEach((error) => {
                    // eslint-disable-next-line no-console
                    console.error(error);
                });
                throw new node_core_library_1.AlreadyReportedError();
            }
            await this._verifyAsync();
            return;
        }
        const sortedProjectList = (await this._getChangedProjectNamesAsync()).sort();
        if (sortedProjectList.length === 0) {
            this._logNoChangeFileRequired();
            await this._warnUnstagedChangesAsync();
            return;
        }
        await this._warnUnstagedChangesAsync();
        const inquirer = await Promise.resolve().then(() => __importStar(require('inquirer')));
        const promptModule = inquirer.createPromptModule();
        let changeFileData = new Map();
        let interactiveMode = false;
        if (this._bulkChangeParameter.value) {
            if (!this._bulkChangeBumpTypeParameter.value ||
                (!this._bulkChangeMessageParameter.value &&
                    this._bulkChangeBumpTypeParameter.value !== ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.none])) {
                throw new Error(`The ${this._bulkChangeBumpTypeParameter.longName} and ${this._bulkChangeMessageParameter.longName} ` +
                    `parameters must provided if the ${this._bulkChangeParameter.longName} flag is provided. If the value ` +
                    `"${ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.none]}" is provided to the ${this._bulkChangeBumpTypeParameter.longName} ` +
                    `parameter, the ${this._bulkChangeMessageParameter.longName} parameter may be omitted.`);
            }
            const email = this._changeEmailParameter.value || this._detectEmail();
            if (!email) {
                throw new Error("Unable to detect Git email and an email address wasn't provided using the " +
                    `${this._changeEmailParameter.longName} parameter.`);
            }
            const errors = [];
            const comment = this._bulkChangeMessageParameter.value || '';
            const changeType = this._bulkChangeBumpTypeParameter.value;
            for (const packageName of sortedProjectList) {
                const allowedBumpTypes = Object.keys(this._getBumpOptions(packageName));
                let projectChangeType = changeType;
                if (allowedBumpTypes.length === 0) {
                    projectChangeType = ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.none];
                }
                else if (projectChangeType !== ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.none] &&
                    allowedBumpTypes.indexOf(projectChangeType) === -1) {
                    errors.push(`The "${projectChangeType}" change type is not allowed for package "${packageName}".`);
                }
                changeFileData.set(packageName, {
                    changes: [
                        {
                            comment,
                            type: projectChangeType,
                            packageName
                        }
                    ],
                    packageName,
                    email
                });
            }
            if (errors.length > 0) {
                for (const error of errors) {
                    // eslint-disable-next-line no-console
                    console.error(error);
                }
                throw new node_core_library_1.AlreadyReportedError();
            }
        }
        else if (this._bulkChangeBumpTypeParameter.value || this._bulkChangeMessageParameter.value) {
            throw new Error(`The ${this._bulkChangeParameter.longName} flag must be provided with the ` +
                `${this._bulkChangeBumpTypeParameter.longName} and ${this._bulkChangeMessageParameter.longName} parameters.`);
        }
        else {
            interactiveMode = true;
            const existingChangeComments = ChangeFiles_1.ChangeFiles.getChangeComments(await this._getChangeFilesAsync());
            changeFileData = await this._promptForChangeFileDataAsync(promptModule, sortedProjectList, existingChangeComments);
            if (this._isEmailRequired(changeFileData)) {
                const email = this._changeEmailParameter.value
                    ? this._changeEmailParameter.value
                    : await this._detectOrAskForEmailAsync(promptModule);
                changeFileData.forEach((changeFile) => {
                    var _a, _b;
                    changeFile.email = ((_b = (_a = this.rushConfiguration.getProjectByName(changeFile.packageName)) === null || _a === void 0 ? void 0 : _a.versionPolicy) === null || _b === void 0 ? void 0 : _b.includeEmailInChangeFile)
                        ? email
                        : '';
                });
            }
        }
        let changefiles;
        try {
            changefiles = await this._writeChangeFilesAsync(promptModule, changeFileData, this._overwriteFlagParameter.value, interactiveMode);
        }
        catch (error) {
            throw new Error(`There was an error creating a change file: ${error.toString()}`);
        }
        if (this._commitChangesFlagParameter.value || this._commitChangesMessageStringParameter.value) {
            if (changefiles && changefiles.length !== 0) {
                await this._git.stageAndCommitGitChangesAsync(changefiles, this._commitChangesMessageStringParameter.value ||
                    this.rushConfiguration.gitChangefilesCommitMessage ||
                    'Rush change', this.terminal);
            }
            else {
                this.terminal.writeWarningLine('Warning: No change files generated, nothing to commit.');
            }
        }
    }
    _generateHostMap() {
        var _a;
        const hostMap = new Map();
        for (const project of this.rushConfiguration.projects) {
            let hostProjectName = project.packageName;
            if ((_a = project.versionPolicy) === null || _a === void 0 ? void 0 : _a.isLockstepped) {
                const lockstepPolicy = project.versionPolicy;
                hostProjectName = lockstepPolicy.mainProject || project.packageName;
            }
            hostMap.set(project, hostProjectName);
        }
        return hostMap;
    }
    async _verifyAsync() {
        const changedPackages = await this._getChangedProjectNamesAsync();
        if (changedPackages.length > 0) {
            await this._validateChangeFileAsync(changedPackages);
        }
        else {
            this._logNoChangeFileRequired();
        }
    }
    async _getTargetBranchAsync() {
        if (!this._targetBranchName) {
            this._targetBranchName =
                this._targetBranchParameter.value || (await this._git.getRemoteDefaultBranchAsync());
        }
        return this._targetBranchName;
    }
    async _getChangedProjectNamesAsync() {
        var _a;
        const projectChangeAnalyzer = new ProjectChangeAnalyzer_1.ProjectChangeAnalyzer(this.rushConfiguration);
        const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({
            targetBranchName: await this._getTargetBranchAsync(),
            terminal: this.terminal,
            shouldFetch: !this._noFetchParameter.value,
            // Lockfile evaluation will expand the set of projects that request change files
            // Not enabling, since this would be a breaking change
            includeExternalDependencies: false,
            // Since install may not have happened, cannot read rush-project.json
            enableFiltering: false
        });
        const projectHostMap = this._generateHostMap();
        const changedProjectNames = new Set();
        for (const changedProject of changedProjects) {
            if (changedProject.shouldPublish && !((_a = changedProject.versionPolicy) === null || _a === void 0 ? void 0 : _a.exemptFromRushChange)) {
                const hostName = projectHostMap.get(changedProject);
                if (hostName) {
                    changedProjectNames.add(hostName);
                }
            }
        }
        return Array.from(changedProjectNames);
    }
    async _validateChangeFileAsync(changedPackages) {
        const files = await this._getChangeFilesAsync();
        ChangeFiles_1.ChangeFiles.validate(files, changedPackages, this.rushConfiguration);
    }
    async _getChangeFilesAsync() {
        const repoRoot = (0, package_deps_hash_1.getRepoRoot)(this.rushConfiguration.rushJsonFolder);
        const relativeChangesFolder = path.relative(repoRoot, this.rushConfiguration.changesFolder);
        const targetBranch = await this._getTargetBranchAsync();
        const changedFiles = await this._git.getChangedFilesAsync(targetBranch, this.terminal, true, relativeChangesFolder);
        const result = [];
        for (const changedFile of changedFiles) {
            result.push(path.join(repoRoot, changedFile));
        }
        return result;
    }
    /**
     * The main loop which prompts the user for information on changed projects.
     */
    async _promptForChangeFileDataAsync(promptModule, sortedProjectList, existingChangeComments) {
        const changedFileData = new Map();
        for (const projectName of sortedProjectList) {
            const changeInfo = await this._askQuestionsAsync(promptModule, projectName, existingChangeComments);
            if (changeInfo) {
                // Save the info into the change file
                let changeFile = changedFileData.get(changeInfo.packageName);
                if (!changeFile) {
                    changeFile = {
                        changes: [],
                        packageName: changeInfo.packageName,
                        email: undefined
                    };
                    changedFileData.set(changeInfo.packageName, changeFile);
                }
                changeFile.changes.push(changeInfo);
            }
        }
        return changedFileData;
    }
    /**
     * Asks all questions which are needed to generate changelist for a project.
     */
    async _askQuestionsAsync(promptModule, packageName, existingChangeComments) {
        // eslint-disable-next-line no-console
        console.log(`\n${packageName}`);
        const comments = existingChangeComments.get(packageName);
        if (comments) {
            // eslint-disable-next-line no-console
            console.log(`Found existing comments:`);
            comments.forEach((comment) => {
                // eslint-disable-next-line no-console
                console.log(`    > ${comment}`);
            });
            const { appendComment } = await promptModule({
                name: 'appendComment',
                type: 'list',
                default: 'skip',
                message: 'Append to existing comments or skip?',
                choices: [
                    {
                        name: 'Skip',
                        value: 'skip'
                    },
                    {
                        name: 'Append',
                        value: 'append'
                    }
                ]
            });
            if (appendComment === 'skip') {
                return undefined;
            }
            else {
                return await this._promptForCommentsAsync(promptModule, packageName);
            }
        }
        else {
            return await this._promptForCommentsAsync(promptModule, packageName);
        }
    }
    async _promptForCommentsAsync(promptModule, packageName) {
        const bumpOptions = this._getBumpOptions(packageName);
        const { comment } = await promptModule({
            name: 'comment',
            type: 'input',
            message: `Describe changes, or ENTER if no changes:`
        });
        if (Object.keys(bumpOptions).length === 0 || !comment) {
            return {
                packageName: packageName,
                comment: comment || '',
                type: ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.none]
            };
        }
        else {
            const { bumpType } = await promptModule({
                choices: Object.keys(bumpOptions).map((option) => {
                    return {
                        value: option,
                        name: bumpOptions[option]
                    };
                }),
                default: 'patch',
                message: 'Select the type of change:',
                name: 'bumpType',
                type: 'list'
            });
            return {
                packageName: packageName,
                comment: comment,
                type: bumpType
            };
        }
    }
    _getBumpOptions(packageName) {
        let bumpOptions = this.rushConfiguration && this.rushConfiguration.hotfixChangeEnabled
            ? {
                [ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.hotfix]]: 'hotfix - for changes that need to be published in a separate hotfix package'
            }
            : {
                [ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.major]]: 'major - for changes that break compatibility, e.g. removing an API',
                [ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.minor]]: 'minor - for backwards compatible changes, e.g. adding a new API',
                [ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.patch]]: 'patch - for changes that do not affect compatibility, e.g. fixing a bug',
                [ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.none]]: 'none - for changes that do not need an immediate release, e.g. eslint config change'
            };
        if (packageName) {
            const project = this.rushConfiguration.getProjectByName(packageName);
            const versionPolicy = project.versionPolicy;
            if (versionPolicy) {
                if (versionPolicy.definitionName === VersionPolicy_1.VersionPolicyDefinitionName.lockStepVersion) {
                    const lockStepPolicy = versionPolicy;
                    // No need to ask for bump types if project is lockstep versioned with an explicit nextBump
                    if (lockStepPolicy.nextBump !== undefined) {
                        bumpOptions = {};
                    }
                }
                else if (versionPolicy.definitionName === VersionPolicy_1.VersionPolicyDefinitionName.individualVersion) {
                    const individualPolicy = versionPolicy;
                    if (individualPolicy.lockedMajor !== undefined) {
                        delete bumpOptions[ChangeManagement_1.ChangeType[ChangeManagement_1.ChangeType.major]];
                    }
                }
            }
        }
        return bumpOptions;
    }
    _isEmailRequired(changeFileData) {
        return [...changeFileData.values()].some((changeFile) => {
            var _a, _b;
            return !!((_b = (_a = this.rushConfiguration.getProjectByName(changeFile.packageName)) === null || _a === void 0 ? void 0 : _a.versionPolicy) === null || _b === void 0 ? void 0 : _b.includeEmailInChangeFile);
        });
    }
    /**
     * Will determine a user's email by first detecting it from their Git config,
     * or will ask for it if it is not found or the Git config is wrong.
     */
    async _detectOrAskForEmailAsync(promptModule) {
        return ((await this._detectAndConfirmEmailAsync(promptModule)) ||
            (await this._promptForEmailAsync(promptModule)));
    }
    _detectEmail() {
        try {
            return child_process
                .execSync('git config user.email')
                .toString()
                .replace(/(\r\n|\n|\r)/gm, '');
        }
        catch (err) {
            // eslint-disable-next-line no-console
            console.log('There was an issue detecting your Git email...');
            return undefined;
        }
    }
    /**
     * Detects the user's email address from their Git configuration, prompts the user to approve the
     * detected email. It returns undefined if it cannot be detected.
     */
    async _detectAndConfirmEmailAsync(promptModule) {
        const email = this._detectEmail();
        if (email) {
            const { isCorrectEmail } = await promptModule([
                {
                    type: 'confirm',
                    name: 'isCorrectEmail',
                    default: 'Y',
                    message: `Is your email address ${email}?`
                }
            ]);
            return isCorrectEmail ? email : undefined;
        }
        else {
            return undefined;
        }
    }
    /**
     * Asks the user for their email address
     */
    async _promptForEmailAsync(promptModule) {
        const { email } = await promptModule([
            {
                type: 'input',
                name: 'email',
                message: 'What is your email address?',
                validate: (input) => {
                    return true; // @todo should be an email
                }
            }
        ]);
        return email;
    }
    async _warnUnstagedChangesAsync() {
        try {
            const hasUnstagedChanges = await this._git.hasUnstagedChangesAsync();
            if (hasUnstagedChanges) {
                // eslint-disable-next-line no-console
                console.log('\n' +
                    terminal_1.Colorize.yellow('Warning: You have unstaged changes, which do not trigger prompting for change ' +
                        'descriptions.'));
            }
        }
        catch (error) {
            // eslint-disable-next-line no-console
            console.log(`An error occurred when detecting unstaged changes: ${error}`);
        }
    }
    /**
     * Writes change files to the common/changes folder. Will prompt for overwrite if file already exists.
     */
    async _writeChangeFilesAsync(promptModule, changeFileData, overwrite, interactiveMode) {
        const writtenFiles = [];
        await changeFileData.forEach(async (changeFile) => {
            const writtenFile = await this._writeChangeFileAsync(promptModule, changeFile, overwrite, interactiveMode);
            if (writtenFile) {
                writtenFiles.push(writtenFile);
            }
        });
        return writtenFiles;
    }
    async _writeChangeFileAsync(promptModule, changeFileData, overwrite, interactiveMode) {
        const output = JSON.stringify(changeFileData, undefined, 2);
        const changeFile = new ChangeFile_1.ChangeFile(changeFileData, this.rushConfiguration);
        const filePath = changeFile.generatePath();
        const fileExists = node_core_library_1.FileSystem.exists(filePath);
        const shouldWrite = !fileExists ||
            overwrite ||
            (interactiveMode ? await this._promptForOverwriteAsync(promptModule, filePath) : false);
        if (!interactiveMode && fileExists && !overwrite) {
            throw new Error(`Changefile ${filePath} already exists`);
        }
        if (shouldWrite) {
            this._writeFile(filePath, output, shouldWrite && fileExists);
            return filePath;
        }
    }
    async _promptForOverwriteAsync(promptModule, filePath) {
        const overwrite = await promptModule([
            {
                name: 'overwrite',
                type: 'confirm',
                message: `Overwrite ${filePath}?`
            }
        ]);
        if (overwrite) {
            return true;
        }
        else {
            // eslint-disable-next-line no-console
            console.log(`Not overwriting ${filePath}`);
            return false;
        }
    }
    /**
     * Writes a file to disk, ensuring the directory structure up to that point exists
     */
    _writeFile(fileName, output, isOverwrite) {
        node_core_library_1.FileSystem.writeFile(fileName, output, { ensureFolderExists: true });
        if (isOverwrite) {
            // eslint-disable-next-line no-console
            console.log(`Overwrote file: ${fileName}`);
        }
        else {
            // eslint-disable-next-line no-console
            console.log(`Created file: ${fileName}`);
        }
    }
    _logNoChangeFileRequired() {
        // eslint-disable-next-line no-console
        console.log('No changes were detected to relevant packages on this branch. Nothing to do.');
    }
}
exports.ChangeAction = ChangeAction;
//# sourceMappingURL=ChangeAction.js.map