#!/usr/bin/env node

const { execSync } = require('child_process');
const conventionalCommitsParser = require('conventional-commits-parser');
const chalk = require('chalk');
const { version } = require('../package.json');

const releaseType = process.argv[2];
let ignoreCommits = process.argv[3];

if (!releaseType) {
  console.error(
    'Must specify a release type:\n$ node build/cherry-pick.js [<releaseType> | minor | patch] [<ignoreCommits>]'
  );
  process.exit(1);
}

// doing a major release should just be merging develop into master,
// no cherry-picking required
if (!['minor', 'patch'].includes(releaseType)) {
  console.error('Release type not supported:', releaseType);
  process.exit(1);
}

if (ignoreCommits) {
  // use the short hash if a long hash is provided
  ignoreCommits = ignoreCommits.split(',').map(hash => hash.substring(0, 8));
  console.log(chalk.yellow('Ignoring commits:'), ignoreCommits.join(','), '\n');
} else {
  ignoreCommits = [];
}

// don't run on master or develop branch
const currentBranch = execSync('git branch --show-current').toString().trim();
if (['develop', 'master'].includes(currentBranch)) {
  console.error(
    `Please run the script on a release branch and not the ${currentBranch} branch`
  );
  process.exit(1);
}

// find the version we should start pulling commits from
// e.g. if we are currently on version 3.4.5 and need to do a minor release we pull commits starting from 3.4.0
const [major, minor] = version.split('.').map(Number);

let targetVersion = releaseType === 'patch' ? version : `${major}.${minor}.0`;

// get all commits from a branch
function getCommits(branch) {
  // all commits are too large for execSync buffer size so we'll just get since the last 3 years
  const date = new Date(new Date().setFullYear(new Date().getFullYear() - 3));
  const stdout = execSync(
    `git log ${branch || ''} --abbrev-commit --since=${date.getFullYear()}`
  ).toString();
  const allCommits = stdout
    .split(/commit (?=[\w\d]{8}[\n\r])/)
    .filter(commit => !!commit);

  // parse commits
  const commits = [];
  for (let i = 0; i < allCommits.length; i++) {
    const commit = allCommits[i];

    const hash = commit.substring(0, 8);
    const msg = commit.substring(commit.indexOf('\n\n')).trim();

    const { type, scope, subject, merge, notes } =
      conventionalCommitsParser.sync(msg, {
        // parse merge commits
        mergePattern: /^Merge pull request #(\d+) from (.*)$/,
        mergeCorrespondence: ['id', 'source'],

        // allow comma in scope
        headerPattern: /^(\w*)(?:\(([\w\$\.\-\*, ]*)\))?\: (.*)$/
      });

    const isBreakingChange = notes.some(
      note => note.title === 'BREAKING CHANGE'
    );
    const isFeat = type === 'feat';

    const commitType = {
      minor: !isBreakingChange,
      patch: !isFeat && !isBreakingChange
    };

    // only get commits since the target version
    // example commit message generated by running the release script:
    // chore(release): 3.4.5
    if (scope === 'release' && subject === targetVersion) {
      break;
    }

    // filter merge commits and any types that don't match the release type
    if (
      !merge &&
      subject &&
      !subject.startsWith('merge branch') &&
      scope !== 'release' &&
      commitType[releaseType] &&
      !ignoreCommits.includes(hash)
    ) {
      // add in reverse order (order commited)
      commits.unshift({ hash, msg, type, scope, subject });
    }
  }

  return commits;
}

// only cherry-pick commits that have not been added to the current branch already
const currentCommits = getCommits();
const commitsToCherryPick = getCommits('develop').filter(commit => {
  return !currentCommits.find(
    currentCommit => currentCommit.subject === commit.subject
  );
});

if (!commitsToCherryPick.length) {
  console.log(chalk.yellow('No commits to cherry-pick'));
  process.exit(1);
}

// cherry-pick all commits and accept whatever is in develop to avoid merge conflicts
commitsToCherryPick.forEach(({ hash, type, scope, subject }) => {
  console.log(
    `${chalk.yellow('Cherry-picking')} ${hash} ${type}${
      scope ? `(${scope})` : ''
    }: ${subject}`
  );

  try {
    execSync(`git cherry-pick ${hash} -X theirs`);
  } catch {
    console.error(
      chalk.red.bold('\nAborting cherry-pick and reseting to master')
    );
    console.error(
      '\nCannot auto-resolve cherry-pick commit. This can be caused by the commit already being applied or a file being edited in the cherry-pick branch that does not yet exist (the commit it depends on was not cherry-picked). Please review the commits being cherry-picked and either manually resolve or ignore the commit.'
    );
    console.error(
      `$ node build/cherry-pick.js ${releaseType} [commitSHA1,commitSHA2,...]`
    );

    execSync('git cherry-pick --abort; git reset --hard origin/master');
    process.exit(1);
  }
});
