diff --git a/README.md b/README.md index f9f577fec..ed4dbd1c9 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,33 @@ The most important prefixes you should have in mind are: * `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the `!`) and will result in a SemVer major. +### What if my PR contains multiple fixes or features? + +Release Please allows you to represent multiple changes in a single commit, +using footers: + +``` +feat: adds v4 UUID to crypto + +This adds support for v4 UUIDs to the library. + +fix(utils): unicode no longer throws exception + PiperOrigin-RevId: 345559154 + BREAKING-CHANGE: encode method no longer throws. + Source-Link: googleapis/googleapis@5e0dcb2 + +feat(utils): update encode to support unicode + PiperOrigin-RevId: 345559182 + Source-Link: googleapis/googleapis@e5eef86 +``` + +The above commit message will contain: + +1. an entry for the **"adds v4 UUID to crypto"** feature. +2. an entry for the fix **"unicode no longer throws exception"**, along with a note + that it's a breaking change. +3. an entry for the feature **"update encode to support unicode"**. + ## How do I change the version number? When a commit to the main branch has `Release-As: x.x.x` in the **commit body**, Release Please will open a new pull request for the specified version. diff --git a/__snapshots__/conventional-commits.js b/__snapshots__/conventional-commits.js index 69b2509a3..d36ece339 100644 --- a/__snapshots__/conventional-commits.js +++ b/__snapshots__/conventional-commits.js @@ -73,3 +73,60 @@ exports['ConventionalCommits generateChangelogEntry supports additional markdown * upgrade to Node 7 ([abc345](https://www.github.com/bcoe/release-please/commit/abc345)) ` + +exports['ConventionalCommits generateChangelogEntry parses additional commits in footers 1'] = ` +## v1.0.0 (1665-10-10) + + +### ⚠ BREAKING CHANGES + +* cool feature + +### Features + +* awesome feature ([abc678](https://www.github.com/bcoe/release-please/commit/abc678)) +* cool feature ([abc345](https://www.github.com/bcoe/release-please/commit/abc345)) + + +### Bug Fixes + +* **subsystem:** also a fix ([abc345](https://www.github.com/bcoe/release-please/commit/abc345)) +` + +exports['ConventionalCommits generateChangelogEntry parses footer commits that contain footers 1'] = ` +## v1.0.0 (1665-10-10) + + +### ⚠ BREAKING CHANGES + +* **recaptchaenterprise:** for some reason this migration is breaking. + +### Features + +* awesome feature ([abc678](https://www.github.com/bcoe/release-please/commit/abc678)) +* **recaptchaenterprise:** migrate microgenertor ([abc345](https://www.github.com/bcoe/release-please/commit/abc345)) + + +### Bug Fixes + +* **securitycenter:** fixes security center. ([abc345](https://www.github.com/bcoe/release-please/commit/abc345)) +` + +exports['ConventionalCommits generateChangelogEntry parses commits from footer, when body contains multiple paragraphs 1'] = ` +## v1.0.0 (1665-10-10) + + +### ⚠ BREAKING CHANGES + +* **recaptchaenterprise:** for some reason this migration is breaking. + +### Features + +* **recaptchaenterprise:** migrate microgenertor ([abc345](https://www.github.com/bcoe/release-please/commit/abc345)) + + +### Bug Fixes + +* fixes bug [#733](https://www.github.com/bcoe/release-please/issues/733) ([abc345](https://www.github.com/bcoe/release-please/commit/abc345)) +* **securitycenter:** fixes security center. ([abc345](https://www.github.com/bcoe/release-please/commit/abc345)) +` diff --git a/package.json b/package.json index f14f277f1..41a2e6d17 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "typescript": "^3.8.3" }, "dependencies": { - "@conventional-commits/parser": "^0.3.0", + "@conventional-commits/parser": "^0.4.1", "@iarna/toml": "^2.2.5", "@octokit/graphql": "^4.3.1", "@octokit/request": "^5.3.4", diff --git a/src/conventional-commits.ts b/src/conventional-commits.ts index 3e633224f..cbe46401a 100644 --- a/src/conventional-commits.ts +++ b/src/conventional-commits.ts @@ -71,6 +71,30 @@ interface Note { text: string; } +function getParsedCommits(commits: Commit[]): CommitWithHash[] { + const parsedCommits: CommitWithHash[] = []; + for (const commit of commits) { + try { + for (const parsedCommit of toConventionalChangelogFormat( + parser(commit.message) + )) { + const commitWithHash = postProcessCommits( + parsedCommit + ) as CommitWithHash; + commitWithHash.hash = commit.sha; + parsedCommits.push(commitWithHash); + } + } catch (_err) { + // Commit is not in conventional commit format, it does not + // contribute to the CHANGELOG generation. + } + } + return parsedCommits; +} + +// TODO(@bcoe): now that we walk the actual AST of conventional commits +// we should be able to move post processing into +// to-conventional-changelog.ts. function postProcessCommits(commit: ConventionalChangelogCommit) { commit.notes.forEach(note => { let text = ''; @@ -179,41 +203,16 @@ export class ConventionalCommits { this.headerPartial || preset.writerOpts.headerPartial; preset.writerOpts.mainTemplate = this.mainTemplate || preset.writerOpts.mainTemplate; - const parsedCommits = []; - for (const commit of this.commits) { - try { - const parsedCommit = postProcessCommits( - toConventionalChangelogFormat(parser(commit.message)) - ) as CommitWithHash; - parsedCommit.hash = commit.sha; - parsedCommits.push(parsedCommit); - } catch (_err) { - // Commit is not in conventional commit format, it does not - // contribute to the CHANGELOG generation. - } - } const parsed: string = conventionalChangelogWriter - .parseArray(parsedCommits, context, preset.writerOpts) + .parseArray(getParsedCommits(this.commits), context, preset.writerOpts) .trim(); return parsed; } private async guessReleaseType(preMajor: boolean): Promise { const VERSIONS = ['major', 'minor', 'patch']; const preset = await presetFactory({preMajor}); - const parsedCommits = []; - for (const commit of this.commits) { - try { - const parsedCommit = toConventionalChangelogFormat( - parser(commit.message) - ); - parsedCommits.push(parsedCommit); - } catch (_err) { - // Commit is not in conventional commit format, it does not - // contribute to the CHANGELOG generation. - } - } const commits = conventionalCommitsFilter( - parsedCommits + getParsedCommits(this.commits) ) as ConventionalChangelogCommit; let result = preset.recommendedBumpOpts.whatBump( diff --git a/src/util/to-conventional-changelog-format.ts b/src/util/to-conventional-changelog-format.ts index 5ff15d405..4b20b5d59 100644 --- a/src/util/to-conventional-changelog-format.ts +++ b/src/util/to-conventional-changelog-format.ts @@ -19,18 +19,21 @@ const visitWithAncestors = require('unist-util-visit-parents'); const NUMBER_REGEX = /^[0-9]+$/; import * as parser from '@conventional-commits/parser'; -type SummaryNode = +type SummaryNodes = | parser.Type | parser.Scope | parser.BreakingChange | parser.Text; +type FooterNodes = + | parser.Type + | parser.Scope + | parser.BreakingChange + | parser.Separator + | parser.Text + | parser.Newline; -// Converts conventional commit AST into conventional-changelog's -// output format, see: https://www.npmjs.com/package/conventional-commits-parser -export default function toConventionalChangelogFormat( - ast: parser.Message -): parser.ConventionalChangelogCommit { - const cc: parser.ConventionalChangelogCommit = { +function getBlankConventionalCommit(): parser.ConventionalChangelogCommit { + return { body: '', subject: '', type: '', @@ -43,6 +46,15 @@ export default function toConventionalChangelogFormat( header: '', footer: null, }; +} + +// Converts conventional commit AST into conventional-changelog's +// output format, see: https://www.npmjs.com/package/conventional-commits-parser +export default function toConventionalChangelogFormat( + ast: parser.Message +): parser.ConventionalChangelogCommit[] { + const commits: parser.ConventionalChangelogCommit[] = []; + const headerCommit = getBlankConventionalCommit(); // Separate the body and summary nodes, this simplifies the subsequent // tree walking logic: let body; @@ -59,22 +71,22 @@ export default function toConventionalChangelogFormat( }); // , "(", , ")", ["!"], ":", *, - visit(summary, (node: SummaryNode) => { + visit(summary, (node: SummaryNodes) => { switch (node.type) { case 'type': - cc.type = node.value; - cc.header += node.value; + headerCommit.type = node.value; + headerCommit.header += node.value; break; case 'scope': - cc.scope = node.value; - cc.header += `(${node.value})`; + headerCommit.scope = node.value; + headerCommit.header += `(${node.value})`; break; case 'breaking-change': - cc.header += '!'; + headerCommit.header += '!'; break; case 'text': - cc.subject = node.value; - cc.header += `: ${node.value}`; + headerCommit.subject = node.value; + headerCommit.header += `: ${node.value}`; break; default: break; @@ -83,10 +95,8 @@ export default function toConventionalChangelogFormat( // [] if (body) { - visit(body, 'text', (node: parser.Text) => { - // TODO(@bcoe): once we have \n tokens in tree we can drop this: - if (cc.body !== '') cc.body += '\n'; - cc.body += node.value; + visit(body, ['text', 'newline'], (node: parser.Text) => { + headerCommit.body += node.value; }); } @@ -104,10 +114,9 @@ export default function toConventionalChangelogFormat( if (!parent) { return; } - let startCollecting = false; switch (parent.type) { case 'summary': - breaking.text = cc.subject; + breaking.text = headerCommit.subject; break; case 'body': breaking.text = ''; @@ -115,28 +124,25 @@ export default function toConventionalChangelogFormat( // the breaking change notes: visit( parent, - ['text', 'breaking-change'], + ['text', 'newline'], (node: parser.Text | parser.BreakingChange) => { - // TODO(@bcoe): once we have \n tokens in tree we can drop this: - if (startCollecting && node.type === 'text') { - if (breaking.text !== '') breaking.text += '\n'; - breaking.text += node.value; - } else if (node.type === 'breaking-change') { - startCollecting = true; - } + breaking.text += node.value; } ); break; case 'token': + // If the '!' breaking change marker is used, the breaking change + // will be identified when the footer is parsed as a commit: + if (!node.value.includes('BREAKING')) return; parent = ancestors.pop(); - visit(parent, 'text', (node: parser.Text) => { + visit(parent, ['text', 'newline'], (node: parser.Text) => { breaking.text = node.value; }); break; } } ); - if (breaking.text !== '') cc.notes.push(breaking); + if (breaking.text !== '') headerCommit.notes.push(breaking); // Populates references array from footers: // references: [{ @@ -183,9 +189,60 @@ export default function toConventionalChangelogFormat( ); // TODO(@bcoe): how should references like "Refs: v8:8940" work. if (hasRefSepartor && reference.issue.match(NUMBER_REGEX)) { - cc.references.push(reference); + headerCommit.references.push(reference); } }); - return cc; + /* + * Split footers that resemble commits into additional commits, e.g., + * chore: multiple commits + * chore(recaptchaenterprise): migrate recaptchaenterprise to the Java microgenerator + * Committer: @miraleung + * PiperOrigin-RevId: 345559154 + * ... + */ + visitWithAncestors( + ast, + ['type'], + (node: parser.Type, ancestors: parser.Node[]) => { + let parent = ancestors.pop(); + if (!parent) { + return; + } + if (parent.type === 'token') { + parent = ancestors.pop(); + let footerText = ''; + visit( + parent, + ['type', 'scope', 'breaking-change', 'separator', 'text', 'newline'], + (node: FooterNodes) => { + switch (node.type) { + case 'scope': + footerText += `(${node.value})`; + break; + case 'separator': + // Footers of the form Fixes #99, should not be parsed. + if (node.value.includes('#')) return; + footerText += `${node.value} `; + break; + default: + footerText += node.value; + break; + } + } + ); + try { + for (const commit of toConventionalChangelogFormat( + parser.parser(footerText) + )) { + commits.push(commit); + } + } catch (err) { + // Footer does not appear to be an additional commit. + } + } + } + ); + commits.push(headerCommit); + return commits; } diff --git a/test/conventional-commits.ts b/test/conventional-commits.ts index 471d64d1f..7527b6617 100644 --- a/test/conventional-commits.ts +++ b/test/conventional-commits.ts @@ -126,5 +126,89 @@ describe('ConventionalCommits', () => { }); snapshot(cl.replace(/[0-9]{4}-[0-9]{2}-[0-9]{2}/g, '1665-10-10')); }); + + it('parses additional commits in footers', async () => { + const cc = new ConventionalCommits({ + commits: [ + { + message: + 'chore: multiple commits\n\nfeat!: cool feature\nfix(subsystem): also a fix', + sha: 'abc345', + files: [], + }, + {message: 'feat: awesome feature', sha: 'abc678', files: []}, + ], + githubRepoUrl: 'https://github.com/bcoe/release-please.git', + bumpMinorPreMajor: true, + }); + const cl = await cc.generateChangelogEntry({ + version: 'v1.0.0', + }); + snapshot(cl.replace(/[0-9]{4}-[0-9]{2}-[0-9]{2}/g, '1665-10-10')); + }); + + it('parses footer commits that contain footers', async () => { + const cc = new ConventionalCommits({ + commits: [ + { + message: `meta: multiple commits. + +feat(recaptchaenterprise): migrate microgenertor + Committer: @miraleung + PiperOrigin-RevId: 345559154 + BREAKING-CHANGE: for some reason this migration is breaking. + Source-Link: googleapis/googleapis@5e0dcb2 + +fix(securitycenter): fixes security center. + Committer: @miraleung + PiperOrigin-RevId: 345559182 + Source-Link: googleapis/googleapis@e5eef86`, + sha: 'abc345', + files: [], + }, + {message: 'feat: awesome feature', sha: 'abc678', files: []}, + ], + githubRepoUrl: 'https://github.com/bcoe/release-please.git', + bumpMinorPreMajor: true, + }); + const cl = await cc.generateChangelogEntry({ + version: 'v1.0.0', + }); + snapshot(cl.replace(/[0-9]{4}-[0-9]{2}-[0-9]{2}/g, '1665-10-10')); + }); + + it('parses commits from footer, when body contains multiple paragraphs', async () => { + const cc = new ConventionalCommits({ + commits: [ + { + message: `meta: multiple commits. + +Details. + +Some clarifying facts. + +fix: fixes bug #733 +feat(recaptchaenterprise): migrate microgenertor + Committer: @miraleung + PiperOrigin-RevId: 345559154 + BREAKING-CHANGE: for some reason this migration is breaking. + Source-Link: goo gleapis/googleapis@5e0dcb2 + +fix(securitycenter): fixes security center. + Committer: @miraleung + PiperOrigin-RevId: 345559182 + Source-Link: googleapis/googleapis@e5eef86`, + sha: 'abc345', + files: [], + }, + ], + githubRepoUrl: 'https://github.com/bcoe/release-please.git', + bumpMinorPreMajor: true, + }); + const cl = await cc.generateChangelogEntry({ + version: 'v1.0.0', + }); + snapshot(cl.replace(/[0-9]{4}-[0-9]{2}-[0-9]{2}/g, '1665-10-10')); + }); }); });