@ 261105.4 · fetching
current
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;