backport
Version:
A CLI tool that automates the process of backporting commits
173 lines • 7.63 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.waitForCherrypick = void 0;
const chalk_1 = __importDefault(require("chalk"));
const lodash_1 = require("lodash");
const entrypoint_api_1 = require("../../entrypoint.api");
const ora_1 = require("../../lib/ora");
const author_1 = require("../author");
const child_process_promisified_1 = require("../child-process-promisified");
const env_1 = require("../env");
const git_1 = require("../git");
const commitFormatters_1 = require("../github/commitFormatters");
const logger_1 = require("../logger");
const prompts_1 = require("../prompts");
const getCommitsWithoutBackports_1 = require("./getCommitsWithoutBackports");
async function waitForCherrypick(options, commit, targetBranch) {
const spinnerText = `Cherry-picking: ${chalk_1.default.greenBright((0, commitFormatters_1.getFirstLine)(commit.sourceCommit.message))}`;
const cherrypickSpinner = (0, ora_1.ora)(options.interactive, spinnerText).start();
const commitAuthor = (0, author_1.getCommitAuthor)({ options, commit });
const { hasCommitsWithConflicts } = await cherrypickAndHandleConflicts({
options,
commit,
commitAuthor,
targetBranch,
cherrypickSpinner,
});
// At this point conflict are resolved (or committed if `commitConflicts: true`) and files are staged
// Now we just need to commit them (user may already have done this manually)
try {
// Run `git commit` in case conflicts were not manually committed
await (0, git_1.commitChanges)({ options, commit, commitAuthor });
cherrypickSpinner.succeed();
return { hasCommitsWithConflicts };
}
catch (e) {
cherrypickSpinner.fail();
throw e;
}
}
exports.waitForCherrypick = waitForCherrypick;
async function cherrypickAndHandleConflicts({ options, commit, commitAuthor, targetBranch, cherrypickSpinner, }) {
const mergedTargetPullRequest = commit.targetPullRequestStates.find((pr) => pr.state === 'MERGED' && pr.branch === targetBranch);
let conflictingFiles;
let unstagedFiles;
let needsResolving;
try {
({ conflictingFiles, unstagedFiles, needsResolving } = await (0, git_1.cherrypick)({
options,
sha: commit.sourceCommit.sha,
mergedTargetPullRequest,
commitAuthor,
}));
// no conflicts encountered
if (!needsResolving) {
return { hasCommitsWithConflicts: false };
}
// cherrypick failed due to conflicts
cherrypickSpinner.fail();
}
catch (e) {
cherrypickSpinner.fail();
throw e;
}
const repoPath = (0, env_1.getRepoPath)(options);
// resolve conflicts automatically
if (options.autoFixConflicts) {
const autoResolveSpinner = (0, ora_1.ora)(options.interactive, 'Attempting to resolve conflicts automatically').start();
const didAutoFix = await options.autoFixConflicts({
files: conflictingFiles.map((f) => f.absolute),
directory: repoPath,
logger: logger_1.logger,
targetBranch,
});
// conflicts were automatically resolved
if (didAutoFix) {
autoResolveSpinner.succeed();
return { hasCommitsWithConflicts: false };
}
autoResolveSpinner.fail();
}
// commits with conflicts should be committed and pushed to the target branch
if (!options.interactive && options.commitConflicts) {
await (0, git_1.gitAddAll)({ options });
await (0, git_1.commitChanges)({ options, commit, commitAuthor });
return { hasCommitsWithConflicts: true };
}
const conflictingFilesRelative = conflictingFiles
.map((f) => f.relative)
.slice(0, 50);
let commitsWithoutBackports;
try {
commitsWithoutBackports = await (0, getCommitsWithoutBackports_1.getCommitsWithoutBackports)({
options,
commit,
targetBranch,
conflictingFiles: conflictingFilesRelative,
});
}
catch (e) {
commitsWithoutBackports = [];
if (e instanceof Error) {
logger_1.logger.warn(`Unable to fetch commits without backports: ${e.message}`);
}
}
if (!options.interactive) {
throw new entrypoint_api_1.BackportError({
code: 'merge-conflict-exception',
commitsWithoutBackports,
conflictingFiles: conflictingFilesRelative,
});
}
(0, logger_1.consoleLog)(chalk_1.default.bold('\nThe commit could not be backported due to conflicts\n'));
(0, logger_1.consoleLog)(`Please fix the conflicts in ${repoPath}`);
if (commitsWithoutBackports.length > 0) {
(0, logger_1.consoleLog)(chalk_1.default.italic(`Hint: Before fixing the conflicts manually you should consider backporting the following pull requests to "${targetBranch}":`));
(0, logger_1.consoleLog)(`${commitsWithoutBackports.map((c) => c.formatted).join('\n')}\n\n`);
}
/*
* Commit could not be cleanly cherrypicked: Initiating conflict resolution
*/
if (options.editor) {
await (0, child_process_promisified_1.spawnPromise)(options.editor, [repoPath], options.cwd, true);
}
// list files with conflict markers + unstaged files and require user to resolve them
await listConflictingAndUnstagedFiles({
retries: 0,
options,
conflictingFiles: conflictingFiles.map((f) => f.absolute),
unstagedFiles,
});
return { hasCommitsWithConflicts: false };
}
async function listConflictingAndUnstagedFiles({ retries, options, conflictingFiles, unstagedFiles, }) {
const hasUnstagedFiles = !(0, lodash_1.isEmpty)((0, lodash_1.difference)(unstagedFiles, conflictingFiles));
const hasConflictingFiles = !(0, lodash_1.isEmpty)(conflictingFiles);
if (!hasConflictingFiles && !hasUnstagedFiles) {
return;
}
// add divider between prompts
if (retries > 0) {
(0, logger_1.consoleLog)('\n----------------------------------------\n');
}
const header = chalk_1.default.reset(`Fix the following conflicts manually:`);
// show conflict section if there are conflicting files
const conflictSection = hasConflictingFiles
? `Conflicting files:\n${chalk_1.default.reset(conflictingFiles.map((file) => ` - ${file}`).join('\n'))}`
: '';
const unstagedSection = hasUnstagedFiles
? `Unstaged files:\n${chalk_1.default.reset(unstagedFiles.map((file) => ` - ${file}`).join('\n'))}`
: '';
const didConfirm = await (0, prompts_1.confirmPrompt)(`${header}\n\n${conflictSection}\n${unstagedSection}\n\nPress ENTER when the conflicts are resolved and files are staged`);
if (!didConfirm) {
throw new entrypoint_api_1.BackportError({ code: 'abort-conflict-resolution-exception' });
}
const MAX_RETRIES = 100;
if (retries++ > MAX_RETRIES) {
throw new Error(`Maximum number of retries (${MAX_RETRIES}) exceeded`);
}
const [_conflictingFiles, _unstagedFiles] = await Promise.all([
(0, git_1.getConflictingFiles)(options),
(0, git_1.getUnstagedFiles)(options),
]);
await listConflictingAndUnstagedFiles({
retries,
options,
conflictingFiles: _conflictingFiles.map((file) => file.absolute),
unstagedFiles: _unstagedFiles,
});
}
//# sourceMappingURL=waitForCherrypick.js.map