Skip to content

Commit

Permalink
feat: introduce processCommits hook for plugins (#1607)
Browse files Browse the repository at this point in the history
feat: introduce `sentence-case` plugin that capitalizes commit messages
  • Loading branch information
bcoe committed Aug 31, 2022
1 parent 95b4488 commit 414eb5f
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 1 deletion.
16 changes: 16 additions & 0 deletions docs/manifest-releaser.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,19 @@ Example:
]
}
```

### sentence-case

Capitalize the leading word in a commit message, taking into account common exceptions, e.g., gRPC.

As an example:

`fix: patch issues in OpenSSL`

Will be output in the CHANGELOG thusly:

```
Bug Fixes:
* Patch issues in OpenSSL`
```
7 changes: 7 additions & 0 deletions schemas/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,13 @@
"merge": {
"description": "Whether to merge in-scope pull requests into a combined release pull request. Defaults to `true`.",
"type": "boolean"
},
"specialWords": {
"description": "Words that sentence casing logic will not be applied to",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["type", "groupName", "components"]
Expand Down
9 changes: 9 additions & 0 deletions src/factories/plugin-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
LinkedVersionPluginConfig,
PluginType,
RepositoryConfig,
SentenceCasePluginConfig,
} from '../manifest';
import {GitHub} from '../github';
import {ManifestPlugin} from '../plugin';
Expand All @@ -25,6 +26,7 @@ import {NodeWorkspace} from '../plugins/node-workspace';
import {VersioningStrategyType} from './versioning-strategy-factory';
import {MavenWorkspace} from '../plugins/maven-workspace';
import {ConfigurationError} from '../errors';
import {SentenceCase} from '../plugins/sentence-case';

export interface PluginFactoryOptions {
type: PluginType;
Expand Down Expand Up @@ -72,6 +74,13 @@ const pluginFactories: Record<string, PluginBuilder> = {
options.repositoryConfig,
options
),
'sentence-case': options =>
new SentenceCase(
options.github,
options.targetBranch,
options.repositoryConfig,
(options.type as SentenceCasePluginConfig).specialWords
),
};

