diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..74877ab8 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +bin/backport diff --git a/.vscode/launch.json b/.vscode/launch.json index fceb9e2f..7a91870f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,16 +7,7 @@ { "type": "node", "request": "launch", - "name": "Run file", - "program": "${file}", - "runtimeArgs": ["-r", "ts-node/register/transpile-only"], - "console": "integratedTerminal" - }, - - { - "type": "node", - "request": "launch", - "name": "Start backport (CLI)", + "name": "Run backport (ts-node)", "program": "${workspaceRoot}/src/entrypoint.cli.ts", "runtimeArgs": ["-r", "ts-node/register/transpile-only"], "args": [ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e492209f..20779023 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,29 +3,19 @@ ### Run ``` -yarn start --branch 6.1 --repo-owner backport-org --repo-name backport-demo --all +yarn start --branch 6.1 --repo backport-org/backport-demo --all ``` -or +**Run `backport` CLI globally** +This will build backport continously and link it, so it can accessed with `backport` command globally ``` -# Compile -yarn tsc - -# Run -node dist/entrypoint.cli.js --branch 6.1 --repo-owner backport-org --repo-name backport-demo --all +yarn tsc && sudo chmod +x bin/backport && yarn link && yarn tsc --watch ``` -**Run `backport` CLI globally** - +**Remove linked backport** ``` -yarn global remove backport -npm -g uninstall backport -yarn unlink backport -yarn unlink -yarn link -sudo chmod +x dist/entrypoint.cli.js -yarn tsc --watch +yarn global remove backport; npm -g uninstall backport; yarn unlink backport; yarn unlink; ``` You can now use `backport` command anywhere, and it'll point to the development version. diff --git a/bin/backport b/bin/backport new file mode 100755 index 00000000..c13f9022 --- /dev/null +++ b/bin/backport @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../dist/entrypoint.cli.js') diff --git a/package.json b/package.json index 3d21b66e..8c2c8213 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "main": "./dist/entrypoint.module.js", "types": "dist/entrypoint.module.d.ts", "bin": { - "backport": "./dist/entrypoint.cli.js" + "backport": "./bin/backport" }, "license": "MIT", "scripts": { diff --git a/src/entrypoint.cli.e2e.private.test.ts b/src/entrypoint.cli.e2e.private.test.ts index d9cb3f15..7410e437 100644 --- a/src/entrypoint.cli.e2e.private.test.ts +++ b/src/entrypoint.cli.e2e.private.test.ts @@ -265,6 +265,77 @@ describe('inquirer cli', () => { `); }); + describe('repo: repo-with-backportrc-removed (missing .backportrc.json config file)', () => { + it('should list commits', async () => { + const output = await runBackportAsync( + [ + '--branch', + 'foo', + '--repo', + 'backport-org/repo-with-backportrc-removed', + '--accessToken', + devAccessToken, + ], + { waitForString: 'Select commit' } + ); + + expect(output).toMatchInlineSnapshot(` + "? Select commit (Use arrow keys) + ❯ 1. Rename README.me to README.md + 2. Merge pull request #1 from backport-org/add-readme + 3. Create README.me + 4. Delete .backportrc.json + 5. Create .backportrc.json + 6. Delete .backportrc.json + 7. Create .backportrc.json" + `); + }); + + it('should attempt to backport by PR', async () => { + const output = await runBackportAsync( + [ + '--branch', + 'foo', + '--repo', + 'backport-org/repo-with-backportrc-removed', + '--pr', + '1', + '--accessToken', + devAccessToken, + ], + { waitForString: "is invalid or doesn't exist" } + ); + + expect(output).toMatchInlineSnapshot(` + " + Backporting to foo: + The branch \\"foo\\" is invalid or doesn't exist" + `); + }); + + it('should attempt to backport by commit sha', async () => { + const output = await runBackportAsync( + [ + '--branch', + 'foo', + '--repo', + 'backport-org/repo-with-backportrc-removed', + '--sha', + 'be59df6912a550c8cb49ba3e18be3e512f3d608c', + '--accessToken', + devAccessToken, + ], + { waitForString: "is invalid or doesn't exist" } + ); + + expect(output).toMatchInlineSnapshot(` + " + Backporting to foo: + The branch \\"foo\\" is invalid or doesn't exist" + `); + }); + }); + describe('repo: different-merge-strategies', () => { it('list all commits regardless how they were merged', async () => { jest.setTimeout(TIMEOUT_IN_SECONDS * 1000 * 1.1); @@ -278,19 +349,30 @@ describe('inquirer cli', () => { 'different-merge-strategies', '--accessToken', devAccessToken, + '-n', + '20', ], { waitForString: 'Select commit' } ); expect(output).toMatchInlineSnapshot(` "? Select commit (Use arrow keys) - ❯ 1. Using squash to merge commits (#3) 7.x - 2. Rebase strategy: Second commit 7.x - 3. Rebase strategy: First commit - 4. Merge pull request #1 from backport-org/merge-strategy - 5. Merge strategy: Second commit - 6. Merge strategy: First commit - 7. Initial commit" + ❯ 1. Merge pull request #9 from backport-org/many-merge-commits + 2. Merge strategy: Eighth of many merges + 3. Merge strategy: Seventh of many merges + 4. Merge strategy: Sixth of many merges + 5. Merge strategy: Fifth of many merges + 6. Merge strategy: Fourth of many merges + 7. Merge strategy: Third of many merges + 8. Merge strategy: Second of many merges + 9. Merge strategy: First of many merges + 10.Using squash to merge commits (#3) 7.x + 11.Rebase strategy: Second commit 7.x + 12.Rebase strategy: First commit + 13.Merge pull request #1 from backport-org/merge-strategy + 14.Merge strategy: Second commit + 15.Merge strategy: First commit + 16.Initial commit" `); }); }); diff --git a/src/runSequentially.test.ts b/src/runSequentially.test.ts index 01b7a81d..71a6da01 100644 --- a/src/runSequentially.test.ts +++ b/src/runSequentially.test.ts @@ -92,7 +92,7 @@ describe('runSequentially', () => { rpcExecMock = jest .spyOn(childProcess, 'exec') - .mockResolvedValue({ stdout: 'success', stderr: '' }); + .mockResolvedValue({ stdout: '', stderr: '' }); rpcExecOriginalMock = jest.spyOn(childProcess, 'execAsCallback'); const scope = nock('https://api.github.com') @@ -176,6 +176,7 @@ describe('runSequentially', () => { '/myHomeDir/.backport/repositories/elastic/kibana', '/myHomeDir/.backport/repositories/elastic/kibana', '/myHomeDir/.backport/repositories/elastic/kibana', + '/myHomeDir/.backport/repositories/elastic/kibana', ]); }); @@ -189,6 +190,7 @@ describe('runSequentially', () => { 'git remote add elastic https://x-access-token:myAccessToken@github.com/elastic/kibana.git', 'git reset --hard && git clean -d --force && git fetch elastic 7.x && git checkout -B backport/7.x/pr-55 elastic/7.x --no-track', 'git fetch elastic master:master --force', + 'git rev-list -1 --merges abcd~1..abcd', 'git cherry-pick -x abcd', 'git push sqren_authenticated backport/7.x/pr-55:backport/7.x/pr-55 --force', 'git reset --hard && git checkout my-source-branch-from-options && git branch -D backport/7.x/pr-55', diff --git a/src/runSequentially.ts b/src/runSequentially.ts index 6e7b7980..a978f729 100755 --- a/src/runSequentially.ts +++ b/src/runSequentially.ts @@ -52,6 +52,7 @@ export async function runSequentially({ pullRequestNumber: number, }); } catch (e) { + process.exitCode = 1; results.push({ targetBranch, status: 'failure', diff --git a/src/services/git.integration.test.ts b/src/services/git.integration.test.ts index eabb5e65..a13be4ac 100644 --- a/src/services/git.integration.test.ts +++ b/src/services/git.integration.test.ts @@ -7,8 +7,10 @@ import * as childProcess from './child-process-promisified'; import { cherrypick, cloneRepo, + getCommitsInMergeCommit, getIsCommitInBranch, getSourceRepoPath, + getIsMergeCommit, } from './git'; import { getShortSha } from './github/commitFormatters'; import { RepoOwnerAndNameResponse } from './github/v4/getRepoOwnerAndNameFromGitRemotes'; @@ -16,37 +18,6 @@ import { RepoOwnerAndNameResponse } from './github/v4/getRepoOwnerAndNameFromGit jest.unmock('del'); jest.unmock('make-dir'); -async function createAndCommitFile({ - filename, - content, - execOpts, -}: { - filename: string; - content: string; - execOpts: { cwd: string }; -}) { - await childProcess.exec(`echo "${content}" > "${filename}"`, execOpts); - await childProcess.exec( - `git add -A && git commit -m 'Update ${filename}'`, - execOpts - ); - - return getCurrentSha(execOpts); -} - -async function getCurrentSha(execOpts: { cwd: string }) { - const { stdout } = await childProcess.exec('git rev-parse HEAD', execOpts); - return stdout.trim(); -} - -async function getCurrentMessage(execOpts: { cwd: string }) { - const { stdout } = await childProcess.exec( - 'git --no-pager log -1 --pretty=%B', - execOpts - ); - return stdout.trim(); -} - describe('git.integration', () => { describe('getIsCommitInBranch', () => { let firstSha: string; @@ -324,8 +295,129 @@ describe('git.integration', () => { ); }); }); + + describe('when cloning "backport-org/different-merge-strategies"', () => { + const MERGE_COMMIT_HASH_1 = 'bdc5a17f81e5f32129e27b05c742e055c650bc54'; + const MERGE_COMMIT_HASH_2 = '0db7f1ac1233461563d8708511d1c14adbab46da'; + const SQUASH_COMMIT_HASH = '74a76fa64b34e3ffe8f2a3f73840e1b42fd07299'; + const REBASE_COMMIT_HASH = '9059ae0ca31caa2eebc035f2542842d6c2fde83b'; + + let sandboxPath: string; + beforeAll(async () => { + sandboxPath = getSandboxPath({ + filename: __filename, + specname: 'different-merge-strategies', + }); + await resetSandbox(sandboxPath); + + await childProcess.exec( + 'git clone https://github.com/backport-org/different-merge-strategies.git ./', + { cwd: sandboxPath } + ); + }); + + describe('getIsMergeCommit', () => { + it('returns true for first merge commit', async () => { + const res = await getIsMergeCommit( + { dir: sandboxPath } as ValidConfigOptions, + MERGE_COMMIT_HASH_1 + ); + + expect(res).toBe(true); + }); + + it('returns true for second merge commit', async () => { + const res = await getIsMergeCommit( + { dir: sandboxPath } as ValidConfigOptions, + MERGE_COMMIT_HASH_2 + ); + + expect(res).toBe(true); + }); + + it('returns false for rebased commits', async () => { + const res = await getIsMergeCommit( + { dir: sandboxPath } as ValidConfigOptions, + REBASE_COMMIT_HASH + ); + + expect(res).toBe(false); + }); + + it('returns false for squashed commits', async () => { + const res = await getIsMergeCommit( + { dir: sandboxPath } as ValidConfigOptions, + SQUASH_COMMIT_HASH + ); + + expect(res).toBe(false); + }); + }); + + describe('getCommitsInMergeCommit', () => { + it('returns a list of commit hashes - excluding the merge hash itself', async () => { + const res = await getCommitsInMergeCommit( + { dir: sandboxPath } as ValidConfigOptions, + MERGE_COMMIT_HASH_1 + ); + + expect(res).not.toContain(MERGE_COMMIT_HASH_1); + + expect(res).toEqual([ + 'f9a760e0d9eb3ebcc64f8cb75ce885714b836496', + '7b92e29e88266004485ce0fae0260605b01df887', + 'a1facf8c006fb815d6a6ecd1b2907e6e64f29576', + 'b8d4bcfb0fd875be4ab0230f6db40ddf72f45378', + '6f224054db5f7772b04f23f17659070216bae84c', + '7fed54cbd1ba9cb973462670ff82ac80bf8a79f8', + '78c24d0058859e7d511d10ce91ebf279c7b58ac2', + '709ccf707d443dd8c001b3c3ae40fdf037bb43f5', + ]); + }); + + it('returns empty for squash commits', async () => { + const res = await getCommitsInMergeCommit( + { dir: sandboxPath } as ValidConfigOptions, + SQUASH_COMMIT_HASH + ); + + expect(res).toEqual([]); + }); + }); + }); }); +async function createAndCommitFile({ + filename, + content, + execOpts, +}: { + filename: string; + content: string; + execOpts: { cwd: string }; +}) { + await childProcess.exec(`echo "${content}" > "${filename}"`, execOpts); + await childProcess.exec( + `git add -A && git commit -m 'Update ${filename}'`, + execOpts + ); + + return getCurrentSha(execOpts); +} + +async function getCurrentSha(execOpts: { cwd: string }) { + const { stdout } = await childProcess.exec('git rev-parse HEAD', execOpts); + return stdout.trim(); +} + +async function getCurrentMessage(execOpts: { cwd: string }) { + const { stdout } = await childProcess.exec( + 'git --no-pager log -1 --pretty=%B', + execOpts + ); + return stdout.trim(); +} + function mockRepoOwnerAndName({ repoName, parentRepoOwner, diff --git a/src/services/git.test.ts b/src/services/git.test.ts index 2febcac3..5e1713ee 100644 --- a/src/services/git.test.ts +++ b/src/services/git.test.ts @@ -335,7 +335,9 @@ describe('cherrypick', () => { jest .spyOn(childProcess, 'exec') - // mock cherry pick command + // mock getIsMergeCommit(...) + .mockResolvedValueOnce({ stderr: '', stdout: '' }) + // mock cherrypick(...) .mockResolvedValueOnce({ stderr: '', stdout: '' }); expect(await cherrypick(options, 'abcd')).toEqual({ @@ -349,6 +351,9 @@ describe('cherrypick', () => { const execSpy = jest .spyOn(childProcess, 'exec') + // mock getIsMergeCommit(...) + .mockResolvedValueOnce({ stderr: '', stdout: '' }) + // mock cherry pick command .mockResolvedValueOnce({ stderr: '', stdout: '' }); @@ -363,6 +368,9 @@ describe('cherrypick', () => { jest .spyOn(childProcess, 'exec') + // mock getIsMergeCommit(...) + .mockResolvedValueOnce({ stderr: '', stdout: '' }) + // mock cherry pick command .mockRejectedValueOnce( new ExecError({ @@ -405,6 +413,9 @@ describe('cherrypick', () => { jest .spyOn(childProcess, 'exec') + // mock getIsMergeCommit(...) + .mockResolvedValueOnce({ stderr: '', stdout: '' }) + // mock cherry pick command .mockRejectedValueOnce( new ExecError({ @@ -434,6 +445,9 @@ Or refer to the git documentation for more information: https://git-scm.com/docs jest .spyOn(childProcess, 'exec') + // mock getIsMergeCommit(...) + .mockResolvedValueOnce({ stderr: '', stdout: '' }) + // mock cherry pick command .mockRejectedValueOnce( new ExecError({ @@ -457,6 +471,9 @@ Or refer to the git documentation for more information: https://git-scm.com/docs jest .spyOn(childProcess, 'exec') + // mock getIsMergeCommit(...) + .mockResolvedValueOnce({ stderr: '', stdout: '' }) + // mock cherry pick command .mockRejectedValueOnce( new ExecError({ @@ -493,6 +510,9 @@ Or refer to the git documentation for more information: https://git-scm.com/docs jest .spyOn(childProcess, 'exec') + // mock getIsMergeCommit(...) + .mockResolvedValueOnce({ stderr: '', stdout: '' }) + // mock cherry pick command .mockRejectedValueOnce(new Error('non-cherrypick error')) diff --git a/src/services/git.ts b/src/services/git.ts index a91d13a4..fa61355f 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -1,5 +1,5 @@ import { resolve as pathResolve } from 'path'; -import { uniq, isEmpty } from 'lodash'; +import { uniq, isEmpty, first, last } from 'lodash'; import { ValidConfigOptions } from '../options/options'; import { ora } from '../ui/ora'; import { filterNil } from '../utils/filterEmpty'; @@ -195,6 +195,40 @@ export async function fetchBranch(options: ValidConfigOptions, branch: string) { }); } +export async function getIsMergeCommit( + options: ValidConfigOptions, + sha: string +) { + const res = await exec(`git rev-list -1 --merges ${sha}~1..${sha}`, { + cwd: getRepoPath(options), + }); + + return res.stdout !== ''; +} + +export async function getCommitsInMergeCommit( + options: ValidConfigOptions, + sha: string +) { + try { + const res = await exec( + `git --no-pager log ${sha}^1..${sha}^2 --pretty=format:"%H"`, + { + cwd: getRepoPath(options), + } + ); + + return res.stdout.split('\n'); + } catch (e) { + // swallow error + if (e.code === 128) { + return []; + } + + throw e; + } +} + export async function cherrypick( options: ValidConfigOptions, sha: string, @@ -209,9 +243,27 @@ export async function cherrypick( }> { const mainlinArg = options.mainline != undefined ? ` --mainline ${options.mainline}` : ''; - const cherrypickRefArg = options.cherrypickRef === false ? '' : ' -x'; - const cmd = `git cherry-pick${cherrypickRefArg}${mainlinArg} ${sha}`; + + let shaOrRange = sha; + + if (!options.mainline) { + try { + const isMergeCommit = await getIsMergeCommit(options, sha); + if (isMergeCommit) { + const shas = await getCommitsInMergeCommit(options, sha); + shaOrRange = `${last(shas)}^..${first(shas)}`; + } + } catch (e) { + // swallow error if it's a known error + // exit 128 will happen for many things, among others when the cherrypicked commit is empty + if (e.code !== 128) { + throw e; + } + } + } + const cmd = `git cherry-pick${cherrypickRefArg}${mainlinArg} ${shaOrRange}`; + try { await exec(cmd, { cwd: getRepoPath(options) }); return { conflictingFiles: [], unstagedFiles: [], needsResolving: false }; diff --git a/src/services/github/v4/apiRequestV4.ts b/src/services/github/v4/apiRequestV4.ts index 03a98230..8b68af27 100644 --- a/src/services/github/v4/apiRequestV4.ts +++ b/src/services/github/v4/apiRequestV4.ts @@ -111,13 +111,16 @@ type AxiosGithubResponse = AxiosResponse< any >; export class GithubV4Exception extends Error { + axiosResponse: AxiosGithubResponse & { request: undefined }; + constructor( public message: string, - public axiosResponse: AxiosGithubResponse + axiosResponse: AxiosGithubResponse ) { super(message); Error.captureStackTrace(this, HandledError); this.name = 'GithubV4Exception'; + this.axiosResponse = { ...axiosResponse, request: undefined }; } } diff --git a/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.ts b/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.ts index ad956893..af1a6b8d 100644 --- a/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.ts +++ b/src/services/github/v4/fetchCommits/fetchCommitByPullNumber.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { ValidConfigOptions } from '../../../../options/options'; import { HandledError } from '../../../HandledError'; +import { swallowMissingConfigFileException } from '../../../remoteConfig'; import { Commit, SourceCommitWithTargetPullRequest, @@ -44,16 +45,21 @@ export async function fetchCommitByPullNumber(options: { ${sourceCommitWithTargetPullRequestFragment} `; - const res = await apiRequestV4({ - githubApiBaseUrlV4, - accessToken, - query, - variables: { - repoOwner, - repoName, - pullNumber, - }, - }); + let res: CommitByPullNumberResponse; + try { + res = await apiRequestV4({ + githubApiBaseUrlV4, + accessToken, + query, + variables: { + repoOwner, + repoName, + pullNumber, + }, + }); + } catch (e) { + res = swallowMissingConfigFileException(e); + } const pullRequestNode = res.repository.pullRequest; if (!pullRequestNode) { diff --git a/src/services/github/v4/fetchCommits/fetchCommitBySha.ts b/src/services/github/v4/fetchCommits/fetchCommitBySha.ts index 6b4247df..9b51aa7e 100644 --- a/src/services/github/v4/fetchCommits/fetchCommitBySha.ts +++ b/src/services/github/v4/fetchCommits/fetchCommitBySha.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { ValidConfigOptions } from '../../../../options/options'; import { HandledError } from '../../../HandledError'; +import { swallowMissingConfigFileException } from '../../../remoteConfig'; import { Commit, SourceCommitWithTargetPullRequest, @@ -39,16 +40,21 @@ export async function fetchCommitBySha(options: { ${sourceCommitWithTargetPullRequestFragment} `; - const res = await apiRequestV4({ - githubApiBaseUrlV4, - accessToken, - query, - variables: { - repoOwner, - repoName, - sha, - }, - }); + let res: CommitsByShaResponse; + try { + res = await apiRequestV4({ + githubApiBaseUrlV4, + accessToken, + query, + variables: { + repoOwner, + repoName, + sha, + }, + }); + } catch (e) { + res = swallowMissingConfigFileException(e); + } const sourceCommit = res.repository.object; if (!sourceCommit) { diff --git a/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.ts b/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.ts index 72897297..f3c68075 100644 --- a/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.ts +++ b/src/services/github/v4/fetchCommits/fetchCommitsByAuthor.ts @@ -3,6 +3,7 @@ import { isEmpty, uniqBy, orderBy } from 'lodash'; import { ValidConfigOptions } from '../../../../options/options'; import { filterNil } from '../../../../utils/filterEmpty'; import { HandledError } from '../../../HandledError'; +import { swallowMissingConfigFileException } from '../../../remoteConfig'; import { Commit, SourceCommitWithTargetPullRequest, @@ -12,7 +13,7 @@ import { import { apiRequestV4 } from '../apiRequestV4'; import { fetchAuthorId } from '../fetchAuthorId'; -function fetchByCommitPath({ +async function fetchByCommitPath({ options, authorId, commitPath, @@ -89,12 +90,16 @@ function fetchByCommitPath({ dateUntil, }; - return apiRequestV4({ - githubApiBaseUrlV4, - accessToken, - query, - variables, - }); + try { + return await apiRequestV4({ + githubApiBaseUrlV4, + accessToken, + query, + variables, + }); + } catch (e) { + return swallowMissingConfigFileException(e); + } } export async function fetchCommitsByAuthor(options: { diff --git a/src/services/github/v4/fetchCommits/fetchPullRequestBySearchQuery.ts b/src/services/github/v4/fetchCommits/fetchPullRequestBySearchQuery.ts index 245bc231..4555b079 100644 --- a/src/services/github/v4/fetchCommits/fetchPullRequestBySearchQuery.ts +++ b/src/services/github/v4/fetchCommits/fetchPullRequestBySearchQuery.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag'; import { isEmpty } from 'lodash'; import { ValidConfigOptions } from '../../../../options/options'; import { HandledError } from '../../../HandledError'; +import { swallowMissingConfigFileException } from '../../../remoteConfig'; import { Commit, SourceCommitWithTargetPullRequest, @@ -47,12 +48,18 @@ export async function fetchPullRequestBySearchQuery( query: searchQuery, maxNumber: maxNumber, }; - const res = await apiRequestV4({ - githubApiBaseUrlV4, - accessToken, - query, - variables, - }); + + let res; + try { + res = await apiRequestV4({ + githubApiBaseUrlV4, + accessToken, + query, + variables, + }); + } catch (e) { + res = swallowMissingConfigFileException(e); + } const commits = res.search.nodes.map((pullRequestNode) => { const sourceCommit = pullRequestNode.mergeCommit; diff --git a/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.ts b/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.ts index b3e1e8a1..ef656c1e 100644 --- a/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.ts +++ b/src/services/github/v4/getOptionsFromGithub/getOptionsFromGithub.ts @@ -6,7 +6,10 @@ import { isLocalConfigFileModified, } from '../../../git'; import { logger } from '../../../logger'; -import { parseRemoteConfig } from '../../../remoteConfig'; +import { + parseRemoteConfig, + swallowMissingConfigFileException, +} from '../../../remoteConfig'; import { apiRequestV4, GithubV4Exception } from '../apiRequestV4'; import { throwOnInvalidAccessToken } from '../throwOnInvalidAccessToken'; import { GithubConfigOptionsResponse, query } from './query'; @@ -44,9 +47,8 @@ export async function getOptionsFromGithub(options: { throw e; } - const error = e as GithubV4Exception; - throwOnInvalidAccessToken({ error, repoName, repoOwner }); - res = swallowErrorIfConfigFileIsMissing(error); + throwOnInvalidAccessToken({ error: e, repoName, repoOwner }); + res = swallowMissingConfigFileException(e); } // it is not possible to have a branch named "backport" @@ -123,19 +125,3 @@ async function getRemoteConfigFileOptions( return parseRemoteConfig(remoteConfig); } - -function swallowErrorIfConfigFileIsMissing(error: GithubV4Exception) { - const { data, errors } = error.axiosResponse.data; - - const missingConfigError = errors?.some((error) => { - return error.path.includes('remoteConfig') && error.type === 'NOT_FOUND'; - }); - - // swallow error if it's just the config file that's missing - if (missingConfigError && data != null) { - return data; - } - - // Throw unexpected error - throw error; -} diff --git a/src/services/remoteConfig.ts b/src/services/remoteConfig.ts index f0ec45c7..db89b8bc 100644 --- a/src/services/remoteConfig.ts +++ b/src/services/remoteConfig.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { ConfigFileOptions } from '../entrypoint.module'; import { withConfigMigrations } from '../options/config/readConfigFile'; +import { GithubV4Exception } from './github/v4/apiRequestV4'; import { logger } from './logger'; export const RemoteConfigHistoryFragment = gql` @@ -49,3 +50,21 @@ export function parseRemoteConfig(remoteConfig: RemoteConfig) { return; } } + +export function swallowMissingConfigFileException( + error: GithubV4Exception +) { + const { data, errors } = error.axiosResponse.data; + + const missingConfigError = errors?.some((error) => { + return error.path.includes('remoteConfig') && error.type === 'NOT_FOUND'; + }); + + // swallow error if it's just the config file that's missing + if (missingConfigError && data != null) { + return data; + } + + // Throw unexpected error + throw error; +} diff --git a/src/ui/__snapshots__/cherrypickAndCreateTargetPullRequest.test.ts.snap b/src/ui/__snapshots__/cherrypickAndCreateTargetPullRequest.test.ts.snap index 1efa595e..a7d5ce33 100644 --- a/src/ui/__snapshots__/cherrypickAndCreateTargetPullRequest.test.ts.snap +++ b/src/ui/__snapshots__/cherrypickAndCreateTargetPullRequest.test.ts.snap @@ -14,6 +14,12 @@ Array [ "cwd": "/myHomeDir/.backport/repositories/elastic/kibana", }, ], + Array [ + "git rev-list -1 --merges mySha~1..mySha", + Object { + "cwd": "/myHomeDir/.backport/repositories/elastic/kibana", + }, + ], Array [ "git cherry-pick -x mySha", Object { @@ -115,6 +121,12 @@ Array [ "cwd": "/myHomeDir/.backport/repositories/elastic/kibana", }, ], + Array [ + "git rev-list -1 --merges mySha~1..mySha", + Object { + "cwd": "/myHomeDir/.backport/repositories/elastic/kibana", + }, + ], Array [ "git cherry-pick -x mySha", Object { @@ -127,6 +139,12 @@ Array [ "cwd": "/myHomeDir/.backport/repositories/elastic/kibana", }, ], + Array [ + "git rev-list -1 --merges mySha2~1..mySha2", + Object { + "cwd": "/myHomeDir/.backport/repositories/elastic/kibana", + }, + ], Array [ "git cherry-pick -x mySha2", Object { diff --git a/src/ui/cherrypickAndCreateTargetPullRequest.test.ts b/src/ui/cherrypickAndCreateTargetPullRequest.test.ts index 36b0c685..726f3278 100644 --- a/src/ui/cherrypickAndCreateTargetPullRequest.test.ts +++ b/src/ui/cherrypickAndCreateTargetPullRequest.test.ts @@ -490,6 +490,11 @@ function setupExecSpyForCherryPick() { return { stdout: ``, stderr: '' }; } + // getIsMergeCommit + if (cmd.startsWith('git rev-list -1 --merges')) { + return { stdout: ``, stderr: '' }; + } + throw new Error(`Missing exec mock for "${cmd}"`); }); } diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index d7a67395..10460ff8 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "include": ["**.js", "./**/*.ts", "./**/*.js"] + "include": ["**.js", "**/*.ts", "**/*.js"] } diff --git a/yarn.lock b/yarn.lock index 79d1d05f..86357508 100644 --- a/yarn.lock +++ b/yarn.lock @@ -649,11 +649,6 @@ tslib "~2.3.0" value-or-promise "1.0.11" -"@graphql-typed-document-node/core@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052" - integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg== - "@humanwhocodes/config-array@^0.9.2": version "0.9.2" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914" @@ -1331,14 +1326,6 @@ "@typescript-eslint/types" "5.11.0" eslint-visitor-keys "^3.0.0" -"@urql/core@^2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.4.1.tgz#bae449dafe98fb4944f3be61eb8e70ba33f8a46d" - integrity sha512-HnS54oNwO4pAACKl/2/tNLbRrxAxKawVJuG9UPiixqeEVekiecUQQnCjb9SpOW4Qr54HYzCMDbr3c5px3hfEEg== - dependencies: - "@graphql-typed-document-node/core" "^3.1.1" - wonka "^4.0.14" - abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -5103,11 +5090,6 @@ winston@^3.5.1: triple-beam "^1.3.0" winston-transport "^4.4.2" -wonka@^4.0.14: - version "4.0.15" - resolved "https://registry.yarnpkg.com/wonka/-/wonka-4.0.15.tgz#9aa42046efa424565ab8f8f451fcca955bf80b89" - integrity sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg== - word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"