From 6cf72f9363fc802baeae83cdf46fad82bf68d276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 20 Apr 2020 11:52:39 +0200 Subject: [PATCH] Add `--dry-run` option (#182) --- .gitignore | 3 +- .vscode/settings.json | 3 +- README.md | 7 ++- src/__snapshots__/runWithOptions.test.ts.snap | 10 +-- src/options/cliArgs.test.ts | 1 + src/options/cliArgs.ts | 5 ++ src/options/config/config.ts | 2 +- src/options/options.test.ts | 2 + src/runWithOptions.test.ts | 1 + src/runWithOptions.ts | 28 ++------- src/services/git.test.ts | 28 ++++++++- src/services/git.ts | 53 ++++++++++++---- .../github/v3/addLabelsToPullRequest.ts | 38 ++++++++---- src/services/github/v3/createPullRequest.ts | 43 +++++++++---- src/services/prompts.ts | 5 +- .../__snapshots__/integration.test.ts.snap | 8 --- .../github.com/elastic/backport-demo | 1 - .../github.com/sqren/backport-demo | 1 - .../repositories/elastic/backport-demo | 1 - src/ui/cherrypickAndCreatePullRequest.test.ts | 28 ++++++--- src/ui/cherrypickAndCreatePullRequest.ts | 62 +++++++++++-------- 21 files changed, 214 insertions(+), 116 deletions(-) delete mode 160000 src/test/integration/mock-environment/github.com/elastic/backport-demo delete mode 160000 src/test/integration/mock-environment/github.com/sqren/backport-demo delete mode 160000 src/test/integration/mock-environment/homedir/.backport/repositories/elastic/backport-demo diff --git a/.gitignore b/.gitignore index 80213931..dcd153e5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /dist /yarn-error.log .DS_Store -/test/integration/mock-environment +/src/test/integration/mock-environment + diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c2060fe..b734442a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,6 @@ "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/README.md b/README.md index e176a778..d3a9eb82 100644 --- a/README.md +++ b/README.md @@ -98,15 +98,16 @@ The above commands will start an interactive prompt. You can use the `arrow keys | --accesstoken | Github access token | | string | | --all | Show commits from other than me | false | boolean | | --author | Filter commits by author | _Current user_ | string | -| --branch | Branch to backport to | | string | +| --branch | Target branch to backport to | | string | | --commits-count | Number of commits to choose from | 10 | number | +| --dry-run | Perform backport without pushing to Github | false | boolean | | --editor | Editor (eg. `code`) to open and solve conflicts | | string | | --fork | Create backports in fork (true) or origin repo (false) | true | boolean | | --git-hostname | Hostname for Git remotes | github.com | string | | --github-api-base-url-v3 | Base url for Github's Rest (v3) API | https://api.github.com | string | | --github-api-base-url-v4 | Base url for Github's GraphQL (v4) API | https://api.github.com/graphql | string | | --labels | Pull request labels | | string | -| --mainline | Parent id of merge commit | 1 (when no parent id given) | number | +| --mainline | Parent id of merge commit | 1 | number | | --multiple | Select multiple commits/branches | false | boolean | | --path | Only list commits touching files under a specific path | | string | | --pr-description | Pull request description suffix | | string | @@ -114,7 +115,7 @@ The above commands will start an interactive prompt. You can use the `arrow keys | --pr | Pull request to backport | | number | | --reset-author | Set yourself as commit author | | boolean | | --sha | Sha of commit to backport | | string | -| --sourceBranch | Backport commits from a non-default branch | | string | +| --sourceBranch | The branch to source commits from | | string | | --upstream | Name of organization and repository | | string | | --username | Github username | | string | | --help | Show help | | | diff --git a/src/__snapshots__/runWithOptions.test.ts.snap b/src/__snapshots__/runWithOptions.test.ts.snap index 778984db..4f69fb16 100644 --- a/src/__snapshots__/runWithOptions.test.ts.snap +++ b/src/__snapshots__/runWithOptions.test.ts.snap @@ -86,7 +86,7 @@ Array [ "choices": Array [ Object { "name": "1. Add 👻 (2e63475c) ", - "short": "Add 👻 (2e63475c)", + "short": "2e63475c", "value": Object { "existingBackports": Array [], "formattedMessage": "Add 👻 (2e63475c)", @@ -98,7 +98,7 @@ Array [ }, Object { "name": "2. Add witch (#85) ", - "short": "Add witch (#85)", + "short": "#85 (f3b618b9)", "value": Object { "existingBackports": Array [], "formattedMessage": "Add witch (#85)", @@ -110,7 +110,7 @@ Array [ }, Object { "name": "3. Add SF mention (#80) 6.3", - "short": "Add SF mention (#80)", + "short": "#80 (79cf1845)", "value": Object { "existingBackports": Array [ Object { @@ -127,7 +127,7 @@ Array [ }, Object { "name": "4. Add backport config (3827bbba) ", - "short": "Add backport config (3827bbba)", + "short": "3827bbba", "value": Object { "existingBackports": Array [], "formattedMessage": "Add backport config (3827bbba)", @@ -139,7 +139,7 @@ Array [ }, Object { "name": "5. Initial commit (5ea0da55) ", - "short": "Initial commit (5ea0da55)", + "short": "5ea0da55", "value": Object { "existingBackports": Array [], "formattedMessage": "Initial commit (5ea0da55)", diff --git a/src/options/cliArgs.test.ts b/src/options/cliArgs.test.ts index cf8850ad..2ce68e4e 100644 --- a/src/options/cliArgs.test.ts +++ b/src/options/cliArgs.test.ts @@ -41,6 +41,7 @@ describe('getOptionsFromCliArgs', () => { githubApiBaseUrlV3: 'https://api.github.com', githubApiBaseUrlV4: 'https://api.github.com/graphql', backportCreatedLabels: [], + dryRun: false, targetBranches: ['6.0', '6.1'], targetBranchChoices: [], fork: true, diff --git a/src/options/cliArgs.ts b/src/options/cliArgs.ts index 1326cdfc..ed67beae 100644 --- a/src/options/cliArgs.ts +++ b/src/options/cliArgs.ts @@ -46,6 +46,11 @@ export function getOptionsFromCliArgs( alias: 'count', type: 'number', }) + .option('dryRun', { + default: false, + description: 'Perform backport without pushing to Github', + type: 'boolean', + }) .option('editor', { default: configOptions.editor, description: 'Editor to be opened during conflict resolution', diff --git a/src/options/config/config.ts b/src/options/config/config.ts index b82e6b93..8b97aaca 100644 --- a/src/options/config/config.ts +++ b/src/options/config/config.ts @@ -20,7 +20,7 @@ export async function getOptionsFromConfigFiles() { return { // defaults - backportCreatedLabels: [] as string[], + backportCreatedLabels: [] as string[] | never[], fork: true, multiple: false, multipleCommits: false, diff --git a/src/options/options.test.ts b/src/options/options.test.ts index e82dd94f..ba58e469 100644 --- a/src/options/options.test.ts +++ b/src/options/options.test.ts @@ -78,6 +78,7 @@ describe('getOptions', () => { githubApiBaseUrlV4: 'https://api.github.com/graphql', author: 'sqren', backportCreatedLabels: [], + dryRun: false, targetBranchChoices: [ { checked: false, name: '6.0' }, { checked: false, name: '5.9' }, @@ -109,6 +110,7 @@ describe('validateRequiredOptions', () => { githubApiBaseUrlV4: 'https://api.github.com/graphql', author: undefined, backportCreatedLabels: [], + dryRun: false, targetBranchChoices: [], targetBranches: ['branchA'], commitsCount: 10, diff --git a/src/runWithOptions.test.ts b/src/runWithOptions.test.ts index 8e310f11..34b1fd1f 100644 --- a/src/runWithOptions.test.ts +++ b/src/runWithOptions.test.ts @@ -27,6 +27,7 @@ describe('runWithOptions', () => { githubApiBaseUrlV4: 'https://api.github.com/graphql', author: 'sqren', backportCreatedLabels: [], + dryRun: false, targetBranches: [], targetBranchChoices: [ { name: '6.x' }, diff --git a/src/runWithOptions.ts b/src/runWithOptions.ts index 5a0bc4af..d85a7c4c 100755 --- a/src/runWithOptions.ts +++ b/src/runWithOptions.ts @@ -1,21 +1,23 @@ +import chalk from 'chalk'; import { BackportOptions } from './options/options'; import { HandledError } from './services/HandledError'; -import { addLabelsToPullRequest } from './services/github/v3/addLabelsToPullRequest'; import { logger, consoleLog } from './services/logger'; import { sequentially } from './services/sequentially'; import { cherrypickAndCreatePullRequest } from './ui/cherrypickAndCreatePullRequest'; 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) { + if (options.dryRun) { + consoleLog(chalk.red('Dry run: Nothing will be pushed to Github\n')); + } + const commits = await getCommits(options); const targetBranches = await getTargetBranches(options, commits); await maybeSetupRepo(options); - let backportSucceeded = false; // minimum 1 backport PR was successfully created await sequentially(targetBranches, async (targetBranch) => { logger.info(`Backporting ${JSON.stringify(commits)} to ${targetBranch}`); try { @@ -24,7 +26,6 @@ export async function runWithOptions(options: BackportOptions) { commits, targetBranch, }); - backportSucceeded = true; } catch (e) { if (e instanceof HandledError) { consoleLog(e.message); @@ -33,23 +34,4 @@ export async function runWithOptions(options: BackportOptions) { } } }); - - if (backportSucceeded && options.backportCreatedLabels.length > 0) { - await Promise.all( - commits.map(async ({ pullNumber }) => { - if (pullNumber) { - return withSpinner( - { text: `Adding labels to #${pullNumber}` }, - () => { - return addLabelsToPullRequest( - options, - pullNumber, - options.backportCreatedLabels - ); - } - ); - } - }) - ); - } } diff --git a/src/services/git.test.ts b/src/services/git.test.ts index 545d1a93..8e6bc95e 100644 --- a/src/services/git.test.ts +++ b/src/services/git.test.ts @@ -246,7 +246,7 @@ describe('cherrypick', () => { ); await expect(cherrypick(options, commit)).rejects - .toThrowError(`Failed to cherrypick because the selected commit was a merge. Please try again by specifying the parent with the \`mainline\` argument: + .toThrowError(`Cherrypick failed because the selected commit was a merge commit. Please try again by specifying the parent with the \`mainline\` argument: > backport --mainline @@ -257,6 +257,32 @@ or: Or refer to the git documentation for more information: https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt---mainlineparent-number`); }); + it('it should gracefully handle empty commits', async () => { + jest + .spyOn(childProcess, 'exec') + + // mock git fetch + .mockResolvedValueOnce({ stderr: '', stdout: '' }) + + // mock cherry pick command + .mockRejectedValueOnce( + new ExecError({ + killed: false, + code: 1, + signal: null, + cmd: 'git cherry-pick fe6b13b83cc010f722548cd5a0a8c2d5341a20dd', + stdout: + 'On branch backport/7.x/pr-58692\nYou are currently cherry-picking commit fe6b13b83cc.\n\nnothing to commit, working tree clean\n', + stderr: + "The previous cherry-pick is now empty, possibly due to conflict resolution.\nIf you wish to commit it anyway, use:\n\n git commit --allow-empty\n\nOtherwise, please use 'git cherry-pick --skip'\n", + }) + ); + + await expect(cherrypick(options, commit)).rejects.toThrowError( + `Cherrypick failed because the selected commit (abcd) is empty. This is most likely caused by attemping to backporting a commit that was already backported` + ); + }); + it('should re-throw non-cherrypick errors', async () => { jest .spyOn(childProcess, 'exec') diff --git a/src/services/git.ts b/src/services/git.ts index 98583d6c..acb5fffa 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -2,12 +2,14 @@ import { resolve as pathResolve } from 'path'; import del from 'del'; import isEmpty from 'lodash.isempty'; import uniq from 'lodash.uniq'; +import ora from 'ora'; import { BackportOptions } from '../options/options'; import { CommitSelected } from '../types/Commit'; import { HandledError } from './HandledError'; import { execAsCallback, exec } from './child-process-promisified'; import { getRepoOwnerPath, getRepoPath } from './env'; import { stat } from './fs-promisified'; +import { getShortSha } from './github/commitFormatters'; import { logger } from './logger'; async function folderExists(path: string): Promise { @@ -114,9 +116,18 @@ export async function cherrypick( await exec(cmd, { cwd: getRepoPath(options) }); return { needsResolving: false }; } catch (e) { + // missing `mainline` option if (e.message.includes('is a merge but no -m option was given')) { throw new HandledError( - 'Failed to cherrypick because the selected commit was a merge. Please try again by specifying the parent with the `mainline` argument:\n\n> backport --mainline\n\nor:\n\n> backport --mainline \n\nOr refer to the git documentation for more information: https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt---mainlineparent-number' + 'Cherrypick failed because the selected commit was a merge commit. Please try again by specifying the parent with the `mainline` argument:\n\n> backport --mainline\n\nor:\n\n> backport --mainline \n\nOr refer to the git documentation for more information: https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt---mainlineparent-number' + ); + } + + // commit was already backported + if (e.message.includes('The previous cherry-pick is now empty')) { + const shortSha = getShortSha(commit.sha); + throw new HandledError( + `Cherrypick failed because the selected commit (${shortSha}) is empty. This is most likely caused by attemping to backporting a commit that was already backported` ); } @@ -163,7 +174,10 @@ export async function getFilesWithConflicts(options: BackportOptions) { if (isConflictError) { const files = (e.stdout as string) .split('\n') - .filter((line: string) => !!line.trim()) + .filter( + (line: string) => + !!line.trim() && !line.startsWith('+') && !line.startsWith('-') + ) .map((line: string) => { const posSeparator = line.indexOf(':'); const filename = line.slice(0, posSeparator).trim(); @@ -239,13 +253,30 @@ export function getRemoteName(options: BackportOptions) { return options.fork ? options.username : options.repoOwner; } -export function pushFeatureBranch( - options: BackportOptions, - featureBranch: string -) { - const remoteName = getRemoteName(options); - return exec( - `git push ${remoteName} ${featureBranch}:${featureBranch} --force`, - { cwd: getRepoPath(options) } - ); +export function pushFeatureBranch({ + options, + featureBranch, + headBranchName, +}: { + options: BackportOptions; + featureBranch: string; + headBranchName: string; +}) { + const spinner = ora(`Pushing branch "${headBranchName}"`).start(); + + if (options.dryRun) { + spinner.succeed(`Dry run: Pushing branch "${headBranchName}"`); + return exec('true', { cwd: getRepoPath(options) }); + } + + try { + const remoteName = getRemoteName(options); + return exec( + `git push ${remoteName} ${featureBranch}:${featureBranch} --force`, + { cwd: getRepoPath(options) } + ); + } catch (e) { + spinner.fail(); + throw e; + } } diff --git a/src/services/github/v3/addLabelsToPullRequest.ts b/src/services/github/v3/addLabelsToPullRequest.ts index 6f45bbd2..48e11851 100644 --- a/src/services/github/v3/addLabelsToPullRequest.ts +++ b/src/services/github/v3/addLabelsToPullRequest.ts @@ -1,3 +1,4 @@ +import ora from 'ora'; import { BackportOptions } from '../../../options/options'; import { logger } from '../../logger'; import { apiRequestV3 } from './apiRequestV3'; @@ -9,19 +10,34 @@ export async function addLabelsToPullRequest( repoOwner, accessToken, username, + dryRun, }: BackportOptions, pullNumber: number, labels: string[] -) { - logger.info(`Adding label "${labels}" to #${pullNumber}`); +): Promise { + const text = `Adding labels to #${pullNumber}: ${labels.join(', ')}`; + logger.info(text); + const spinner = ora(text).start(); - return apiRequestV3({ - method: 'post', - url: `${githubApiBaseUrlV3}/repos/${repoOwner}/${repoName}/issues/${pullNumber}/labels`, - data: labels, - auth: { - username: username, - password: accessToken, - }, - }); + if (dryRun) { + spinner.succeed(`Dry run: ${text}`); + return; + } + + try { + await apiRequestV3({ + method: 'post', + url: `${githubApiBaseUrlV3}/repos/${repoOwner}/${repoName}/issues/${pullNumber}/labels`, + data: labels, + auth: { + username: username, + password: accessToken, + }, + }); + spinner.succeed(); + return; + } catch (e) { + spinner.fail(); + throw e; + } } diff --git a/src/services/github/v3/createPullRequest.ts b/src/services/github/v3/createPullRequest.ts index f85080b2..738fbd04 100644 --- a/src/services/github/v3/createPullRequest.ts +++ b/src/services/github/v3/createPullRequest.ts @@ -1,3 +1,4 @@ +import ora from 'ora'; import { BackportOptions } from '../../../options/options'; import { logger } from '../../logger'; import { apiRequestV3 } from './apiRequestV3'; @@ -14,6 +15,7 @@ export async function createPullRequest( repoOwner, accessToken, username, + dryRun, }: BackportOptions, payload: { title: string; @@ -25,18 +27,33 @@ export async function createPullRequest( logger.info( `Creating PR with title: "${payload.title}". ${payload.head} -> ${payload.base}` ); - const res = await apiRequestV3({ - method: 'post', - url: `${githubApiBaseUrlV3}/repos/${repoOwner}/${repoName}/pulls`, - data: payload, - auth: { - username: username, - password: accessToken, - }, - }); - return { - html_url: res.html_url, - number: res.number, - }; + const spinner = ora(`Creating pull request`).start(); + + if (dryRun) { + spinner.succeed(`Dry run: Creating pull request #1337`); + return { html_url: 'example_url', number: 1337 }; + } + + try { + const res = await apiRequestV3({ + method: 'post', + url: `${githubApiBaseUrlV3}/repos/${repoOwner}/${repoName}/pulls`, + data: payload, + auth: { + username: username, + password: accessToken, + }, + }); + + spinner.succeed(`Created pull request #${res.number}`); + + return { + html_url: res.html_url, + number: res.number, + }; + } catch (e) { + spinner.fail(); + throw e; + } } diff --git a/src/services/prompts.ts b/src/services/prompts.ts index 36c195d7..933cc3a8 100644 --- a/src/services/prompts.ts +++ b/src/services/prompts.ts @@ -7,6 +7,7 @@ import inquirer, { import isEmpty from 'lodash.isempty'; import { CommitChoice } from '../types/Commit'; import { BranchChoice } from '../types/Config'; +import { getShortSha } from './github/commitFormatters'; type Question = CheckboxQuestion | ListQuestion | ConfirmQuestion; @@ -36,7 +37,9 @@ export async function promptForCommits({ return { name: `${position} ${c.formattedMessage} ${backportTags}`, - short: c.formattedMessage, + short: c.pullNumber + ? `#${c.pullNumber} (${getShortSha(c.sha)})` + : getShortSha(c.sha), value: c, }; }); diff --git a/src/test/integration/__snapshots__/integration.test.ts.snap b/src/test/integration/__snapshots__/integration.test.ts.snap index b148a1df..4ff5f03a 100644 --- a/src/test/integration/__snapshots__/integration.test.ts.snap +++ b/src/test/integration/__snapshots__/integration.test.ts.snap @@ -501,10 +501,6 @@ fatal: No such remote: 'elastic' 1 file changed, 1 insertion(+), 1 deletion(-) ", ], - Array [ - "[INFO] Pushing branch \\"sqren:backport/6.0/pr-85\\"", - undefined, - ], Array [ "[VERBOSE] exec success 'git push sqren backport/6.0/pr-85:backport/6.0/pr-85 --force':", "", @@ -514,10 +510,6 @@ fatal: No such remote: 'elastic' "Deleted branch backport/6.0/pr-85 (was ). ", ], - Array [ - "[INFO] Creating pull request", - undefined, - ], Array [ "[INFO] Creating PR with title: \\"[6.0] Add witch (#85)\\". sqren:backport/6.0/pr-85 -> 6.0", undefined, diff --git a/src/test/integration/mock-environment/github.com/elastic/backport-demo b/src/test/integration/mock-environment/github.com/elastic/backport-demo deleted file mode 160000 index 2e63475c..00000000 --- a/src/test/integration/mock-environment/github.com/elastic/backport-demo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2e63475c483f7844b0f2833bc57fdee32095bacb diff --git a/src/test/integration/mock-environment/github.com/sqren/backport-demo b/src/test/integration/mock-environment/github.com/sqren/backport-demo deleted file mode 160000 index 2e63475c..00000000 --- a/src/test/integration/mock-environment/github.com/sqren/backport-demo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2e63475c483f7844b0f2833bc57fdee32095bacb diff --git a/src/test/integration/mock-environment/homedir/.backport/repositories/elastic/backport-demo b/src/test/integration/mock-environment/homedir/.backport/repositories/elastic/backport-demo deleted file mode 160000 index 2e63475c..00000000 --- a/src/test/integration/mock-environment/homedir/.backport/repositories/elastic/backport-demo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2e63475c483f7844b0f2833bc57fdee32095bacb diff --git a/src/ui/cherrypickAndCreatePullRequest.test.ts b/src/ui/cherrypickAndCreatePullRequest.test.ts index feb2eecc..764e3987 100644 --- a/src/ui/cherrypickAndCreatePullRequest.test.ts +++ b/src/ui/cherrypickAndCreatePullRequest.test.ts @@ -52,6 +52,7 @@ describe('cherrypickAndCreatePullRequest', () => { repoOwner: 'elastic', username: 'sqren', sourceBranch: 'myDefaultSourceBranch', + backportCreatedLabels: [], } as BackportOptions; const commits: CommitSelected[] = [ @@ -89,10 +90,10 @@ describe('cherrypickAndCreatePullRequest', () => { "Pulling latest changes", ], Array [ - "Cherry-picking commit mySha", + "Cherry-picking: myCommitMessage (#1000)", ], Array [ - "Cherry-picking commit mySha2", + "Cherry-picking: myOtherCommitMessage (#2000)", ], Array [ "Pushing branch \\"sqren:backport/6.x/pr-1000_pr-2000\\"", @@ -100,6 +101,9 @@ describe('cherrypickAndCreatePullRequest', () => { Array [ "Creating pull request", ], + Array [ + "Adding labels to #1337: backport", + ], ] `); }); @@ -144,6 +148,7 @@ describe('cherrypickAndCreatePullRequest', () => { repoName: 'kibana', repoOwner: 'elastic', username: 'sqren', + backportCreatedLabels: [], } as BackportOptions; await cherrypickAndCreatePullRequest({ @@ -203,6 +208,7 @@ describe('cherrypickAndCreatePullRequest', () => { repoOwner: 'elastic', username: 'sqren', sourceBranch: 'myDefaultSourceBranch', + backportCreatedLabels: [], } as BackportOptions; const res = await runTimersUntilResolved(() => @@ -233,7 +239,7 @@ describe('cherrypickAndCreatePullRequest', () => { You do not need to \`git add\` or \`git commit\` the files - simply fix the conflicts. - Press ENTER to continue", + Press ENTER when the conflicts are resolved", ], Array [ "The following files have conflicts: @@ -241,7 +247,7 @@ describe('cherrypickAndCreatePullRequest', () => { You do not need to \`git add\` or \`git commit\` the files - simply fix the conflicts. - Press ENTER to continue", + Press ENTER when the conflicts are resolved", ], Array [ "The following files are unstaged: @@ -256,9 +262,7 @@ describe('cherrypickAndCreatePullRequest', () => { Array [ Array [ " - Backporting the following commits to 6.x: - - myCommitMessage - ", + Backporting to 6.x:", ], Array [ "", @@ -272,6 +276,9 @@ describe('cherrypickAndCreatePullRequest', () => { Array [ "", ], + Array [ + "View pull request: myHtmlUrl", + ], ] `); expect((ora as any).mock.calls).toMatchInlineSnapshot(` @@ -280,10 +287,10 @@ describe('cherrypickAndCreatePullRequest', () => { "Pulling latest changes", ], Array [ - "Cherry-picking commit mySha", + "Cherry-picking: myCommitMessage", ], Array [ - "Staging and committing files", + "Finalizing cherrypick", ], Array [ "Pushing branch \\"sqren:backport/6.x/commit-mySha\\"", @@ -291,6 +298,9 @@ describe('cherrypickAndCreatePullRequest', () => { Array [ "Creating pull request", ], + Array [ + "Adding labels to #1337: backport", + ], ] `); expect(execSpy.mock.calls).toMatchSnapshot(); diff --git a/src/ui/cherrypickAndCreatePullRequest.ts b/src/ui/cherrypickAndCreatePullRequest.ts index d3b09804..6240c195 100644 --- a/src/ui/cherrypickAndCreatePullRequest.ts +++ b/src/ui/cherrypickAndCreatePullRequest.ts @@ -37,14 +37,7 @@ export async function cherrypickAndCreatePullRequest({ targetBranch: string; }) { const featureBranch = getFeatureBranchName(targetBranch, commits); - const commitMessages = commits - .map((commit) => ` - ${commit.formattedMessage}`) - .join('\n'); - consoleLog( - `\n${chalk.bold( - `Backporting the following commits to ${targetBranch}:` - )}\n${commitMessages}\n` - ); + consoleLog(`\n${chalk.bold(`Backporting to ${targetBranch}:`)}`); await withSpinner({ text: 'Pulling latest changes' }, () => createFeatureBranch(options, targetBranch, featureBranch) @@ -61,23 +54,42 @@ export async function cherrypickAndCreatePullRequest({ const headBranchName = getHeadBranchName(options, featureBranch); - await withSpinner({ text: `Pushing branch "${headBranchName}"` }, () => - pushFeatureBranch(options, featureBranch) - ); - + await pushFeatureBranch({ options, featureBranch, headBranchName }); await deleteFeatureBranch(options, featureBranch); - return withSpinner({ text: 'Creating pull request' }, async (spinner) => { - const payload = getPullRequestPayload(options, targetBranch, commits); - const pullRequest = await createPullRequest(options, payload); + const payload = getPullRequestPayload(options, targetBranch, commits); + const pullRequest = await createPullRequest(options, payload); - if (options.labels.length > 0) { - await addLabelsToPullRequest(options, pullRequest.number, options.labels); - } + // add targetPRLabels + if (options.labels.length > 0) { + await addLabelsToPullRequest(options, pullRequest.number, options.labels); + } + + // add sourcePRLabels + if (options.backportCreatedLabels.length > 0) { + const promises = commits.map((commit) => { + if (commit.pullNumber) { + return addLabelsToPullRequest( + options, + commit.pullNumber, + options.backportCreatedLabels + ); + } + }); + await Promise.all(promises); + } + + consoleLog(`View pull request: ${pullRequest.html_url}`); + + // output PR summary in dry run mode + if (options.dryRun) { + consoleLog(chalk.bold('\nPull request summary:')); + consoleLog(`Branch: ${payload.head} -> ${payload.base}`); + consoleLog(`Title: ${payload.title}`); + consoleLog(`Body: ${payload.body}\n`); + } - spinner.text = `Created pull request: ${pullRequest.html_url}`; - return pullRequest; - }); + return pullRequest; } function getFeatureBranchName(targetBranch: string, commits: CommitSelected[]) { @@ -97,7 +109,7 @@ async function waitForCherrypick( commit: CommitSelected ) { const cherrypickSpinner = ora( - `Cherry-picking commit ${getShortSha(commit.sha)}` + `Cherry-picking: ${chalk.greenBright(commit.formattedMessage)}` ).start(); try { @@ -129,14 +141,14 @@ async function waitForCherrypick( await listUnstagedFiles(options); // Conflicts resolved and unstaged files will now be staged and committed - const stagingSpinner = ora(`Staging and committing files`).start(); + const stagingSpinner = ora(`Finalizing cherrypick`).start(); try { // add unstaged files await addUnstagedFiles(options); // Run `cherrypick --continue` (similar to `git commit`) await cherrypickContinue(options); - stagingSpinner.succeed(); + stagingSpinner.stop(); } catch (e) { stagingSpinner.fail(); throw e; @@ -161,7 +173,7 @@ async function listConflictingFiles(options: BackportOptions) { 'You do not need to `git add` or `git commit` the files - simply fix the conflicts.' )} - Press ENTER to continue + Press ENTER when the conflicts are resolved `) ); if (!res) {