diff --git a/packages/owl-bot/package.json b/packages/owl-bot/package.json index 7d524917d04..8e9c2c57699 100644 --- a/packages/owl-bot/package.json +++ b/packages/owl-bot/package.json @@ -18,7 +18,7 @@ "compile": "tsc -p .", "fix": "gts fix", "lint": "gts check", - "mocha": "c8 mocha build/test", + "mocha": "npm run compile && c8 mocha build/test", "test": "c8 mocha build/test && node ./build/src/bin/owl-bot.js --help", "system-test": "c8 mocha build/system-test", "pretest": "npm run compile", diff --git a/packages/owl-bot/src/bin/commands/copy-code-and-create-pull-request.ts b/packages/owl-bot/src/bin/commands/copy-code-and-create-pull-request.ts index 8ee6cb18b0c..73a3b88e2bf 100644 --- a/packages/owl-bot/src/bin/commands/copy-code-and-create-pull-request.ts +++ b/packages/owl-bot/src/bin/commands/copy-code-and-create-pull-request.ts @@ -19,12 +19,14 @@ import * as cc from '../../copy-code'; import {octokitFactoryFrom, OctokitParams} from '../../octokit-util'; import {githubRepoFromOwnerSlashName} from '../../github-repo'; import {FakeCopyStateStore} from '../../fake-copy-state-store'; +import {WithNestedCommitDelimiters} from '../../create-pr'; interface Args extends OctokitParams { 'source-repo': string; 'source-repo-commit-hash': string; 'dest-repo': string; 'dest-owlbot-yaml': string; + 'use-nested-commit-delimiters': boolean; } export const copyCodeAndCreatePullRequestCommand: yargs.CommandModule< @@ -74,6 +76,13 @@ export const copyCodeAndCreatePullRequestCommand: yargs.CommandModule< type: 'string', default: '.github/.OwlBot.yaml', demand: false, + }) + .option('use-nested-commit-delimiters', { + describe: + 'Whether to use BEGIN_NESTED_COMMIT delimiters when separating multiple commit messages', + type: 'boolean', + default: false, + demand: false, }); }, async handler(argv) { @@ -87,8 +96,13 @@ export const copyCodeAndCreatePullRequestCommand: yargs.CommandModule< copyStateStore: new FakeCopyStateStore(), octokitFactory, }; - await cc.copyCodeAndAppendOrCreatePullRequest(params, [ - argv['dest-owlbot-yaml'], - ]); + await cc.copyCodeAndAppendOrCreatePullRequest( + params, + [argv['dest-owlbot-yaml']], + undefined, + argv['use-nested-commit-delimiters'] + ? WithNestedCommitDelimiters.Yes + : WithNestedCommitDelimiters.No + ); }, }; diff --git a/packages/owl-bot/src/copy-code.ts b/packages/owl-bot/src/copy-code.ts index 713dc8c17c1..a0a359c8caa 100644 --- a/packages/owl-bot/src/copy-code.ts +++ b/packages/owl-bot/src/copy-code.ts @@ -29,12 +29,12 @@ import {OWL_BOT_COPY} from './core'; import {newCmd} from './cmd'; import { createPullRequestFromLastCommit, - EMPTY_REGENERATE_CHECKBOX_TEXT, Force, - REGENERATE_CHECKBOX_TEXT, resplit, WithRegenerateCheckbox, insertApiName, + prependCommitMessage, + WithNestedCommitDelimiters, } from './create-pr'; import {GithubRepo, githubRepoFromOwnerSlashName} from './github-repo'; import {CopyStateStore} from './copy-state-store'; @@ -280,7 +280,8 @@ export function branchNameForCopies(yamlPaths: string[]): string { async function findAndAppendPullRequest( params: WorkingCopyParams, yamlPaths: string[], - logger: Logger = console + logger: Logger = console, + withNestedCommitDelimiters: WithNestedCommitDelimiters = WithNestedCommitDelimiters.No ): Promise { const cmd = newCmd(logger); const octokit = await params.octokitFactory.getShortLivedOctokit(); @@ -372,14 +373,11 @@ async function findAndAppendPullRequest( }) .toString('utf8') .trim(); - const {title, body} = resplit( - `${commitBody}\n\n` + - `${pull.title}\n` + - pull.body - ?.replace(REGENERATE_CHECKBOX_TEXT, '') - .replace(EMPTY_REGENERATE_CHECKBOX_TEXT, '') - .trim() ?? '', - WithRegenerateCheckbox.Yes + const {title, body} = prependCommitMessage( + commitBody, + {title: pull.title, body: pull.body ?? ''}, + WithRegenerateCheckbox.Yes, + withNestedCommitDelimiters ); const apiNames = copiedYamls.map(tag => tag.yaml['api-name']).filter(Boolean); const apiList = abbreviateApiListForTitle(apiNames as string[]); @@ -422,7 +420,8 @@ interface WorkingCopyParams extends CopyParams { export async function copyCodeAndAppendOrCreatePullRequest( params: CopyParams, yamlPaths: string[], - logger: Logger = console + logger: Logger = console, + withNestedCommitDelimiters: WithNestedCommitDelimiters = WithNestedCommitDelimiters.No ): Promise { const workDir = tmp.dirSync().name; logger.info(`Working in ${workDir}`); @@ -449,7 +448,14 @@ export async function copyCodeAndAppendOrCreatePullRequest( const leftOvers: string[] = []; const wparams: WorkingCopyParams = {...params, destDir, workDir}; for (const yamlPath of yamlPaths) { - if (!(await findAndAppendPullRequest(wparams, [yamlPath], logger))) { + if ( + !(await findAndAppendPullRequest( + wparams, + [yamlPath], + logger, + withNestedCommitDelimiters + )) + ) { leftOvers.push(yamlPath); } } @@ -459,7 +465,14 @@ export async function copyCodeAndAppendOrCreatePullRequest( if (isDeepStrictEqual(yamlPaths, leftOvers)) { // Don't repeat exactly the same search } else { - if (await findAndAppendPullRequest(wparams, leftOvers, logger)) { + if ( + await findAndAppendPullRequest( + wparams, + leftOvers, + logger, + withNestedCommitDelimiters + ) + ) { return; // Appended a pull request for all the left-over APIs. Done. } } diff --git a/packages/owl-bot/src/create-pr.ts b/packages/owl-bot/src/create-pr.ts index 4600435d8e5..792c5c574c1 100644 --- a/packages/owl-bot/src/create-pr.ts +++ b/packages/owl-bot/src/create-pr.ts @@ -27,6 +27,11 @@ export const EMPTY_REGENERATE_CHECKBOX_TEXT = REGENERATE_CHECKBOX_TEXT.replace( '[ ]' ); +interface PullRequestContent { + title: string; + body: string; +} + /*** * Github will reject the pull request if the title is longer than 255 * characters. This function will move characters from the title to the body @@ -37,7 +42,7 @@ export const EMPTY_REGENERATE_CHECKBOX_TEXT = REGENERATE_CHECKBOX_TEXT.replace( export function resplit( rawBody: string, withRegenerateCheckbox: WithRegenerateCheckbox -): {title: string; body: string} { +): PullRequestContent { const regexp = /([^\r\n]*)([\r\n]*)((.|\r|\n)*)/; const match = regexp.exec(rawBody)!; let title = match[1]; @@ -53,6 +58,72 @@ export function resplit( return {title, body: body.substring(0, MAX_BODY_LENGTH)}; } +const NESTED_COMMIT_SEPARATOR = 'BEGIN_NESTED_COMMIT'; + +/** + * Given pull request content and a new commit message. Rewrite the pull request + * title and body using the newest message as the title. If the initial pull + * request title was truncated with ellipses, rejoin the title to the remaining part. + * + * For example, if the existing pull request is something like: + * Title: `feat: original feature` + * Body: `Copy-Tag: 1234` + * + * and we prepend a new message of `feat: another new feature\nCopy-Tag: 2345`, the + * output will be: + * Title: `feat: another new feature` + * Body: `Copy-Tag: 2345\nBEGIN_NESTED_COMMIT\nfeat: original feature\nCopy-Tag:1234\nEND_NESTED_COMMIT` + * + * @param {string} newCommitMessage the new commit message + * @param {PullRequestContent} existingContent exisiting pull request title and body + * @param {WithRegenerateCheckbox} withRegenerateCheckbox whether to include the + * checkbox to regenerate the pull request + */ +export function prependCommitMessage( + newCommitMessage: string, + existingContent: PullRequestContent, + withRegenerateCheckbox: WithRegenerateCheckbox, + withNestedCommitDelimiters: WithNestedCommitDelimiters = WithNestedCommitDelimiters.No +): PullRequestContent { + // remove any regenerate checkbox content and leading/trailing whitespace + const oldStrippedBody = existingContent.body + .replace(EMPTY_REGENERATE_CHECKBOX_TEXT, '') + .replace(REGENERATE_CHECKBOX_TEXT, '') + .trim(); + // if title was truncated, re-add it to the beginning of the commit message + const oldBody = existingContent.title.endsWith('...') + ? `${existingContent.title.substring( + 0, + existingContent.title.length - 3 + )}${oldStrippedBody}` + : `${existingContent.title}\n${oldStrippedBody}`; + if (withNestedCommitDelimiters === WithNestedCommitDelimiters.Yes) { + // anything before the first BEGIN_NESTED_COMMIT marker, is considered part of + // the previous commit + const bodyParts = oldBody + .split(NESTED_COMMIT_SEPARATOR) + .map(part => part.trim()); + const oldBodyWithNestedCommitMarkers = + bodyParts.length === 1 + ? // there is a single commit -- wrap the old body in the nested commit tags + `${NESTED_COMMIT_SEPARATOR}\n${oldBody}\nEND_NESTED_COMMIT` + : // there are already existing nested commit tags, content before the first + // one is wrapped in a new nested commit tag + `${NESTED_COMMIT_SEPARATOR}\n${ + bodyParts[0] + }\nEND_NESTED_COMMIT\n${NESTED_COMMIT_SEPARATOR}\n${bodyParts + .slice(1) + .join(NESTED_COMMIT_SEPARATOR)}`; + + // prepend the new commit message and use original title truncation logic + return resplit( + `${newCommitMessage}\n\n${oldBodyWithNestedCommitMarkers}`, + withRegenerateCheckbox + ); + } + return resplit(`${newCommitMessage}\n\n${oldBody}`, withRegenerateCheckbox); +} + // Exported for testing only. /** * Inserts an API name into a pr title after the first colon. @@ -101,6 +172,17 @@ export enum WithRegenerateCheckbox { No = 'no', } +/** + * Should createPullRequestFromLastCommit() separate multiple commit message + * bodies with `BEGIN_NESTED_COMMIT`/`END_NESTED_COMMIT`? + * + * More type safe and readable than a boolean. + */ +export enum WithNestedCommitDelimiters { + Yes = 'yes', + No = 'no', +} + /** * Creates a pull request using the title and commit message from the most * recent commit. diff --git a/packages/owl-bot/src/scan-googleapis-gen-and-create-pull-requests.ts b/packages/owl-bot/src/scan-googleapis-gen-and-create-pull-requests.ts index 880a884a738..9ea85ad66b8 100644 --- a/packages/owl-bot/src/scan-googleapis-gen-and-create-pull-requests.ts +++ b/packages/owl-bot/src/scan-googleapis-gen-and-create-pull-requests.ts @@ -27,6 +27,7 @@ import {newCmd} from './cmd'; import {CopyStateStore} from './copy-state-store'; import {GithubRepo} from './github-repo'; import {Logger, LoggerWithTimestamp} from './logger'; +import {WithNestedCommitDelimiters} from './create-pr'; interface Todo { repo: GithubRepo; @@ -79,7 +80,8 @@ export async function scanGoogleapisGenAndCreatePullRequests( cloneDepth = 100, copyStateStore: CopyStateStore, combinePullsThreshold = Number.MAX_SAFE_INTEGER, - logger: Logger = console + logger: Logger = console, + withNestedCommitDelimiters: WithNestedCommitDelimiters = WithNestedCommitDelimiters.No ): Promise { logger = new LoggerWithTimestamp(logger); // Clone the source repo. @@ -184,7 +186,12 @@ export async function scanGoogleapisGenAndCreatePullRequests( copyStateStore, octokitFactory, }; - await copyCodeAndAppendOrCreatePullRequest(params, todo.yamlPaths, logger); + await copyCodeAndAppendOrCreatePullRequest( + params, + todo.yamlPaths, + logger, + withNestedCommitDelimiters + ); } return todoStack.length; } diff --git a/packages/owl-bot/test/create-pr.ts b/packages/owl-bot/test/create-pr.ts index d0363e16f37..10cc3846c6c 100644 --- a/packages/owl-bot/test/create-pr.ts +++ b/packages/owl-bot/test/create-pr.ts @@ -21,8 +21,13 @@ import { MAX_TITLE_LENGTH, resplit, WithRegenerateCheckbox, + prependCommitMessage, + WithNestedCommitDelimiters, } from '../src/create-pr'; +const loremIpsum = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; + describe('resplit', () => { it('leaves a short title unchanged', () => { const tb = resplit('title\nbody\n', WithRegenerateCheckbox.No); @@ -37,9 +42,6 @@ describe('resplit', () => { }); }); - const loremIpsum = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; - it('resplits a long title', () => { const tb = resplit(loremIpsum + '\n\nbody', WithRegenerateCheckbox.No); assert.strictEqual(tb.title.length, MAX_TITLE_LENGTH); @@ -97,6 +99,253 @@ describe('resplit', () => { }); }); +describe('prependCommitMessage', () => { + describe('with checkbox', () => { + describe('with nested delimiters', () => { + it('handles an initial pull request content', () => { + const pullContent = resplit( + 'feat: some feature\n\nadditional context', + WithRegenerateCheckbox.Yes + ); + const prependedContent = prependCommitMessage( + 'fix: some new feature\n\nmore additional context', + pullContent, + WithRegenerateCheckbox.Yes, + WithNestedCommitDelimiters.Yes + ); + assert.strictEqual(prependedContent.title, 'fix: some new feature'); + assert.strictEqual( + prependedContent.body, + `- [ ] Regenerate this pull request now. + +more additional context + +BEGIN_NESTED_COMMIT +feat: some feature +additional context +END_NESTED_COMMIT` + ); + }); + + it('handles an initial pull request with long title', () => { + const pullContent = resplit(loremIpsum, WithRegenerateCheckbox.Yes); + const prependedContent = prependCommitMessage( + 'fix: some new feature\n\nmore additional context', + pullContent, + WithRegenerateCheckbox.Yes, + WithNestedCommitDelimiters.Yes + ); + assert.strictEqual(prependedContent.title, 'fix: some new feature'); + assert.strictEqual( + prependedContent.body, + `- [ ] Regenerate this pull request now. + +more additional context + +BEGIN_NESTED_COMMIT +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +END_NESTED_COMMIT` + ); + }); + + it('handles pull request already updated', () => { + const pullContent = resplit( + 'feat: some feature\n\nadditional context', + WithRegenerateCheckbox.Yes + ); + const prependedContent = prependCommitMessage( + 'fix: some new feature\n\nmore additional context', + pullContent, + WithRegenerateCheckbox.Yes, + WithNestedCommitDelimiters.Yes + ); + const prependedContent2 = prependCommitMessage( + 'fix: another new feature\n\nfurther context', + prependedContent, + WithRegenerateCheckbox.Yes, + WithNestedCommitDelimiters.Yes + ); + assert.strictEqual(prependedContent2.title, 'fix: another new feature'); + assert.strictEqual( + prependedContent2.body, + `- [ ] Regenerate this pull request now. + +further context + +BEGIN_NESTED_COMMIT +fix: some new feature +more additional context +END_NESTED_COMMIT +BEGIN_NESTED_COMMIT +feat: some feature +additional context +END_NESTED_COMMIT` + ); + }); + }); + describe('without nested delimiters', () => { + it('handles an initial pull request content', () => { + const pullContent = resplit( + 'feat: some feature\n\nadditional context', + WithRegenerateCheckbox.Yes + ); + const prependedContent = prependCommitMessage( + 'fix: some new feature\n\nmore additional context', + pullContent, + WithRegenerateCheckbox.Yes, + WithNestedCommitDelimiters.No + ); + assert.strictEqual(prependedContent.title, 'fix: some new feature'); + assert.strictEqual( + prependedContent.body, + `- [ ] Regenerate this pull request now. + +more additional context + +feat: some feature +additional context` + ); + }); + + it('handles an initial pull request with long title', () => { + const pullContent = resplit(loremIpsum, WithRegenerateCheckbox.Yes); + const prependedContent = prependCommitMessage( + 'fix: some new feature\n\nmore additional context', + pullContent, + WithRegenerateCheckbox.Yes, + WithNestedCommitDelimiters.No + ); + assert.strictEqual(prependedContent.title, 'fix: some new feature'); + assert.strictEqual( + prependedContent.body, + `- [ ] Regenerate this pull request now. + +more additional context + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.` + ); + }); + }); + }); + describe('without checkbox', () => { + describe('with nested delimiters', () => { + it('handles an initial pull request content', () => { + const pullContent = resplit( + 'feat: some feature\n\nadditional context', + WithRegenerateCheckbox.No + ); + const prependedContent = prependCommitMessage( + 'fix: some new feature\n\nmore additional context', + pullContent, + WithRegenerateCheckbox.No, + WithNestedCommitDelimiters.Yes + ); + assert.strictEqual(prependedContent.title, 'fix: some new feature'); + assert.strictEqual( + prependedContent.body, + `more additional context + +BEGIN_NESTED_COMMIT +feat: some feature +additional context +END_NESTED_COMMIT` + ); + }); + + it('handles an initial pull request with long title', () => { + const pullContent = resplit(loremIpsum, WithRegenerateCheckbox.No); + const prependedContent = prependCommitMessage( + 'fix: some new feature\n\nmore additional context', + pullContent, + WithRegenerateCheckbox.No, + WithNestedCommitDelimiters.Yes + ); + assert.strictEqual(prependedContent.title, 'fix: some new feature'); + assert.strictEqual( + prependedContent.body, + `more additional context + +BEGIN_NESTED_COMMIT +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +END_NESTED_COMMIT` + ); + }); + + it('handles pull request already updated', () => { + const pullContent = resplit( + 'feat: some feature\n\nadditional context', + WithRegenerateCheckbox.No + ); + const prependedContent = prependCommitMessage( + 'fix: some new feature\n\nmore additional context', + pullContent, + WithRegenerateCheckbox.No, + WithNestedCommitDelimiters.Yes + ); + const prependedContent2 = prependCommitMessage( + 'fix: another new feature\n\nfurther context', + prependedContent, + WithRegenerateCheckbox.No, + WithNestedCommitDelimiters.Yes + ); + assert.strictEqual(prependedContent2.title, 'fix: another new feature'); + assert.strictEqual( + prependedContent2.body, + `further context + +BEGIN_NESTED_COMMIT +fix: some new feature +more additional context +END_NESTED_COMMIT +BEGIN_NESTED_COMMIT +feat: some feature +additional context +END_NESTED_COMMIT` + ); + }); + }); + describe('without nested delimiters', () => { + it('handles an initial pull request content', () => { + const pullContent = resplit( + 'feat: some feature\n\nadditional context', + WithRegenerateCheckbox.No + ); + const prependedContent = prependCommitMessage( + 'fix: some new feature\n\nmore additional context', + pullContent, + WithRegenerateCheckbox.No, + WithNestedCommitDelimiters.No + ); + assert.strictEqual(prependedContent.title, 'fix: some new feature'); + assert.strictEqual( + prependedContent.body, + `more additional context + +feat: some feature +additional context` + ); + }); + + it('handles an initial pull request with long title', () => { + const pullContent = resplit(loremIpsum, WithRegenerateCheckbox.No); + const prependedContent = prependCommitMessage( + 'fix: some new feature\n\nmore additional context', + pullContent, + WithRegenerateCheckbox.No, + WithNestedCommitDelimiters.No + ); + assert.strictEqual(prependedContent.title, 'fix: some new feature'); + assert.strictEqual( + prependedContent.body, + `more additional context + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.` + ); + }); + }); + }); +}); + describe('insertApiName', () => { it('does nothing when the api name is empty', () => { const title = 'chore(bazel): Update gapic-generator-php to v1.2.1'; diff --git a/packages/owl-bot/test/scan-googleapis-gen-and-create-pull-requests.ts b/packages/owl-bot/test/scan-googleapis-gen-and-create-pull-requests.ts index fa1625d7fd0..b385ab73ba9 100644 --- a/packages/owl-bot/test/scan-googleapis-gen-and-create-pull-requests.ts +++ b/packages/owl-bot/test/scan-googleapis-gen-and-create-pull-requests.ts @@ -34,7 +34,10 @@ import { } from './fake-octokit'; import tmp from 'tmp'; import {copyTagFrom} from '../src/copy-code'; -import {EMPTY_REGENERATE_CHECKBOX_TEXT} from '../src/create-pr'; +import { + EMPTY_REGENERATE_CHECKBOX_TEXT, + WithNestedCommitDelimiters, +} from '../src/create-pr'; import {FakeCopyStateStore} from '../src/fake-copy-state-store'; // Use anys to mock parts of the octokit API. @@ -384,6 +387,78 @@ Copy-Tag: ${copyTag}` assert.strictEqual(prCount, 0); }); + it('copies files and appends a pull request with nested commit tags', async () => { + const [destRepo, configsStore] = makeDestRepoAndConfigsStore(bYaml); + + // Create a branch in the dest dir for the existing pull request. + const destDir = destRepo.getCloneUrl(); + cmd('git branch owl-bot-copy', {cwd: destDir}); + + // Create an existing pull request to be appended. + const pullBody = 'This is the greatest pull request ever.'; + const pulls = new FakePulls(); + pulls.create({ + owner: 'googleapis', + repo: 'nodejs-spell-check', + title: 'q', + body: pullBody, + head: 'owl-bot-copy', + }); + + const issues = new FakeIssues(); + const octokit = newFakeOctokit(pulls, issues); + const copyStateStore = new FakeCopyStateStore(); + await scanGoogleapisGenAndCreatePullRequests( + abcRepo, + factory(octokit), + configsStore, + undefined, + copyStateStore + ); + + // Confirm it updated the body. + const copyTag = cc.copyTagFrom('.github/.OwlBot.yaml', abcCommits[1]); + assert.strictEqual(pulls.updates.length, 1); + assert.deepStrictEqual(pulls.updates, [ + { + owner: 'googleapis', + repo: 'nodejs-spell-check', + pull_number: 1, + body: + '- [ ] Regenerate this pull request now.\n\n' + + `Source-Link: https://github.com/googleapis/googleapis-gen/commit/${abcCommits[1]}\n` + + `Copy-Tag: ${copyTag}\n\nq\nThis is the greatest pull request ever.`, + title: 'b', + }, + ]); + + // Confirm the pull request branch contains the new file. + cmd(`git checkout ${pulls.pulls[0].head}`, {cwd: destDir}); + const bpath = path.join(destDir, 'src', 'b.txt'); + assert.strictEqual(fs.readFileSync(bpath).toString('utf8'), '2'); + + // But of course the main branch doesn't have it until the PR is merged. + cmd('git checkout main', {cwd: destDir}); + assert.ok(!cc.stat(bpath)); + + // Confirm the PR was recorded in firestore. + assert.ok(await copyStateStore.findBuildForCopy(destRepo, copyTag)); + + // Because the PR is recorded in firestore, a second call should skip + // creating a new one. + const prCount = await scanGoogleapisGenAndCreatePullRequests( + abcRepo, + factory(octokit), + configsStore, + undefined, + copyStateStore, + undefined, + undefined, + WithNestedCommitDelimiters.Yes + ); + assert.strictEqual(prCount, 0); + }); + it('reports error for corrupt yaml in new pull request', async () => { const [destRepo, configsStore] = makeDestRepoAndConfigsStore(bYaml);