Skip to content

Commit

Permalink
feat!: support multiple commits in footer (#686)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcoe committed Jan 8, 2021
1 parent a3a1df6 commit b3f96d8
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 61 deletions.
27 changes: 27 additions & 0 deletions README.md
Expand Up @@ -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.
Expand Down
57 changes: 57 additions & 0 deletions __snapshots__/conventional-commits.js
Expand Up @@ -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))
`
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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",
Expand Down
53 changes: 26 additions & 27 deletions src/conventional-commits.ts
Expand Up @@ -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 = '';
Expand Down Expand Up @@ -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<BumpSuggestion> {
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(
Expand Down
123 changes: 90 additions & 33 deletions src/util/to-conventional-changelog-format.ts
Expand Up @@ -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: '',
Expand All @@ -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;
Expand All @@ -59,22 +71,22 @@ export default function toConventionalChangelogFormat(
});

// <type>, "(", <scope>, ")", ["!"], ":", <whitespace>*, <text>
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;
Expand All @@ -83,10 +95,8 @@ export default function toConventionalChangelogFormat(

// [<any body-text except pre-footer>]
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;
});
}

Expand All @@ -104,39 +114,35 @@ 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 = '';
// We treat text from the BREAKING CHANGE marker forward as
// 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: [{
Expand Down Expand Up @@ -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;
}

0 comments on commit b3f96d8

Please sign in to comment.