export function buildPlugin(options: PluginFactoryOptions): ManifestPlugin {
Expand Down
12 changes: 11 additions & 1 deletion src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,17 @@ export interface LinkedVersionPluginConfig extends ConfigurablePluginType {
components: string[];
merge?: boolean;
}
export interface SentenceCasePluginConfig extends ConfigurablePluginType {
specialWords?: string[];
}
export interface WorkspacePluginConfig extends ConfigurablePluginType {
merge?: boolean;
}
export type PluginType =
| DirectPluginType
| ConfigurablePluginType
| LinkedVersionPluginConfig
| SentenceCasePluginConfig
| WorkspacePluginConfig;

/**
Expand Down Expand Up @@ -652,7 +656,13 @@ export class Manifest {
);
this.logger.debug(`type: ${config.releaseType}`);
this.logger.debug(`targetBranch: ${this.targetBranch}`);
const pathCommits = commitsPerPath[path];
let pathCommits = commitsPerPath[path];
// The processCommits hook can be implemented by plugins to
// post-process commits. This can be used to perform cleanup, e.g,, sentence
// casing all commit messages:
for (const plugin of plugins) {
pathCommits = plugin.processCommits(pathCommits);
}
this.logger.debug(`commits: ${pathCommits.length}`);
const latestReleasePullRequest =
releasePullRequestsBySha[releaseShasByPath[path]];
Expand Down
11 changes: 11 additions & 0 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export abstract class ManifestPlugin {
this.logger = logger;
}

/**
* Perform post-processing on commits, e.g, sentence casing them.
* @param {Commit[]} commits The set of commits that will feed into release pull request.
* @returns {Commit[]} The modified commit objects.
*/
// TODO: for next major version, let's run the default conventional commit parser earlier
// (outside of the strategy classes) and pass in the ConventionalCommit[] objects in.
processCommits(commits: Commit[]): Commit[] {
return commits;
}

/**
* Post-process candidate pull requests.
* @param {CandidateReleasePullRequest[]} pullRequests Candidate pull requests
Expand Down
86 changes: 86 additions & 0 deletions src/plugins/sentence-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {ManifestPlugin} from '../plugin';
import {GitHub} from '../github';
import {RepositoryConfig} from '../manifest';
import {Commit} from '../commit';

// A list of words that should not be converted to uppercase:
const SPECIAL_WORDS = ['gRPC', 'npm'];

/**
* This plugin converts commit messages to sentence case, for the benefit
* of the generated CHANGELOG.
*/
export class SentenceCase extends ManifestPlugin {
specialWords: Set<string>;
constructor(
github: GitHub,
targetBranch: string,
repositoryConfig: RepositoryConfig,
specialWords?: Array<string>
) {
super(github, targetBranch, repositoryConfig);
this.specialWords = new Set(
specialWords ? [...specialWords] : SPECIAL_WORDS
);
}

/**
* Perform post-processing on commits, e.g, sentence casing them.
* @param {Commit[]} commits The set of commits that will feed into release pull request.
* @returns {Commit[]} The modified commit objects.
*/
processCommits(commits: Commit[]): Commit[] {
this.logger.info(`SentenceCase processing ${commits.length} commits`);
for (const commit of commits) {
// Check whether commit is in conventional commit format, if it is
// we'll split the string by type and description:
if (commit.message.includes(':')) {
let [prefix, suffix] = commit.message.split(':');
prefix += ': ';
suffix = suffix.trim();
// Extract the first word from the rest of the string:
const match = /\s|$/.exec(suffix);
if (match) {
const endFirstWord = match.index;
const firstWord = suffix.slice(0, endFirstWord);
suffix = suffix.slice(endFirstWord);
// Put the string back together again:
commit.message = `${prefix}${this.toUpperCase(firstWord)}${suffix}`;
}
}
}
return commits;
}

/*
* Convert a string to upper case, taking into account a dictionary of
* common lowercase words, e.g., gRPC, npm.
*
* @param {string} word The original word.
* @returns {string} The word, now upper case.
*/
toUpperCase(word: string): string {
if (this.specialWords.has(word)) {
return word;
}
if (word.match(/^[a-z]/)) {
return word.charAt(0).toUpperCase() + word.slice(1);
} else {
return word;
}
}
}
35 changes: 35 additions & 0 deletions test/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {PullRequest} from '../src/pull-request';
import {readFileSync} from 'fs';
import {resolve} from 'path';
import * as pluginFactory from '../src/factories/plugin-factory';
import {SentenceCase} from '../src/plugins/sentence-case';
import {NodeWorkspace} from '../src/plugins/node-workspace';
import {CargoWorkspace} from '../src/plugins/cargo-workspace';
import {PullRequestTitle} from '../src/util/pull-request-title';
Expand Down Expand Up @@ -2678,6 +2679,7 @@ describe('Manifest', () => {
const mockPlugin = sandbox.createStubInstance(NodeWorkspace);
mockPlugin.run.returnsArg(0);
mockPlugin.preconfigure.returnsArg(0);
mockPlugin.processCommits.returnsArg(0);
sandbox
.stub(pluginFactory, 'buildPlugin')
.withArgs(sinon.match.has('type', 'node-workspace'))
Expand Down Expand Up @@ -2715,9 +2717,11 @@ describe('Manifest', () => {
const mockPlugin = sandbox.createStubInstance(NodeWorkspace);
mockPlugin.run.returnsArg(0);
mockPlugin.preconfigure.returnsArg(0);
mockPlugin.processCommits.returnsArg(0);
const mockPlugin2 = sandbox.createStubInstance(CargoWorkspace);
mockPlugin2.run.returnsArg(0);
mockPlugin2.preconfigure.returnsArg(0);
mockPlugin2.processCommits.returnsArg(0);
sandbox
.stub(pluginFactory, 'buildPlugin')
.withArgs(sinon.match.has('type', 'node-workspace'))
Expand All @@ -2729,6 +2733,37 @@ describe('Manifest', () => {
sinon.assert.calledOnce(mockPlugin.run);
sinon.assert.calledOnce(mockPlugin2.run);
});

it('should apply plugin hook "processCommits"', async () => {
const manifest = new Manifest(
github,
'main',
{
'path/a': {
releaseType: 'node',
component: 'pkg1',
packageName: 'pkg1',
},
},
{
'path/a': Version.parse('1.0.0'),
},
{
plugins: ['sentence-case'],
}
);
const mockPlugin = sandbox.createStubInstance(SentenceCase);
mockPlugin.run.returnsArg(0);
mockPlugin.preconfigure.returnsArg(0);
mockPlugin.processCommits.returnsArg(0);
sandbox
.stub(pluginFactory, 'buildPlugin')
.withArgs(sinon.match.has('type', 'sentence-case'))
.returns(mockPlugin);
const pullRequests = await manifest.buildPullRequests();
expect(pullRequests).not.empty;
sinon.assert.calledOnce(mockPlugin.processCommits);
});
});

it('should fallback to tagged version', async () => {
Expand Down
91 changes: 91 additions & 0 deletions test/plugins/sentence-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {describe, it} from 'mocha';
import {SentenceCase} from '../../src/plugins/sentence-case';
import {expect} from 'chai';

import {GitHub} from '../../src/github';

describe('SentenceCase Plugin', () => {
let github: GitHub;
beforeEach(async () => {
github = await GitHub.create({
owner: 'googleapis',
repo: 'node-test-repo',
defaultBranch: 'main',
});
});
describe('processCommits', () => {
it('converts description to sentence case', async () => {
const plugin = new SentenceCase(github, 'main', {});
const commits = await plugin.processCommits([
{
sha: 'abc123',
message: 'fix: hello world',
},
{
sha: 'abc123',
message: 'fix: Goodnight moon',
},
]);
expect(commits[0].message).to.equal('fix: Hello world');
expect(commits[1].message).to.equal('fix: Goodnight moon');
});
it('leaves reserved words lowercase', async () => {
const plugin = new SentenceCase(github, 'main', {});
const commits = await plugin.processCommits([
{
sha: 'abc123',
message: 'feat: gRPC can now handle proxies',
},
{
sha: 'abc123',
message: 'fix: npm now rocks',
},
]);
expect(commits[0].message).to.equal('feat: gRPC can now handle proxies');
expect(commits[1].message).to.equal('fix: npm now rocks');
});
it('handles sentences with now breaks', async () => {
const plugin = new SentenceCase(github, 'main', {});
const commits = await plugin.processCommits([
{
sha: 'abc123',
message: 'feat: beep-boop-hello',
},
{
sha: 'abc123',
message: 'fix:log4j.foo.bar',
},
]);
expect(commits[0].message).to.equal('feat: Beep-boop-hello');
expect(commits[1].message).to.equal('fix: Log4j.foo.bar');
});
});
it('allows a custom list of specialWords to be provided', async () => {
const plugin = new SentenceCase(github, 'main', {}, ['hello']);
const commits = await plugin.processCommits([
{
sha: 'abc123',
message: 'fix: hello world',
},
{
sha: 'abc123',
message: 'fix: Goodnight moon',
},
]);
expect(commits[0].message).to.equal('fix: hello world');
expect(commits[1].message).to.equal('fix: Goodnight moon');
});
});

0 comments on commit 414eb5f

Please sign in to comment.