From 908c669f701d7145cda5102183068971d2b7222e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 16 Apr 2020 01:17:15 +0200 Subject: [PATCH] Pre-select branches based on PR labels (#180) --- docs/configuration.md | 34 ++++-- jest.config.js | 5 +- package.json | 8 +- src/__snapshots__/runWithOptions.test.ts.snap | 26 +++- src/index.ts | 1 - .../__snapshots__/options.test.ts.snap | 22 ++-- src/options/cliArgs.test.ts | 6 +- src/options/cliArgs.ts | 6 +- src/options/config/config.test.ts | 4 +- src/options/config/config.ts | 8 +- src/options/config/projectConfig.test.ts | 2 +- src/options/options.test.ts | 23 ++-- src/options/options.ts | 25 ++-- src/runWithOptions.test.ts | 7 +- src/runWithOptions.ts | 14 ++- src/services/git.test.ts | 9 +- src/services/git.ts | 8 +- .../github/v3/fetchCommitBySha.test.ts | 3 +- src/services/github/v3/fetchCommitBySha.ts | 3 +- .../fetchCommitsByAuthor.test.ts.snap | 7 +- .../github/v4/fetchCommitByPullNumber.test.ts | 113 ++++++++++++++++++ .../github/v4/fetchCommitByPullNumber.ts | 25 +++- .../github/v4/fetchCommitsByAuthor.test.ts | 47 ++++++-- .../github/v4/fetchCommitsByAuthor.ts | 32 ++++- .../github/v4/getTargetBranchesFromLabels.ts | 30 +++++ .../github/v4/mocks/commitsByAuthorMock.ts | 6 + .../github/v4/mocks/getCommitsByAuthorMock.ts | 3 + .../github/v4/mocks/getPullRequestEdgeMock.ts | 3 + src/services/prompts.ts | 36 ++++-- src/test/getDefaultOptions.ts | 14 --- .../__snapshots__/integration.test.ts.snap | 31 ++++- src/test/yargs.test.ts | 20 ++-- src/types/Commit.d.ts | 3 +- src/types/Config.d.ts | 1 + ...herrypickAndCreatePullRequest.test.ts.snap | 4 +- src/ui/cherrypickAndCreatePullRequest.test.ts | 28 +++-- src/ui/cherrypickAndCreatePullRequest.ts | 30 ++--- src/ui/getBranches.test.ts | 49 +++++--- src/ui/getBranches.ts | 57 +++++++-- src/ui/getCommitBySha.test.ts | 9 +- src/ui/getCommits.ts | 5 +- src/utils/filterEmpty.ts | 5 + tsconfig.json | 4 +- yarn.lock | 67 ++++++++--- 44 files changed, 621 insertions(+), 222 deletions(-) create mode 100644 src/services/github/v4/fetchCommitByPullNumber.test.ts create mode 100644 src/services/github/v4/getTargetBranchesFromLabels.ts delete mode 100644 src/test/getDefaultOptions.ts create mode 100644 src/utils/filterEmpty.ts diff --git a/docs/configuration.md b/docs/configuration.md index f33b93fa..64548ac2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -158,17 +158,27 @@ Config: #### `prTitle` -Pull request title pattern. You can access the base branch (`{baseBranch}`) and commit message (`{commitMessages}`) via the special accessors in quotes. -Multiple commits will be concatenated and separated by pipes. +Pull request title pattern. +Template values: -Default: `"[{baseBranch}] {commitMessages}"` +- `{targetBranch}`: Branch the backport PR will be targeting +- `{commitMessages}`: Multiple commits will be concatenated and separated by pipes (`|`). -CLI: `--pr-title "{commitMessages} backport for {baseBranch}"` +Default: `"[{targetBranch}] {commitMessages}"` + +CLI: `--pr-title "{commitMessages} backport for {targetBranch}"` + +Config: + +```json +{ + "prTitle": "{commitMessages} backport for {targetBranch}" +} +``` #### `prDescription` -Pull request description. -Will be added to the end of the pull request description. +Text that will be appended to the pull request description. For people who often need to add the same description to PRs they can create a bash alias: @@ -178,6 +188,14 @@ alias backport-skip-ci='backport --prDescription "[skip-ci]"' CLI: `--pr-description "skip-ci"` +Config: + +```json +{ + "prDescription": "skip-ci" +} +``` + #### `sourceBranch` By default the list of commits will be sourced from the repository's default branch (mostly "master"). Use `sourceBranch` to list and backport commits from other branches than the default. @@ -204,7 +222,7 @@ CLI: `--git-hostname "github.my-private-company.com"` #### `githubApiBaseUrlV3` -Base url for Github's Rest (v3) API +Base url for Github's REST (v3) API Default: `https://api.github.com` @@ -212,7 +230,7 @@ CLI: `--github-api-base-url-v3 "https://api.github.my-private-company.com"` #### `githubApiBaseUrlV4` -Base url for Github's Rest (v3) API +Base url for Github's GraphQL (v4) API Default: `https://api.github.com/graphql` diff --git a/jest.config.js b/jest.config.js index ed494d37..6530cc89 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,11 +1,8 @@ module.exports = { snapshotSerializers: ['jest-snapshot-serializer-ansi'], setupFiles: ['./src/test/setupFiles/automatic-mocks.ts'], - transform: { - '^.+\\.tsx?$': 'ts-jest', - }, + preset: 'ts-jest', testRegex: '(test|src)/.*test.ts$', - moduleFileExtensions: ['ts', 'js', 'json'], globals: { 'ts-jest': { diagnostics: false, diff --git a/package.json b/package.json index 29b993ae..50f2fa22 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "del": "^5.1.0", "find-up": "^4.1.0", "inquirer": "^7.1.0", + "lodash.flatmap": "^4.5.0", "lodash.isempty": "^4.4.0", "lodash.isstring": "^4.0.1", "lodash.uniq": "^4.5.0", @@ -83,6 +84,7 @@ "@types/inquirer": "^6.5.0", "@types/jest": "^25.2.1", "@types/lodash": "^4.14.144", + "@types/lodash.flatmap": "^4.5.6", "@types/lodash.isempty": "^4.4.6", "@types/lodash.isstring": "^4.0.6", "@types/lodash.uniq": "^4.5.6", @@ -90,13 +92,13 @@ "@types/safe-json-stringify": "^1.1.0", "@types/yargs": "^15.0.4", "@types/yargs-parser": "^15.0.0", - "@typescript-eslint/eslint-plugin": "^2.27.0", - "@typescript-eslint/parser": "^2.27.0", + "@typescript-eslint/eslint-plugin": "^2.28.0", + "@typescript-eslint/parser": "^2.28.0", "eslint": "^6.8.0", "eslint-config-prettier": "^6.10.1", "eslint-plugin-import": "^2.20.2", "eslint-plugin-jest": "^23.8.2", - "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-prettier": "^3.1.3", "husky": "^4.2.3", "jest": "^25.3.0", "jest-snapshot-serializer-ansi": "^1.0.0", diff --git a/src/__snapshots__/runWithOptions.test.ts.snap b/src/__snapshots__/runWithOptions.test.ts.snap index 0f529291..778984db 100644 --- a/src/__snapshots__/runWithOptions.test.ts.snap +++ b/src/__snapshots__/runWithOptions.test.ts.snap @@ -88,29 +88,30 @@ Array [ "name": "1. Add 👻 (2e63475c) ", "short": "Add 👻 (2e63475c)", "value": Object { - "branch": "mySourceBranch", "existingBackports": Array [], "formattedMessage": "Add 👻 (2e63475c)", "pullNumber": undefined, "sha": "2e63475c483f7844b0f2833bc57fdee32095bacb", + "sourceBranch": "mySourceBranch", + "targetBranches": Array [], }, }, Object { "name": "2. Add witch (#85) ", "short": "Add witch (#85)", "value": Object { - "branch": "mySourceBranch", "existingBackports": Array [], "formattedMessage": "Add witch (#85)", "pullNumber": 85, "sha": "f3b618b9421fdecdb36862f907afbdd6344b361d", + "sourceBranch": "mySourceBranch", + "targetBranches": Array [], }, }, Object { "name": "3. Add SF mention (#80) 6.3", "short": "Add SF mention (#80)", "value": Object { - "branch": "mySourceBranch", "existingBackports": Array [ Object { "branch": "6.3", @@ -120,34 +121,42 @@ Array [ "formattedMessage": "Add SF mention (#80)", "pullNumber": 80, "sha": "79cf18453ec32a4677009dcbab1c9c8c73fc14fe", + "sourceBranch": "mySourceBranch", + "targetBranches": Array [], }, }, Object { "name": "4. Add backport config (3827bbba) ", "short": "Add backport config (3827bbba)", "value": Object { - "branch": "mySourceBranch", "existingBackports": Array [], "formattedMessage": "Add backport config (3827bbba)", "pullNumber": undefined, "sha": "3827bbbaf39914eda4f02f6940189844375fd097", + "sourceBranch": "mySourceBranch", + "targetBranches": Array [], }, }, Object { "name": "5. Initial commit (5ea0da55) ", "short": "Initial commit (5ea0da55)", "value": Object { - "branch": "mySourceBranch", "existingBackports": Array [], "formattedMessage": "Initial commit (5ea0da55)", "pullNumber": undefined, "sha": "5ea0da550ac191029459289d67f99ad7d310812b", + "sourceBranch": "mySourceBranch", + "targetBranches": Array [], }, }, + Separator { + "line": "──────────────", + "type": "separator", + }, ], "message": "Select commit to backport", "name": "promptResult", - "pageSize": 5, + "pageSize": 15, "type": "list", }, ], @@ -171,9 +180,14 @@ Array [ Object { "name": "5.4", }, + Separator { + "line": "──────────────", + "type": "separator", + }, ], "message": "Select branch to backport to", "name": "promptResult", + "pageSize": 15, "type": "list", }, ], diff --git a/src/index.ts b/src/index.ts index b798cb04..e8c9e6af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ #!/usr/bin/env node - import { runWithArgs } from './runWithArgs'; const args = process.argv.slice(2); diff --git a/src/options/__snapshots__/options.test.ts.snap b/src/options/__snapshots__/options.test.ts.snap index b8e8b3a8..81f06399 100644 --- a/src/options/__snapshots__/options.test.ts.snap +++ b/src/options/__snapshots__/options.test.ts.snap @@ -14,27 +14,27 @@ It must contain a valid \\"username\\" and \\"accessToken\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#global-config-backportconfigjson" `; -exports[`validateRequiredOptions should throw when both branches and branchChoices are missing 1`] = ` -"Invalid option \\"branches\\" +exports[`validateRequiredOptions should throw when both branches and targetBranchChoices are missing 1`] = ` +"You must specify a target branch -You can add it with either: - - Config file: \\".backportrc.json\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#project-config-backportrcjson - - CLI: \\"--branches 6.1\\"" +You can specify it via either: + - Config file (recommended): \\".backportrc.json\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#project-config-backportrcjson + - CLI: \\"--branch 6.1\\"" `; exports[`validateRequiredOptions should throw when upstream is empty 1`] = ` -"Invalid option \\"upstream\\" +"You must specify a valid Github repository -You can add it with either: - - Config file: \\".backportrc.json\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#project-config-backportrcjson +You can specify it via either: + - Config file (recommended): \\".backportrc.json\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#project-config-backportrcjson - CLI: \\"--upstream elastic/kibana\\"" `; exports[`validateRequiredOptions should throw when upstream is missing 1`] = ` -"Invalid option \\"upstream\\" +"You must specify a valid Github repository -You can add it with either: - - Config file: \\".backportrc.json\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#project-config-backportrcjson +You can specify it via either: + - Config file (recommended): \\".backportrc.json\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#project-config-backportrcjson - CLI: \\"--upstream elastic/kibana\\"" `; diff --git a/src/options/cliArgs.test.ts b/src/options/cliArgs.test.ts index 39cf60e0..cf8850ad 100644 --- a/src/options/cliArgs.test.ts +++ b/src/options/cliArgs.test.ts @@ -9,7 +9,7 @@ describe('getOptionsFromCliArgs', () => { githubApiBaseUrlV3: 'https://api.github.com', githubApiBaseUrlV4: 'https://api.github.com/graphql', backportCreatedLabels: [], - branchChoices: [], + targetBranchChoices: [], fork: true, gitHostname: 'github.com', labels: [], @@ -41,8 +41,8 @@ describe('getOptionsFromCliArgs', () => { githubApiBaseUrlV3: 'https://api.github.com', githubApiBaseUrlV4: 'https://api.github.com/graphql', backportCreatedLabels: [], - branches: ['6.0', '6.1'], - branchChoices: [], + targetBranches: ['6.0', '6.1'], + targetBranchChoices: [], fork: true, gitHostname: 'github.com', labels: [], diff --git a/src/options/cliArgs.ts b/src/options/cliArgs.ts index 3d5714b5..1326cdfc 100644 --- a/src/options/cliArgs.ts +++ b/src/options/cliArgs.ts @@ -33,7 +33,7 @@ export function getOptionsFromCliArgs( description: 'Pull request labels for the original PR', type: 'array', }) - .option('branches', { + .option('targetBranches', { default: [] as string[], description: 'Branch(es) to backport to', type: 'array', @@ -43,6 +43,7 @@ export function getOptionsFromCliArgs( .option('commitsCount', { default: configOptions.commitsCount, description: 'Number of commits to choose from', + alias: 'count', type: 'number', }) .option('editor', { @@ -170,7 +171,8 @@ export function getOptionsFromCliArgs( return { ...rest, accessToken: cliArgs.accessToken || configOptions.accessToken, - branchChoices: configOptions.branchChoices, + targetBranchChoices: configOptions.targetBranchChoices, // not available as cli argument + branchLabelMapping: configOptions.branchLabelMapping, // not available as cli argument multipleBranches: cliArgs.multipleBranches || cliArgs.multiple, multipleCommits: cliArgs.multipleCommits || cliArgs.multiple, }; diff --git a/src/options/config/config.test.ts b/src/options/config/config.test.ts index 3a6e003f..225ad412 100644 --- a/src/options/config/config.test.ts +++ b/src/options/config/config.test.ts @@ -15,7 +15,7 @@ describe('getOptionsFromConfigFiles', () => { githubApiBaseUrlV3: 'https://api.github.com', githubApiBaseUrlV4: 'https://api.github.com/graphql', backportCreatedLabels: [], - branchChoices: [ + targetBranchChoices: [ { checked: false, name: '6.0' }, { checked: false, name: '5.9' }, ], @@ -25,7 +25,7 @@ describe('getOptionsFromConfigFiles', () => { multiple: false, multipleBranches: true, multipleCommits: false, - prTitle: '[{baseBranch}] {commitMessages}', + prTitle: '[{targetBranch}] {commitMessages}', upstream: 'elastic/backport-demo', username: 'sqren', }); diff --git a/src/options/config/config.ts b/src/options/config/config.ts index f611d769..b82e6b93 100644 --- a/src/options/config/config.ts +++ b/src/options/config/config.ts @@ -27,18 +27,18 @@ export async function getOptionsFromConfigFiles() { multipleBranches: true, all: false, labels: [] as string[], - prTitle: '[{baseBranch}] {commitMessages}', + prTitle: '[{targetBranch}] {commitMessages}', gitHostname: 'github.com', githubApiBaseUrlV3: 'https://api.github.com', githubApiBaseUrlV4: 'https://api.github.com/graphql', - branchChoices: getBranchesAsObjects(branches), + targetBranchChoices: getTargetBranchChoices(branches), ...combinedConfig, }; } -// in the config `branches` can either a string or an object. +// in the config `branches` can either be a string or an object. // We need to transform it so that it is always treated as an object troughout the application -function getBranchesAsObjects(branches?: Config['branches']) { +function getTargetBranchChoices(branches?: Config['branches']) { if (!branches) { return; } diff --git a/src/options/config/projectConfig.test.ts b/src/options/config/projectConfig.test.ts index 4ff0a71e..d96e1e1c 100644 --- a/src/options/config/projectConfig.test.ts +++ b/src/options/config/projectConfig.test.ts @@ -34,7 +34,7 @@ describe('getProjectConfig', () => { it('should return empty config', async () => { jest.spyOn(fs, 'readFile').mockResolvedValueOnce('{}'); const projectConfig = await getProjectConfig(); - expect(projectConfig).toEqual({ branchChoices: undefined }); + expect(projectConfig).toEqual({ targetBranchChoices: undefined }); }); }); diff --git a/src/options/options.test.ts b/src/options/options.test.ts index b07c2850..e82dd94f 100644 --- a/src/options/options.test.ts +++ b/src/options/options.test.ts @@ -78,18 +78,18 @@ describe('getOptions', () => { githubApiBaseUrlV4: 'https://api.github.com/graphql', author: 'sqren', backportCreatedLabels: [], - branchChoices: [ + targetBranchChoices: [ { checked: false, name: '6.0' }, { checked: false, name: '5.9' }, ], - branches: ['6.0', '6.1'], + targetBranches: ['6.0', '6.1'], fork: true, gitHostname: 'github.com', labels: [], multiple: false, multipleBranches: true, multipleCommits: false, - prTitle: '[{baseBranch}] {commitMessages}', + prTitle: '[{targetBranch}] {commitMessages}', repoName: 'kibana', repoOwner: 'elastic', resetAuthor: false, @@ -104,12 +104,13 @@ describe('validateRequiredOptions', () => { const validOptions: OptionsFromCliArgs = { accessToken: 'myAccessToken', all: false, + branchLabelMapping: undefined, githubApiBaseUrlV3: 'https://api.github.com', githubApiBaseUrlV4: 'https://api.github.com/graphql', author: undefined, backportCreatedLabels: [], - branchChoices: [], - branches: ['branchA'], + targetBranchChoices: [], + targetBranches: ['branchA'], commitsCount: 10, editor: 'code', fork: true, @@ -136,12 +137,12 @@ describe('validateRequiredOptions', () => { expect(() => validateRequiredOptions(validOptions)).not.toThrow(); }); - it('when all options are valid and `branchChoices` is given', () => { + it('when all options are valid and `targetBranchChoices` is given', () => { expect(() => validateRequiredOptions({ ...validOptions, - branchChoices: [{ name: 'branchA' }], - branches: [], + targetBranchChoices: [{ name: 'branchA' }], + targetBranches: [], }) ).not.toThrow(); }); @@ -166,12 +167,12 @@ describe('validateRequiredOptions', () => { ).toThrowErrorMatchingSnapshot(); }); - it('when both branches and branchChoices are missing', () => { + it('when both branches and targetBranchChoices are missing', () => { expect(() => validateRequiredOptions({ ...validOptions, - branchChoices: [], - branches: [], + targetBranchChoices: [], + targetBranches: [], }) ).toThrowErrorMatchingSnapshot(); }); diff --git a/src/options/options.ts b/src/options/options.ts index b4c0e993..87008a9b 100644 --- a/src/options/options.ts +++ b/src/options/options.ts @@ -41,35 +41,32 @@ export function validateRequiredOptions({ ); } - if (isEmpty(options.branches) && isEmpty(options.branchChoices)) { + if (isEmpty(options.targetBranches) && isEmpty(options.targetBranchChoices)) { throw new HandledError( - getErrorMessage({ field: 'branches', exampleValue: '6.1' }) + `You must specify a target branch\n\nYou can specify it via either:\n - Config file (recommended): ".backportrc.json". Read more: ${PROJECT_CONFIG_DOCS_LINK}\n - CLI: "--branch 6.1"` ); } const [repoOwner, repoName] = upstream.split('/'); if (!repoOwner || !repoName) { throw new HandledError( - getErrorMessage({ field: 'upstream', exampleValue: 'elastic/kibana' }) + `You must specify a valid Github repository\n\nYou can specify it via either:\n - Config file (recommended): ".backportrc.json". Read more: ${PROJECT_CONFIG_DOCS_LINK}\n - CLI: "--upstream elastic/kibana"` ); } return { ...options, + + // no longer optional accessToken: options.accessToken, + username: options.username, + + // split upstream repoName, repoOwner, - username: options.username, + + // define author author: options.author || options.username, + all: options.author ? false : options.all, }; } - -function getErrorMessage({ - field, - exampleValue, -}: { - field: keyof OptionsFromCliArgs; - exampleValue: string; -}) { - return `Invalid option "${field}"\n\nYou can add it with either:\n - Config file: ".backportrc.json". Read more: ${PROJECT_CONFIG_DOCS_LINK}\n - CLI: "--${field} ${exampleValue}"`; -} diff --git a/src/runWithOptions.test.ts b/src/runWithOptions.test.ts index 3d462585..8e310f11 100644 --- a/src/runWithOptions.test.ts +++ b/src/runWithOptions.test.ts @@ -21,13 +21,14 @@ describe('runWithOptions', () => { beforeEach(async () => { const options: BackportOptions = { accessToken: 'myAccessToken', + branchLabelMapping: undefined, all: false, githubApiBaseUrlV3: 'https://api.github.com', githubApiBaseUrlV4: 'https://api.github.com/graphql', author: 'sqren', backportCreatedLabels: [], - branches: [], - branchChoices: [ + targetBranches: [], + targetBranchChoices: [ { name: '6.x' }, { name: '6.0' }, { name: '5.6' }, @@ -45,7 +46,7 @@ describe('runWithOptions', () => { multipleCommits: false, path: undefined, prDescription: 'myPrDescription', - prTitle: 'myPrTitle {baseBranch} {commitMessages}', + prTitle: 'myPrTitle {targetBranch} {commitMessages}', pullNumber: undefined, repoName: 'kibana', repoOwner: 'elastic', diff --git a/src/runWithOptions.ts b/src/runWithOptions.ts index 0926e439..5a0bc4af 100755 --- a/src/runWithOptions.ts +++ b/src/runWithOptions.ts @@ -4,22 +4,26 @@ import { addLabelsToPullRequest } from './services/github/v3/addLabelsToPullRequ import { logger, consoleLog } from './services/logger'; import { sequentially } from './services/sequentially'; import { cherrypickAndCreatePullRequest } from './ui/cherrypickAndCreatePullRequest'; -import { getBranches } from './ui/getBranches'; +import { getTargetBranches } from './ui/getBranches'; import { getCommits } from './ui/getCommits'; import { maybeSetupRepo } from './ui/maybeSetupRepo'; import { withSpinner } from './ui/withSpinner'; export async function runWithOptions(options: BackportOptions) { const commits = await getCommits(options); - const branches = await getBranches(options); + const targetBranches = await getTargetBranches(options, commits); await maybeSetupRepo(options); let backportSucceeded = false; // minimum 1 backport PR was successfully created - await sequentially(branches, async (baseBranch) => { - logger.info(`Backporting ${JSON.stringify(commits)} to ${baseBranch}`); + await sequentially(targetBranches, async (targetBranch) => { + logger.info(`Backporting ${JSON.stringify(commits)} to ${targetBranch}`); try { - await cherrypickAndCreatePullRequest({ options, commits, baseBranch }); + await cherrypickAndCreatePullRequest({ + options, + commits, + targetBranch, + }); backportSucceeded = true; } catch (e) { if (e instanceof HandledError) { diff --git a/src/services/git.test.ts b/src/services/git.test.ts index 05527808..545d1a93 100644 --- a/src/services/git.test.ts +++ b/src/services/git.test.ts @@ -78,7 +78,7 @@ describe('createFeatureBranch', () => { repoName: 'kibana', } as BackportOptions; - const baseBranch = '4.x'; + const targetBranch = '4.x'; const featureBranch = 'backport/4.x/commit-72f94e76'; it('should throw HandledError', async () => { @@ -95,7 +95,7 @@ describe('createFeatureBranch', () => { jest.spyOn(childProcess, 'exec').mockRejectedValueOnce(err); await expect( - createFeatureBranch(options, baseBranch, featureBranch) + createFeatureBranch(options, targetBranch, featureBranch) ).rejects.toThrowErrorMatchingInlineSnapshot( `"The branch \\"4.x\\" is invalid or doesn't exist"` ); @@ -108,7 +108,7 @@ describe('createFeatureBranch', () => { expect.assertions(1); await expect( - createFeatureBranch(options, baseBranch, featureBranch) + createFeatureBranch(options, targetBranch, featureBranch) ).rejects.toThrowErrorMatchingInlineSnapshot(`"just a normal error"`); }); }); @@ -152,9 +152,10 @@ describe('cherrypick', () => { } as BackportOptions; const commit = { - branch: '7.x', + sourceBranch: '7.x', formattedMessage: '', sha: 'abcd', + targetBranches: [], }; it('should return `needsResolving: false` when no errors are encountered', async () => { diff --git a/src/services/git.ts b/src/services/git.ts index 9a3aad42..98583d6c 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -103,7 +103,7 @@ export async function cherrypick( commit: CommitSelected ) { await exec( - `git fetch ${options.repoOwner} ${commit.branch}:${commit.branch} --force`, + `git fetch ${options.repoOwner} ${commit.sourceBranch}:${commit.sourceBranch} --force`, { cwd: getRepoPath(options) } ); const mainline = @@ -203,12 +203,12 @@ export async function addUnstagedFiles(options: BackportOptions) { export async function createFeatureBranch( options: BackportOptions, - baseBranch: string, + targetBranch: string, featureBranch: string ) { try { return await exec( - `git reset --hard && git clean -d --force && git fetch ${options.repoOwner} ${baseBranch} && git checkout -B ${featureBranch} ${options.repoOwner}/${baseBranch} --no-track`, + `git reset --hard && git clean -d --force && git fetch ${options.repoOwner} ${targetBranch} && git checkout -B ${featureBranch} ${options.repoOwner}/${targetBranch} --no-track`, { cwd: getRepoPath(options) } ); } catch (e) { @@ -218,7 +218,7 @@ export async function createFeatureBranch( if (isBranchInvalid) { throw new HandledError( - `The branch "${baseBranch}" is invalid or doesn't exist` + `The branch "${targetBranch}" is invalid or doesn't exist` ); } throw e; diff --git a/src/services/github/v3/fetchCommitBySha.test.ts b/src/services/github/v3/fetchCommitBySha.test.ts index e54724d4..2239b13c 100644 --- a/src/services/github/v3/fetchCommitBySha.test.ts +++ b/src/services/github/v3/fetchCommitBySha.test.ts @@ -25,10 +25,11 @@ describe('fetchCommitBySha', () => { await expect( await fetchCommitBySha({ ...options, sha: commitSha }) ).toEqual({ - branch: 'master', + sourceBranch: 'master', formattedMessage: 'myMessage (sha12345)', pullNumber: undefined, sha: 'sha123456789', + targetBranches: [], }); expect(axiosSpy.mock.calls).toMatchSnapshot(); diff --git a/src/services/github/v3/fetchCommitBySha.ts b/src/services/github/v3/fetchCommitBySha.ts index c9f0c761..a321b40e 100644 --- a/src/services/github/v3/fetchCommitBySha.ts +++ b/src/services/github/v3/fetchCommitBySha.ts @@ -54,7 +54,8 @@ export async function fetchCommitBySha( }); return { - branch: 'master', + sourceBranch: 'master', + targetBranches: [], formattedMessage, sha: fullSha, }; diff --git a/src/services/github/v4/__snapshots__/fetchCommitsByAuthor.test.ts.snap b/src/services/github/v4/__snapshots__/fetchCommitsByAuthor.test.ts.snap index e78e146d..b69de0d4 100644 --- a/src/services/github/v4/__snapshots__/fetchCommitsByAuthor.test.ts.snap +++ b/src/services/github/v4/__snapshots__/fetchCommitsByAuthor.test.ts.snap @@ -63,6 +63,11 @@ Array [ mergeCommit { oid } + labels(first: 50) { + nodes { + name + } + } timelineItems( last: 20 itemTypes: CROSS_REFERENCED_EVENT @@ -109,7 +114,7 @@ Array [ "historyPath": null, "repoName": "kibana", "repoOwner": "elastic", - "sourceBranch": undefined, + "sourceBranch": "master", }, }, Object { diff --git a/src/services/github/v4/fetchCommitByPullNumber.test.ts b/src/services/github/v4/fetchCommitByPullNumber.test.ts new file mode 100644 index 00000000..695a7b71 --- /dev/null +++ b/src/services/github/v4/fetchCommitByPullNumber.test.ts @@ -0,0 +1,113 @@ +import { getTargetBranchesFromLabels } from './getTargetBranchesFromLabels'; + +describe('getTargetBranchesFromLabels', () => { + it(`should support Kibana's label format`, () => { + const branchLabelMapping = { + 'v8.0.0': '', // current major (master) + '^v7.8.0$': '7.x', // current minor (7.x) + '^v(\\d+).(\\d+).\\d+$': '$1.$2', // all other branches + }; + const labels = [ + 'release_note:fix', + 'v5.4.3', + 'v5.5.3', + 'v5.6.16', + 'v6.0.1', + 'v6.1.4', + 'v6.2.5', + 'v6.3.3', + 'v6.4.4', + 'v6.5.5', + 'v6.6.3', + 'v6.7.2', + 'v6.8.4', + 'v7.0.2', + 'v7.1.2', + 'v7.2.2', + 'v7.3.3', + 'v7.4.1', + 'v7.5.0', + 'v7.6.0', + 'v7.7.0', + 'v7.8.0', // 7.x + 'v8.0.0', // master + ]; + const targetBranches = getTargetBranchesFromLabels({ + labels, + branchLabelMapping, + }); + expect(targetBranches).toEqual([ + '5.4', + '5.5', + '5.6', + '6.0', + '6.1', + '6.2', + '6.3', + '6.4', + '6.5', + '6.6', + '6.7', + '6.8', + '7.0', + '7.1', + '7.2', + '7.3', + '7.4', + '7.5', + '7.6', + '7.7', + '7.x', + ]); + }); + + it('should only get first match', () => { + const branchLabelMapping = { + 'label-2': 'branch-b', + 'label-(\\d+)': 'branch-$1', + }; + const labels = ['label-2']; + const targetBranches = getTargetBranchesFromLabels({ + labels, + branchLabelMapping, + }); + expect(targetBranches).toEqual(['branch-b']); + }); + + it('should remove duplicates', () => { + const branchLabelMapping = { + 'label-(\\d+)': 'branch-$1', + }; + const labels = ['label-1', 'label-2', 'label-2']; + const targetBranches = getTargetBranchesFromLabels({ + labels, + branchLabelMapping, + }); + expect(targetBranches).toEqual(['branch-1', 'branch-2']); + }); + + it('should ignore non-matching labels', () => { + const branchLabelMapping = { + 'label-(\\d+)': 'branch-$1', + }; + const labels = ['label-1', 'label-2', 'foo', 'bar']; + const targetBranches = getTargetBranchesFromLabels({ + labels, + branchLabelMapping, + }); + expect(targetBranches).toEqual(['branch-1', 'branch-2']); + }); + + it('should omit empty labels', () => { + const branchLabelMapping = { + 'label-2': '', + 'label-(\\d+)': 'branch-$1', + }; + const labels = ['label-1', 'label-2']; + const targetBranches = getTargetBranchesFromLabels({ + labels, + branchLabelMapping, + }); + expect(targetBranches).toEqual(['branch-1']); + }); +}); diff --git a/src/services/github/v4/fetchCommitByPullNumber.ts b/src/services/github/v4/fetchCommitByPullNumber.ts index 650c3b1a..de892614 100644 --- a/src/services/github/v4/fetchCommitByPullNumber.ts +++ b/src/services/github/v4/fetchCommitByPullNumber.ts @@ -3,6 +3,7 @@ import { CommitSelected } from '../../../types/Commit'; import { HandledError } from '../../HandledError'; import { getFormattedCommitMessage } from '../commitFormatters'; import { apiRequestV4 } from './apiRequestV4'; +import { getTargetBranchesFromLabels } from './getTargetBranchesFromLabels'; export async function fetchCommitByPullNumber( options: BackportOptions & { pullNumber: number } @@ -13,6 +14,7 @@ export async function fetchCommitByPullNumber( repoOwner, pullNumber, accessToken, + branchLabelMapping, } = options; const query = /* GraphQL */ ` query getCommitbyPullNumber( @@ -29,6 +31,11 @@ export async function fetchCommitByPullNumber( oid message } + labels(first: 50) { + nodes { + name + } + } } } } @@ -49,7 +56,7 @@ export async function fetchCommitByPullNumber( throw new HandledError(`The PR #${pullNumber} is not merged`); } - const baseBranch = res.repository.pullRequest.baseRef.name; + const sourceBranch = res.repository.pullRequest.baseRef.name; const sha = res.repository.pullRequest.mergeCommit.oid; const formattedMessage = getFormattedCommitMessage({ message: res.repository.pullRequest.mergeCommit.message, @@ -57,8 +64,17 @@ export async function fetchCommitByPullNumber( pullNumber, }); + const labels = res.repository.pullRequest.labels.nodes.map( + (label) => label.name + ); + const targetBranches = getTargetBranchesFromLabels({ + labels, + branchLabelMapping, + }); + return { - branch: baseBranch, + sourceBranch, + targetBranches, sha, formattedMessage, pullNumber, @@ -75,6 +91,11 @@ interface DataResponse { oid: string; message: string; } | null; + labels: { + nodes: { + name: string; + }[]; + }; }; }; } diff --git a/src/services/github/v4/fetchCommitsByAuthor.test.ts b/src/services/github/v4/fetchCommitsByAuthor.test.ts index 112864ce..3abaf5ae 100644 --- a/src/services/github/v4/fetchCommitsByAuthor.test.ts +++ b/src/services/github/v4/fetchCommitsByAuthor.test.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -import { getDefaultOptions } from '../../../test/getDefaultOptions'; -import { CommitSelected } from '../../../types/Commit'; +import { BackportOptions } from '../../../options/options'; +import { CommitSelected, CommitChoice } from '../../../types/Commit'; import { SpyHelper } from '../../../types/SpyHelper'; import { fetchCommitsByAuthor, @@ -31,35 +31,46 @@ describe('fetchCommitsByAuthor', () => { }); it('Should return a list of commits with pullNumber and existing backports', () => { - expect(res).toEqual([ + const expectedCommits: CommitChoice[] = [ { sha: '2e63475c483f7844b0f2833bc57fdee32095bacb', formattedMessage: 'Add 👻 (2e63475c)', existingBackports: [], + targetBranches: [], + sourceBranch: 'master', }, { sha: 'f3b618b9421fdecdb36862f907afbdd6344b361d', formattedMessage: 'Add witch (#85)', pullNumber: 85, existingBackports: [], + targetBranches: [], + sourceBranch: 'master', }, { sha: '79cf18453ec32a4677009dcbab1c9c8c73fc14fe', formattedMessage: 'Add SF mention (#80)', pullNumber: 80, existingBackports: [{ branch: '6.3', state: 'MERGED' }], + targetBranches: [], + sourceBranch: 'master', }, { sha: '3827bbbaf39914eda4f02f6940189844375fd097', formattedMessage: 'Add backport config (3827bbba)', existingBackports: [], + targetBranches: [], + sourceBranch: 'master', }, { sha: '5ea0da550ac191029459289d67f99ad7d310812b', formattedMessage: 'Initial commit (5ea0da55)', existingBackports: [], + targetBranches: [], + sourceBranch: 'master', }, - ]); + ]; + expect(res).toEqual(expectedCommits); }); it('should call with correct args to fetch author id', () => { @@ -74,26 +85,32 @@ describe('fetchCommitsByAuthor', () => { describe('existingBackports', () => { it('should return existingBackports when repoNames match', async () => { const res = await getExistingBackportsByRepoName('kibana', 'kibana'); - expect(res).toEqual([ + const expectedCommits: CommitChoice[] = [ { existingBackports: [{ branch: '6.3', state: 'MERGED' }], formattedMessage: 'Add SF mention (#80)', pullNumber: 80, sha: '79cf18453ec32a4677009dcbab1c9c8c73fc14fe', + sourceBranch: 'master', + targetBranches: [], }, - ]); + ]; + expect(res).toEqual(expectedCommits); }); it('should not return existingBackports when repoNames does not match', async () => { const res = await getExistingBackportsByRepoName('kibana', 'kibana2'); - expect(res).toEqual([ + const expectedCommits: CommitChoice[] = [ { existingBackports: [], formattedMessage: 'Add SF mention (#80)', pullNumber: 80, sha: '79cf18453ec32a4677009dcbab1c9c8c73fc14fe', + sourceBranch: 'master', + targetBranches: [], }, - ]); + ]; + expect(res).toEqual(expectedCommits); }); }); @@ -201,3 +218,17 @@ async function getExistingBackportsByRepoName( }); return fetchCommitsByAuthor(options); } + +function getDefaultOptions(options: Partial = {}) { + return { + repoOwner: 'elastic', + repoName: 'kibana', + sourceBranch: 'master', + accessToken: 'myAccessToken', + username: 'sqren', + author: 'sqren', + githubApiBaseUrlV3: 'https://api.github.com', + githubApiBaseUrlV4: 'https://api.github.com/graphql', + ...options, + } as BackportOptions; +} diff --git a/src/services/github/v4/fetchCommitsByAuthor.ts b/src/services/github/v4/fetchCommitsByAuthor.ts index 48eb908f..e958e744 100644 --- a/src/services/github/v4/fetchCommitsByAuthor.ts +++ b/src/services/github/v4/fetchCommitsByAuthor.ts @@ -1,5 +1,6 @@ import { BackportOptions } from '../../../options/options'; import { CommitChoice } from '../../../types/Commit'; +import { filterEmpty } from '../../../utils/filterEmpty'; import { HandledError } from '../../HandledError'; import { getFirstCommitMessageLine, @@ -7,12 +8,14 @@ import { } from '../commitFormatters'; import { apiRequestV4 } from './apiRequestV4'; import { fetchAuthorId } from './fetchAuthorId'; +import { getTargetBranchesFromLabels } from './getTargetBranchesFromLabels'; export async function fetchCommitsByAuthor( options: BackportOptions ): Promise { const { accessToken, + branchLabelMapping, githubApiBaseUrlV4, commitsCount, path, @@ -56,6 +59,11 @@ export async function fetchCommitsByAuthor( mergeCommit { oid } + labels(first: 50) { + nodes { + name + } + } timelineItems( last: 20 itemTypes: CROSS_REFERENCED_EVENT @@ -125,6 +133,7 @@ export async function fetchCommitsByAuthor( const commitMessage = edge.node.message; const sha = edge.node.oid; + // check whether the commit was merged via a pull request const associatedPullRequest = isAssociatedPullRequest({ pullRequestEdge, options, @@ -133,6 +142,7 @@ export async function fetchCommitsByAuthor( ? pullRequestEdge : undefined; + // find any existing pull requests const existingBackports = getExistingBackportPRs( commitMessage, associatedPullRequest @@ -148,8 +158,17 @@ export async function fetchCommitsByAuthor( sha, }); + const labels = associatedPullRequest?.node.labels.nodes.map( + (node) => node.name + ); + const targetBranches = getTargetBranchesFromLabels({ + labels, + branchLabelMapping, + }); + return { - branch: sourceBranch, + sourceBranch, + targetBranches, sha, formattedMessage, pullNumber, @@ -191,7 +210,7 @@ export function getExistingBackportPRs( const firstMessageLine = getFirstCommitMessageLine(commitMessage); return associatedPullRequest.node.timelineItems.edges - .filter(notEmpty) + .filter(filterEmpty) .filter((item) => { const { source } = item.node; @@ -230,10 +249,6 @@ export function getExistingBackportPRs( }); } -function notEmpty(value: TValue | null | undefined): value is TValue { - return value !== null && value !== undefined; -} - export interface DataResponse { repository: { ref: { @@ -262,6 +277,11 @@ export interface PullRequestEdge { mergeCommit: { oid: string; }; + labels: { + nodes: { + name: string; + }[]; + }; repository: { owner: { login: string; diff --git a/src/services/github/v4/getTargetBranchesFromLabels.ts b/src/services/github/v4/getTargetBranchesFromLabels.ts new file mode 100644 index 00000000..09774f5a --- /dev/null +++ b/src/services/github/v4/getTargetBranchesFromLabels.ts @@ -0,0 +1,30 @@ +import flatMap from 'lodash.flatmap'; +import uniq from 'lodash.uniq'; +import { filterEmpty } from '../../../utils/filterEmpty'; + +export function getTargetBranchesFromLabels({ + labels, + branchLabelMapping, +}: { labels?: string[]; branchLabelMapping?: Record } = {}) { + if (!branchLabelMapping || !labels) { + return []; + } + const targetBranches = flatMap(labels, (label) => { + // only get first match + const result = Object.entries(branchLabelMapping).find(([labelPattern]) => { + const regex = new RegExp(labelPattern); + const isMatch = label.match(regex) !== null; + return isMatch; + }); + + if (result) { + const [labelPattern, targetBranch] = result; + const regex = new RegExp(labelPattern); + return label.replace(regex, targetBranch); + } + }) + .filter((targetBranch) => targetBranch !== '') + .filter(filterEmpty); + + return uniq(targetBranches); +} diff --git a/src/services/github/v4/mocks/commitsByAuthorMock.ts b/src/services/github/v4/mocks/commitsByAuthorMock.ts index 4b6d93b4..235a3393 100644 --- a/src/services/github/v4/mocks/commitsByAuthorMock.ts +++ b/src/services/github/v4/mocks/commitsByAuthorMock.ts @@ -23,6 +23,9 @@ export const commitsWithPullRequestsMock: DataResponse = { edges: [ { node: { + labels: { + nodes: [{ name: 'my-label-b' }], + }, mergeCommit: { oid: 'f3b618b9421fdecdb36862f907afbdd6344b361d', }, @@ -51,6 +54,9 @@ export const commitsWithPullRequestsMock: DataResponse = { edges: [ { node: { + labels: { + nodes: [{ name: 'my-label-a' }], + }, mergeCommit: { oid: '79cf18453ec32a4677009dcbab1c9c8c73fc14fe', }, diff --git a/src/services/github/v4/mocks/getCommitsByAuthorMock.ts b/src/services/github/v4/mocks/getCommitsByAuthorMock.ts index 7af8e135..9f18ca66 100644 --- a/src/services/github/v4/mocks/getCommitsByAuthorMock.ts +++ b/src/services/github/v4/mocks/getCommitsByAuthorMock.ts @@ -15,6 +15,9 @@ export const getCommitsByAuthorMock = (repoName: string): DataResponse => ({ edges: [ { node: { + labels: { + nodes: [{ name: 'my-label-a' }], + }, mergeCommit: { oid: '79cf18453ec32a4677009dcbab1c9c8c73fc14fe', }, diff --git a/src/services/github/v4/mocks/getPullRequestEdgeMock.ts b/src/services/github/v4/mocks/getPullRequestEdgeMock.ts index 7a35b3d1..4b5c724b 100644 --- a/src/services/github/v4/mocks/getPullRequestEdgeMock.ts +++ b/src/services/github/v4/mocks/getPullRequestEdgeMock.ts @@ -12,6 +12,9 @@ export function getPullRequestEdgeMock({ }): PullRequestEdge { return { node: { + labels: { + nodes: [{ name: 'my-label-a' }], + }, mergeCommit: { oid: 'f3b618b9421fdecdb36862f907afbdd6344b361d', }, diff --git a/src/services/prompts.ts b/src/services/prompts.ts index cd7b3850..36c195d7 100644 --- a/src/services/prompts.ts +++ b/src/services/prompts.ts @@ -17,10 +17,13 @@ async function prompt(options: Question) { return promptResult; } -export async function promptForCommits( - commits: CommitChoice[], - isMultipleChoice: boolean -): Promise { +export async function promptForCommits({ + commits, + isMultipleChoice, +}: { + commits: CommitChoice[]; + isMultipleChoice: boolean; +}): Promise { const choices = commits.map((c, i) => { const backportTags = c.existingBackports .map((item) => { @@ -39,24 +42,28 @@ export async function promptForCommits( }); const res = await prompt({ - choices, + choices: [...choices, new inquirer.Separator()], message: 'Select commit to backport', - pageSize: Math.min(10, commits.length), + pageSize: 15, type: isMultipleChoice ? 'checkbox' : 'list', }); const selectedCommits = Array.isArray(res) ? res.reverse() : [res]; return isEmpty(selectedCommits) - ? promptForCommits(commits, isMultipleChoice) + ? promptForCommits({ commits, isMultipleChoice }) : selectedCommits; } -export async function promptForBranches( - branchChoices: BranchChoice[], - isMultipleChoice: boolean -): Promise { +export async function promptForTargetBranches({ + targetBranchChoices, + isMultipleChoice, +}: { + targetBranchChoices: BranchChoice[]; + isMultipleChoice: boolean; +}): Promise { const res = await prompt({ - choices: branchChoices, + pageSize: 15, + choices: [...targetBranchChoices, new inquirer.Separator()], message: 'Select branch to backport to', type: isMultipleChoice ? 'checkbox' : 'list', }); @@ -64,7 +71,10 @@ export async function promptForBranches( const selectedBranches = Array.isArray(res) ? res : [res]; return isEmpty(selectedBranches) - ? promptForBranches(branchChoices, isMultipleChoice) + ? promptForTargetBranches({ + targetBranchChoices, + isMultipleChoice, + }) : selectedBranches; } diff --git a/src/test/getDefaultOptions.ts b/src/test/getDefaultOptions.ts deleted file mode 100644 index b7657d07..00000000 --- a/src/test/getDefaultOptions.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BackportOptions } from '../options/options'; - -export function getDefaultOptions(options: Partial = {}) { - return { - repoOwner: 'elastic', - repoName: 'kibana', - accessToken: 'myAccessToken', - username: 'sqren', - author: 'sqren', - githubApiBaseUrlV3: 'https://api.github.com', - githubApiBaseUrlV4: 'https://api.github.com/graphql', - ...options, - } as BackportOptions; -} diff --git a/src/test/integration/__snapshots__/integration.test.ts.snap b/src/test/integration/__snapshots__/integration.test.ts.snap index 67ac9751..b148a1df 100644 --- a/src/test/integration/__snapshots__/integration.test.ts.snap +++ b/src/test/integration/__snapshots__/integration.test.ts.snap @@ -52,6 +52,11 @@ Object { mergeCommit { oid } + labels(first: 50) { + nodes { + name + } + } timelineItems( last: 20 itemTypes: CROSS_REFERENCED_EVENT @@ -243,6 +248,11 @@ Array [ mergeCommit { oid } + labels(first: 50) { + nodes { + name + } + } timelineItems( last: 20 itemTypes: CROSS_REFERENCED_EVENT @@ -325,6 +335,13 @@ Array [ "edges": Array [ Object { "node": Object { + "labels": Object { + "nodes": Array [ + Object { + "name": "my-label-b", + }, + ], + }, "mergeCommit": Object { "oid": "f3b618b9421fdecdb36862f907afbdd6344b361d", }, @@ -352,6 +369,13 @@ Array [ "edges": Array [ Object { "node": Object { + "labels": Object { + "nodes": Array [ + Object { + "name": "my-label-a", + }, + ], + }, "mergeCommit": Object { "oid": "79cf18453ec32a4677009dcbab1c9c8c73fc14fe", }, @@ -454,7 +478,7 @@ fatal: No such remote: 'elastic' "", ], Array [ - "[INFO] Backporting [{\\"branch\\":\\"master\\",\\"sha\\":\\"f3b618b9421fdecdb36862f907afbdd6344b361d\\",\\"formattedMessage\\":\\"Add witch (#85)\\",\\"pullNumber\\":85,\\"existingBackports\\":[]}] to 6.0", + "[INFO] Backporting [{\\"sourceBranch\\":\\"master\\",\\"targetBranches\\":[],\\"sha\\":\\"f3b618b9421fdecdb36862f907afbdd6344b361d\\",\\"formattedMessage\\":\\"Add witch (#85)\\",\\"pullNumber\\":85,\\"existingBackports\\":[]}] to 6.0", undefined, ], Array [ @@ -594,6 +618,11 @@ Object { mergeCommit { oid } + labels(first: 50) { + nodes { + name + } + } timelineItems( last: 20 itemTypes: CROSS_REFERENCED_EVENT diff --git a/src/test/yargs.test.ts b/src/test/yargs.test.ts index d148bcb1..0e7acf61 100644 --- a/src/test/yargs.test.ts +++ b/src/test/yargs.test.ts @@ -51,11 +51,11 @@ describe('yargs', () => { `--upstream foo --username ${username} --accessToken ${accessToken}` ); expect(res).toMatchInlineSnapshot(` - "Invalid option \\"branches\\" + "You must specify a target branch - You can add it with either: - - Config file: \\".backportrc.json\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#project-config-backportrcjson - - CLI: \\"--branches 6.1\\" + You can specify it via either: + - Config file (recommended): \\".backportrc.json\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#project-config-backportrcjson + - CLI: \\"--branch 6.1\\" " `); }); @@ -65,10 +65,10 @@ describe('yargs', () => { `--branch foo --username ${username} --accessToken ${accessToken}` ); expect(res).toMatchInlineSnapshot(` - "Invalid option \\"upstream\\" + "You must specify a valid Github repository - You can add it with either: - - Config file: \\".backportrc.json\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#project-config-backportrcjson + You can specify it via either: + - Config file (recommended): \\".backportrc.json\\". Read more: https://github.com/sqren/backport/blob/434a28b431bb58c9a014d4489a95f561e6bb2769/docs/configuration.md#project-config-backportrcjson - CLI: \\"--upstream elastic/kibana\\" " `); @@ -114,7 +114,8 @@ describe('yargs', () => { 3. Add 👻 (2e63475c) 4. Add witch (#85) 5. Add SF mention (#80) 6.3 - 6. Add backport config (3827bbba)" + 6. Add backport config (3827bbba) + ──────────────" `); }); @@ -141,7 +142,8 @@ describe('yargs', () => { 3. Update romeo-and-juliet.txt (91eee967) 4. Add 👻 (2e63475c) 5. Add witch (#85) - 6. Add SF mention (#80) 6.3" + 6. Add SF mention (#80) 6.3 + ──────────────" `); }); }); diff --git a/src/types/Commit.d.ts b/src/types/Commit.d.ts index 6efe98c6..485d5c87 100644 --- a/src/types/Commit.d.ts +++ b/src/types/Commit.d.ts @@ -1,6 +1,7 @@ // Commit object selected from list or via commit sha export interface CommitSelected { - branch: string; + sourceBranch: string; + targetBranches: string[]; sha: string; formattedMessage: string; pullNumber?: number; diff --git a/src/types/Config.d.ts b/src/types/Config.d.ts index 461c0a9a..c36502fc 100644 --- a/src/types/Config.d.ts +++ b/src/types/Config.d.ts @@ -10,6 +10,7 @@ export interface Config { editor?: string; // project config + branchLabelMapping?: Record; branches?: (string | BranchChoice)[]; upstream?: string; fork?: boolean; diff --git a/src/ui/__snapshots__/cherrypickAndCreatePullRequest.test.ts.snap b/src/ui/__snapshots__/cherrypickAndCreatePullRequest.test.ts.snap index edb57d4f..bebde7a9 100644 --- a/src/ui/__snapshots__/cherrypickAndCreatePullRequest.test.ts.snap +++ b/src/ui/__snapshots__/cherrypickAndCreatePullRequest.test.ts.snap @@ -75,7 +75,7 @@ Array [ }, ], Array [ - "git checkout myDefaultRepoBaseBranch && git branch -D backport/6.x/commit-mySha", + "git checkout myDefaultSourceBranch && git branch -D backport/6.x/commit-mySha", Object { "cwd": "/myHomeDir/.backport/repositories/elastic/kibana", }, @@ -122,7 +122,7 @@ Array [ }, ], Array [ - "git checkout myDefaultRepoBaseBranch && git branch -D backport/6.x/pr-1000_pr-2000", + "git checkout myDefaultSourceBranch && git branch -D backport/6.x/pr-1000_pr-2000", Object { "cwd": "/myHomeDir/.backport/repositories/elastic/kibana", }, diff --git a/src/ui/cherrypickAndCreatePullRequest.test.ts b/src/ui/cherrypickAndCreatePullRequest.test.ts index cb4b1bb1..feb2eecc 100644 --- a/src/ui/cherrypickAndCreatePullRequest.test.ts +++ b/src/ui/cherrypickAndCreatePullRequest.test.ts @@ -47,32 +47,34 @@ describe('cherrypickAndCreatePullRequest', () => { fork: true, labels: ['backport'], prDescription: 'myPrSuffix', - prTitle: '[{baseBranch}] {commitMessages}', + prTitle: '[{targetBranch}] {commitMessages}', repoName: 'kibana', repoOwner: 'elastic', username: 'sqren', - sourceBranch: 'myDefaultRepoBaseBranch', + sourceBranch: 'myDefaultSourceBranch', } as BackportOptions; const commits: CommitSelected[] = [ { - branch: '7.x', + sourceBranch: '7.x', sha: 'mySha', formattedMessage: 'myCommitMessage (#1000)', pullNumber: 1000, + targetBranches: [], }, { - branch: '7.x', + sourceBranch: '7.x', sha: 'mySha2', formattedMessage: 'myOtherCommitMessage (#2000)', pullNumber: 2000, + targetBranches: [], }, ]; await cherrypickAndCreatePullRequest({ options, commits, - baseBranch: '6.x', + targetBranch: '6.x', }); }); @@ -138,7 +140,7 @@ describe('cherrypickAndCreatePullRequest', () => { githubApiBaseUrlV3: 'https://api.github.com', fork: true, labels: ['backport'], - prTitle: '[{baseBranch}] {commitMessages}', + prTitle: '[{targetBranch}] {commitMessages}', repoName: 'kibana', repoOwner: 'elastic', username: 'sqren', @@ -148,12 +150,13 @@ describe('cherrypickAndCreatePullRequest', () => { options, commits: [ { - branch: '7.x', + sourceBranch: '7.x', sha: 'mySha', formattedMessage: 'myCommitMessage (mySha)', + targetBranches: [], }, ], - baseBranch: '6.x', + targetBranch: '6.x', }); }); @@ -195,11 +198,11 @@ describe('cherrypickAndCreatePullRequest', () => { const options = { fork: true, labels: ['backport'], - prTitle: '[{baseBranch}] {commitMessages}', + prTitle: '[{targetBranch}] {commitMessages}', repoName: 'kibana', repoOwner: 'elastic', username: 'sqren', - sourceBranch: 'myDefaultRepoBaseBranch', + sourceBranch: 'myDefaultSourceBranch', } as BackportOptions; const res = await runTimersUntilResolved(() => @@ -207,12 +210,13 @@ describe('cherrypickAndCreatePullRequest', () => { options, commits: [ { - branch: '7.x', + sourceBranch: '7.x', sha: 'mySha', formattedMessage: 'myCommitMessage', + targetBranches: [], }, ], - baseBranch: '6.x', + targetBranch: '6.x', }) ); diff --git a/src/ui/cherrypickAndCreatePullRequest.ts b/src/ui/cherrypickAndCreatePullRequest.ts index a0cbd9d3..d3b09804 100644 --- a/src/ui/cherrypickAndCreatePullRequest.ts +++ b/src/ui/cherrypickAndCreatePullRequest.ts @@ -30,24 +30,24 @@ import isEmpty = require('lodash.isempty'); export async function cherrypickAndCreatePullRequest({ options, commits, - baseBranch, + targetBranch, }: { options: BackportOptions; commits: CommitSelected[]; - baseBranch: string; + targetBranch: string; }) { - const featureBranch = getFeatureBranchName(baseBranch, commits); + const featureBranch = getFeatureBranchName(targetBranch, commits); const commitMessages = commits .map((commit) => ` - ${commit.formattedMessage}`) .join('\n'); consoleLog( `\n${chalk.bold( - `Backporting the following commits to ${baseBranch}:` + `Backporting the following commits to ${targetBranch}:` )}\n${commitMessages}\n` ); await withSpinner({ text: 'Pulling latest changes' }, () => - createFeatureBranch(options, baseBranch, featureBranch) + createFeatureBranch(options, targetBranch, featureBranch) ); await sequentially(commits, (commit) => waitForCherrypick(options, commit)); @@ -68,7 +68,7 @@ export async function cherrypickAndCreatePullRequest({ await deleteFeatureBranch(options, featureBranch); return withSpinner({ text: 'Creating pull request' }, async (spinner) => { - const payload = getPullRequestPayload(options, baseBranch, commits); + const payload = getPullRequestPayload(options, targetBranch, commits); const pullRequest = await createPullRequest(options, payload); if (options.labels.length > 0) { @@ -80,7 +80,7 @@ export async function cherrypickAndCreatePullRequest({ }); } -function getFeatureBranchName(baseBranch: string, commits: CommitSelected[]) { +function getFeatureBranchName(targetBranch: string, commits: CommitSelected[]) { const refValues = commits .map((commit) => commit.pullNumber @@ -89,7 +89,7 @@ function getFeatureBranchName(baseBranch: string, commits: CommitSelected[]) { ) .join('_') .slice(0, 200); - return `backport/${baseBranch}/${refValues}`; + return `backport/${targetBranch}/${refValues}`; } async function waitForCherrypick( @@ -197,7 +197,7 @@ async function listUnstagedFiles(options: BackportOptions) { } function getPullRequestTitle( - baseBranch: string, + targetBranch: string, commits: CommitSelected[], prTitle: string ) { @@ -205,7 +205,7 @@ function getPullRequestTitle( .map((commit) => commit.formattedMessage) .join(' | '); return prTitle - .replace('{baseBranch}', baseBranch) + .replace('{targetBranch}', targetBranch) .replace('{commitMessages}', commitMessages) .slice(0, 240); } @@ -217,20 +217,20 @@ function getHeadBranchName(options: BackportOptions, featureBranch: string) { function getPullRequestPayload( options: BackportOptions, - baseBranch: string, + targetBranch: string, commits: CommitSelected[] ) { const { prDescription, prTitle } = options; - const featureBranch = getFeatureBranchName(baseBranch, commits); + const featureBranch = getFeatureBranchName(targetBranch, commits); const commitMessages = commits .map((commit) => ` - ${commit.formattedMessage}`) .join('\n'); const bodySuffix = prDescription ? `\n\n${prDescription}` : ''; return { - title: getPullRequestTitle(baseBranch, commits, prTitle), - body: `Backports the following commits to ${baseBranch}:\n${commitMessages}${bodySuffix}`, + title: getPullRequestTitle(targetBranch, commits, prTitle), + body: `Backports the following commits to ${targetBranch}:\n${commitMessages}${bodySuffix}`, head: getHeadBranchName(options, featureBranch), - base: baseBranch, + base: targetBranch, }; } diff --git a/src/ui/getBranches.test.ts b/src/ui/getBranches.test.ts index c10068cd..ad68c79a 100644 --- a/src/ui/getBranches.test.ts +++ b/src/ui/getBranches.test.ts @@ -1,26 +1,31 @@ +import { BackportOptions } from '../options/options'; import * as prompts from '../services/prompts'; -import { getBranches } from './getBranches'; +import { SpyHelper } from './../types/SpyHelper'; +import { getTargetBranches } from './getBranches'; -describe('getBranches', () => { - let promptSpy: ReturnType; +describe('getTargetBranches', () => { + let promptSpy: SpyHelper; beforeEach(() => { jest.clearAllMocks(); promptSpy = jest - .spyOn(prompts, 'promptForBranches') + .spyOn(prompts, 'promptForTargetBranches') .mockResolvedValueOnce(['branchA']); }); - describe('when `options.branches` is empty', () => { - let branches: ReturnType; + describe('when `options.targetBranches` is empty', () => { + let branches: ReturnType; beforeEach(async () => { - branches = await getBranches({ - branches: [], - branchChoices: ['branchA', 'branchB'], - multipleBranches: false, - } as any); + branches = await getTargetBranches( + ({ + targetBranches: [], + targetBranchChoices: [{ name: 'branchA' }, { name: 'branchB' }], + multipleBranches: false, + } as unknown) as BackportOptions, + [] + ); }); it('should return branches from prompt', () => { @@ -28,19 +33,25 @@ describe('getBranches', () => { }); it('should call prompt with correct args', () => { - expect(promptSpy).toHaveBeenLastCalledWith(['branchA', 'branchB'], false); + expect(promptSpy).toHaveBeenLastCalledWith({ + targetBranchChoices: [{ name: 'branchA' }, { name: 'branchB' }], + isMultipleChoice: false, + }); }); }); - describe('when `options.branches` is not empty', () => { - let branches: ReturnType; + describe('when `options.targetBranches` is not empty', () => { + let branches: ReturnType; beforeEach(() => { - branches = getBranches({ - branches: ['branchA', 'branchB'], - branchChoices: [], - multipleBranches: false, - } as any); + branches = getTargetBranches( + ({ + targetBranches: ['branchA', 'branchB'], + targetBranchChoices: [], + multipleBranches: false, + } as unknown) as BackportOptions, + [] + ); }); it('should return branches from `options.branches`', () => { diff --git a/src/ui/getBranches.ts b/src/ui/getBranches.ts index 6f2884a9..b70e666f 100644 --- a/src/ui/getBranches.ts +++ b/src/ui/getBranches.ts @@ -1,15 +1,54 @@ +import flatMap from 'lodash.flatmap'; import isEmpty from 'lodash.isempty'; import { BackportOptions } from '../options/options'; -import { promptForBranches } from '../services/prompts'; -import { BranchChoice } from '../types/Config'; +import { HandledError } from '../services/HandledError'; +import { promptForTargetBranches } from '../services/prompts'; +import { CommitSelected } from '../types/Commit'; +import { filterEmpty } from '../utils/filterEmpty'; -export function getBranches(options: BackportOptions) { - if (!isEmpty(options.branches)) { - return options.branches; +export function getTargetBranches( + options: BackportOptions, + commits: CommitSelected[] +) { + // target branches specified via cli + if (!isEmpty(options.targetBranches)) { + return options.targetBranches; } - return promptForBranches( - options.branchChoices as BranchChoice[], - options.multipleBranches - ); + // combine target branches from all commits + const targetBranchesFromLabels = flatMap( + commits, + (commit) => commit.targetBranches + ).filter(filterEmpty); + + return promptForTargetBranches({ + targetBranchChoices: getTargetBranchChoices( + options, + targetBranchesFromLabels + ), + isMultipleChoice: options.multipleBranches, + }); +} + +function getTargetBranchChoices( + options: BackportOptions, + targetBranchesFromLabels: string[] +) { + if (!options.targetBranchChoices) { + throw new HandledError('Missing target branch choices'); + } + + // no labels were found + if (isEmpty(targetBranchesFromLabels)) { + return options.targetBranchChoices; + } + + // automatially check options based on pull request labels + return options.targetBranchChoices.map((choice) => { + const isChecked = targetBranchesFromLabels.includes(choice.name); + return { + ...choice, + checked: isChecked, + }; + }); } diff --git a/src/ui/getCommitBySha.test.ts b/src/ui/getCommitBySha.test.ts index 1e9bbc64..6e59a372 100644 --- a/src/ui/getCommitBySha.test.ts +++ b/src/ui/getCommitBySha.test.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import { BackportOptions } from '../options/options'; import { commitByShaMock } from '../services/github/v3/mocks/commitByShaMock'; +import { CommitSelected } from '../types/Commit'; import { getCommitBySha } from './getCommits'; describe('getCommitBySha', () => { @@ -18,14 +19,16 @@ describe('getCommitBySha', () => { githubApiBaseUrlV3: 'https://api.github.com', } as BackportOptions & { sha: string }); - expect(commit).toEqual({ - branch: 'master', + const expectedCommit: CommitSelected = { + sourceBranch: 'master', formattedMessage: '[Chrome] Bootstrap Angular into document.body (myCommit)', sha: 'myCommitSha', pullNumber: undefined, - }); + targetBranches: [], + }; + expect(commit).toEqual(expectedCommit); expect(axiosSpy).toHaveBeenCalledWith({ method: 'get', url: diff --git a/src/ui/getCommits.ts b/src/ui/getCommits.ts index 37158946..28fe890c 100644 --- a/src/ui/getCommits.ts +++ b/src/ui/getCommits.ts @@ -71,7 +71,10 @@ async function getCommitsByPrompt(options: BackportOptions) { process.exit(1); } spinner.stop(); - return promptForCommits(commits, options.multipleCommits); + return promptForCommits({ + commits, + isMultipleChoice: options.multipleCommits, + }); } catch (e) { spinner.fail(); throw e; diff --git a/src/utils/filterEmpty.ts b/src/utils/filterEmpty.ts new file mode 100644 index 00000000..f0db2868 --- /dev/null +++ b/src/utils/filterEmpty.ts @@ -0,0 +1,5 @@ +export function filterEmpty( + value: TValue | null | undefined +): value is TValue { + return value !== null && value !== undefined; +} diff --git a/tsconfig.json b/tsconfig.json index 357182df..f14fac20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,8 @@ "include": ["src/**/*"], "exclude": ["src/**/*.test.*", "src/test/**", "src/**/mocks/**"], "compilerOptions": { - "target": "es2017", - "lib": ["es2017"], + "target": "es2018", + "lib": ["es2019"], "module": "commonjs", "outDir": "./dist", "forceConsistentCasingInFileNames": true, diff --git a/yarn.lock b/yarn.lock index 5806411d..8a9d35fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -586,6 +586,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/lodash.flatmap@^4.5.6": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@types/lodash.flatmap/-/lodash.flatmap-4.5.6.tgz#5f1ea80cebe403f0fbfcc1b5ad75cd09dd8b5785" + integrity sha512-ELNrUL9q+MB7AACaHivWIsKDFDgYhHE3/svXhqvDJgONtn2c467Cy87nEb7CEDvfaGCPv91lPaW596I8s5oiNQ== + dependencies: + "@types/lodash" "*" + "@types/lodash.isempty@^4.4.6": version "4.4.6" resolved "https://registry.yarnpkg.com/@types/lodash.isempty/-/lodash.isempty-4.4.6.tgz#48a5576985727d9b85d59a60199d6b11ac756a3e" @@ -666,17 +673,27 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^2.27.0": - version "2.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.27.0.tgz#e479cdc4c9cf46f96b4c287755733311b0d0ba4b" - integrity sha512-/my+vVHRN7zYgcp0n4z5A6HAK7bvKGBiswaM5zIlOQczsxj/aiD7RcgD+dvVFuwFaGh5+kM7XA6Q6PN0bvb1tw== +"@typescript-eslint/eslint-plugin@^2.28.0": + version "2.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.28.0.tgz#4431bc6d3af41903e5255770703d4e55a0ccbdec" + integrity sha512-w0Ugcq2iatloEabQP56BRWJowliXUP5Wv6f9fKzjJmDW81hOTBxRoJ4LoEOxRpz9gcY51Libytd2ba3yLmSOfg== dependencies: - "@typescript-eslint/experimental-utils" "2.27.0" + "@typescript-eslint/experimental-utils" "2.28.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.27.0", "@typescript-eslint/experimental-utils@^2.5.0": +"@typescript-eslint/experimental-utils@2.28.0": + version "2.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.28.0.tgz#1fd0961cd8ef6522687b4c562647da6e71f8833d" + integrity sha512-4SL9OWjvFbHumM/Zh/ZeEjUFxrYKtdCi7At4GyKTbQlrj1HcphIDXlje4Uu4cY+qzszR5NdVin4CCm6AXCjd6w== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.28.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/experimental-utils@^2.5.0": version "2.27.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.27.0.tgz#801a952c10b58e486c9a0b36cf21e2aab1e9e01a" integrity sha512-vOsYzjwJlY6E0NJRXPTeCGqjv5OHgRU1kzxHKWJVPjDYGbPgLudBXjIlc+OD1hDBZ4l1DLbOc5VjofKahsu9Jw== @@ -686,14 +703,14 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^2.27.0": - version "2.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.27.0.tgz#d91664335b2c46584294e42eb4ff35838c427287" - integrity sha512-HFUXZY+EdwrJXZo31DW4IS1ujQW3krzlRjBrFRrJcMDh0zCu107/nRfhk/uBasO8m0NVDbBF5WZKcIUMRO7vPg== +"@typescript-eslint/parser@^2.28.0": + version "2.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.28.0.tgz#bb761286efd2b0714761cab9d0ee5847cf080385" + integrity sha512-RqPybRDquui9d+K86lL7iPqH6Dfp9461oyqvlXMNtap+PyqYbkY5dB7LawQjDzot99fqzvS0ZLZdfe+1Bt3Jgw== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.27.0" - "@typescript-eslint/typescript-estree" "2.27.0" + "@typescript-eslint/experimental-utils" "2.28.0" + "@typescript-eslint/typescript-estree" "2.28.0" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@2.27.0": @@ -709,6 +726,19 @@ semver "^6.3.0" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@2.28.0": + version "2.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.28.0.tgz#d34949099ff81092c36dc275b6a1ea580729ba00" + integrity sha512-HDr8MP9wfwkiuqzRVkuM3BeDrOC4cKbO5a6BymZBHUt5y/2pL0BXD6I/C/ceq2IZoHWhcASk+5/zo+dwgu9V8Q== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^6.3.0" + tsutils "^3.17.1" + abab@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" @@ -1727,10 +1757,10 @@ eslint-plugin-jest@^23.8.2: dependencies: "@typescript-eslint/experimental-utils" "^2.5.0" -eslint-plugin-prettier@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba" - integrity sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA== +eslint-plugin-prettier@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.3.tgz#ae116a0fc0e598fdae48743a4430903de5b4e6ca" + integrity sha512-+HG5jmu/dN3ZV3T6eCD7a4BlAySdN7mLIbJYo0z1cFQuI+r2DiTJEFeF68ots93PsnrMxbzIZ2S/ieX+mkrBeQ== dependencies: prettier-linter-helpers "^1.0.0" @@ -3362,6 +3392,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.flatmap@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz#ef8cbf408f6e48268663345305c6acc0b778702e" + integrity sha1-74y/QI9uSCaGYzRTBcaswLd4cC4= + lodash.isempty@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"