import path from 'node:path';
import { getMetadata } from '../components/metadata.js';
import {
runAsync, runSync
} from './run.js';
import { getNcuDir } from './config.js';
import LandingSession, { LINT_RESULTS } from './landing_session.js';
export default class CherryPick {
constructor(prid, dir, cli, {
owner,
repo,
upstream,
gpgSign,
lint,
includeCVE,
cveIds,
vulnCveMap,
promptAmend,
skipMessagePrompt
} = {}) {
this.prid = prid;
this.cli = cli;
this.dir = dir;
this.upstream = upstream;
this.gpgSign = gpgSign;
this.skipMessagePrompt = skipMessagePrompt ?? false;
this.options = {
owner, repo, lint, includeCVE, cveIds, vulnCveMap, promptAmend
};
}
get includeCVE() {
return this.options.includeCVE ?? false;
}
// Whether to ask before amending the PR to the proposal. Callers that already
// asked opt out of it.
get promptAmend() {
return this.options.promptAmend ?? true;
}
get cveIds() {
return this.options.cveIds ?? null;
}
get vulnCveMap() {
return this.options.vulnCveMap ?? null;
}
get owner() {
return this.options.owner || 'nodejs';
}
get repo() {
return this.options.repo || 'node';
}
get lint() {
return this.options.lint;
}
getCurrentRev() {
return runSync('git', ['rev-parse', 'HEAD']).trim();
}
getStrayCommits(verbose) {
const { upstream, branch } = this;
const ref = `${upstream}/${branch}...HEAD`;
const gitCmd = verbose
? ['log', '--oneline', '--reverse', ref]
: ['rev-list', '--reverse', ref];
const revs = runSync('git', gitCmd).trim();
return revs ? revs.split('\n') : [];
}
get ncuDir() {
return getNcuDir(this.dir);
}
get pullDir() {
return path.resolve(this.ncuDir, `${this.prid}`);
}
async start() {
const { cli } = this;
const metadata = await getMetadata({
prid: this.prid,
owner: this.owner,
repo: this.repo
}, false, cli);
this.expectedCommitShas =
metadata.data.commits.map(({ commit }) => commit.oid);
// In security releases the fix may have already landed publicly, so the
// PR is merged and refs/pull/<prid>/merge no longer exists - pick the
// commits as they landed on the base branch instead.
this.landedCommitSha = metadata.data.pr.merged
? metadata.data.pr.mergeCommit?.oid
: undefined;
if (this.promptAmend) {
const amend = await cli.prompt(
'Would you like to amend this PR to the proposal?',
{ default: true }
);
if (!amend) {
return true;
}
}
try {
const commitInfo = await this.downloadAndPatch();
const cleanLint = await this.validateLint();
if (cleanLint === LINT_RESULTS.FAILED) {
cli.error('Patch still contains lint errors. ' +
'Please fix manually before proceeding');
return false;
} else if (cleanLint === LINT_RESULTS.SUCCESS) {
cli.ok('Lint passed cleanly');
}
this.metadata = metadata.metadata;
return this.amend(commitInfo);
} catch (e) {
cli.error(e.message);
return false;
}
}
async amend(commitInfo) {
const { cli } = this;
const subjects = await runAsync('git',
['log', '--pretty=format:%s', `${commitInfo.base}..${commitInfo.head}`],
{ captureStdout: 'lines' });
if (commitInfo.shas.length !== 1) {
const fixupAll = this.skipMessagePrompt || await cli.prompt(
`${subjects.length} commits from the original PR are going to be` +
'squashed into a single commit. OK to proceed?', {
defaultAnswer: true
});
if (!fixupAll) {
// TODO: add this support?
throw new Error(`There are ${subjects.length} commits in the PR ` +
'and the ammend were not able to succeed');
}
await runAsync('git', ['reset', '--soft', `HEAD~${subjects.length - 1}`]);
await runAsync('git', ['commit', '--amend', '--no-edit']);
}
return LandingSession.prototype.amend.call(this);
}
readyToAmend() {
return true;
}
startAmending() {
// No-op
}
saveCommitInfo() {
// No-op
}
}
CherryPick.prototype.downloadAndPatch = LandingSession.prototype.downloadAndPatch;
CherryPick.prototype.validateLint = LandingSession.prototype.validateLint;
CherryPick.prototype.getMessagePath = LandingSession.prototype.getMessagePath;
CherryPick.prototype.saveMessage = LandingSession.prototype.saveMessage;
CherryPick.prototype.generateAmendedMessage =
LandingSession.prototype.generateAmendedMessage;