Skip to content

Commit

Permalink
refactor: improve commit message functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
pret-a-porter committed Jan 2, 2022
1 parent f7869e1 commit 91bbca5
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 93 deletions.
51 changes: 51 additions & 0 deletions lib/workers/repository/model/commit-message-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { RenovateSharedConfig } from '../../../config/types';
import type { CommitMessage } from './commit-message';
import { CustomCommitMessage } from './custom-commit-message';
import { SemanticCommitMessage } from './semantic-commit-message';

type CommitMessageConfig = Pick<
RenovateSharedConfig,
| 'commitMessagePrefix'
| 'semanticCommits'
| 'semanticCommitScope'
| 'semanticCommitType'
>;

export class CommitMessageFactory {
private readonly config: CommitMessageConfig;

constructor(config: CommitMessageConfig) {
this.config = config;
}

create(): CommitMessage {
const message = this.areSemanticCommitsEnabled
? this.createSemanticCommitMessage()
: this.createCustomCommitMessage();

return message;
}

private createSemanticCommitMessage(): SemanticCommitMessage {
const message = new SemanticCommitMessage();

message.setType(this.config.semanticCommitType);
message.setScope(this.config.semanticCommitScope);

return message;
}

private createCustomCommitMessage(): CustomCommitMessage {
const message = new CustomCommitMessage();
message.setPrefix(this.config.commitMessagePrefix);

return message;
}

private get areSemanticCommitsEnabled(): boolean {
return (
!this.config.commitMessagePrefix &&
this.config.semanticCommits === 'enabled'
);
}
}
48 changes: 0 additions & 48 deletions lib/workers/repository/model/commit-message.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,8 @@ import { CommitMessage } from './commit-message';

describe('workers/repository/model/commit-message', () => {
describe('CommitMessage', () => {
const TEST_CASES: ReadonlyArray<
[message: string, prefix: string | undefined, result: string]
> = [
['test', undefined, 'Test'],
['test', '', 'Test'],
[' test ', ' ', 'Test'],
['test', 'fix', 'fix: test'],
['test', 'fix:', 'fix: test'],
];

it('has colon character separator', () => {
expect(CommitMessage.SEPARATOR).toBe(':');
});

it.each(TEST_CASES)(
'given %p and %p as arguments, returns %p',
(message, prefix, result) => {
const commitMessage = new CommitMessage(message);
commitMessage.setCustomPrefix(prefix);

expect(commitMessage.toString()).toEqual(result);
}
);

it('should handle not defined semantic prefix', () => {
const message = new CommitMessage('test');
message.setSemanticPrefix();

expect(message.toString()).toBe('Test');
});

it('should handle empty semantic prefix', () => {
const message = new CommitMessage('test');
message.setSemanticPrefix(' ', ' ');

expect(message.toString()).toBe('Test');
});

it('should format sematic prefix', () => {
const message = new CommitMessage('test');
message.setSemanticPrefix(' fix ');

expect(message.toString()).toBe('fix: test');
});

it('should format sematic prefix with scope', () => {
const message = new CommitMessage('test');
message.setSemanticPrefix(' fix ', ' scope ');

expect(message.toString()).toBe('fix(scope): test');
});
});
});
78 changes: 54 additions & 24 deletions lib/workers/repository/model/commit-message.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
export class CommitMessage {
public static readonly SEPARATOR: string = ':';

private message = '';
export interface CommitMessageJSON {
body?: string;
footer?: string;
subject?: string;
}

private prefix = '';
/**
* @see https://git-scm.com/docs/git-commit#_discussion
*
* [optional prefix]: <suject>
* [optional body]
* [optional footer]
*/
export abstract class CommitMessage {
static readonly SEPARATOR: string = ':';
private static readonly EXTRA_WHITESPACES = /\s+/g;

constructor(message = '') {
this.setMessage(message);
}
private body?: string;
private footer?: string;
private subject?: string;

public static formatPrefix(prefix: string): string {
if (!prefix) {
Expand All @@ -21,34 +31,54 @@ export class CommitMessage {
return `${prefix}${CommitMessage.SEPARATOR}`;
}

public setMessage(message: string): void {
this.message = (message || '').trim();
toString(): string {
const parts: ReadonlyArray<string | undefined> = [
this.title,
this.body,
this.footer,
];

return parts.filter(Boolean).join('\n\n');
}

public setCustomPrefix(prefix?: string): void {
this.prefix = (prefix ?? '').trim();
get title(): string {
return [CommitMessage.formatPrefix(this.prefix), this.formatSubject()]
.join(' ')
.trim();
}

public setSemanticPrefix(type?: string, scope?: string): void {
this.prefix = (type ?? '').trim();
toJSON(): CommitMessageJSON {
return {
body: this.body,
footer: this.footer,
subject: this.subject,
};
}

if (scope?.trim()) {
this.prefix += `(${scope.trim()})`;
}
setBody(body?: string): void {
this.body = body?.trim();
}

public toString(): string {
const prefix = CommitMessage.formatPrefix(this.prefix);
const message = this.formatMessage();
setFooter(footer?: string): void {
this.footer = footer?.trim();
}

return [prefix, message].join(' ').trim();
setSubject(subject?: string): void {
this.subject = subject?.trim();
this.subject = this.subject?.replace(CommitMessage.EXTRA_WHITESPACES, ' ');
}

private formatMessage(): string {
formatSubject(): string {
if (!this.subject) {
return '';
}

if (this.prefix) {
return this.message;
return this.subject.charAt(0).toLowerCase() + this.subject.slice(1);
}

return this.message.charAt(0).toUpperCase() + this.message.slice(1);
return this.subject.charAt(0).toUpperCase() + this.subject.slice(1);
}

protected abstract get prefix(): string;
}
40 changes: 40 additions & 0 deletions lib/workers/repository/model/custom-commit-message.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { CustomCommitMessage } from './custom-commit-message';

describe('workers/repository/model/custom-commit-message', () => {
describe('CustomCommitMessage', () => {
const TEST_CASES: ReadonlyArray<
[message: string, prefix: string | undefined, result: string]
> = [
['test', undefined, 'Test'],
['test', '', 'Test'],
[' test ', ' ', 'Test'],
['test', 'fix', 'fix: test'],
['test', 'fix:', 'fix: test'],
[
'Message With Extra Whitespaces ',
' refactor ',
'refactor: message With Extra Whitespaces',
],
];

it.each(TEST_CASES)(
'given %p and %p as arguments, returns %p',
(subject, prefix, result) => {
const commitMessage = new CustomCommitMessage();
commitMessage.setSubject(subject);
commitMessage.setPrefix(prefix);

expect(commitMessage.toString()).toEqual(result);
}
);

it('should provide ability to set body and footer', () => {
const commitMessage = new CustomCommitMessage();
commitMessage.setSubject('subject');
commitMessage.setBody('body');
commitMessage.setFooter('footer');

expect(commitMessage.toString()).toBe('Subject\n\nbody\n\nfooter');
});
});
});
26 changes: 26 additions & 0 deletions lib/workers/repository/model/custom-commit-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CommitMessage, CommitMessageJSON } from './commit-message';

export interface CustomCommitMessageJSON extends CommitMessageJSON {
prefix?: string;
}

export class CustomCommitMessage extends CommitMessage {
private _prefix?: string;

setPrefix(prefix?: string): void {
this._prefix = prefix?.trim();
}

override toJSON(): CustomCommitMessageJSON {
const json = super.toJSON();

return {
...json,
prefix: this._prefix,
};
}

protected get prefix(): string {
return this._prefix;
}
}
59 changes: 59 additions & 0 deletions lib/workers/repository/model/semantic-commit-message.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { SemanticCommitMessage } from './semantic-commit-message';

describe('workers/repository/model/semantic-commit-message', () => {
it('should format message without prefix', () => {
const message = new SemanticCommitMessage();
message.setSubject('test');

expect(message.toString()).toBe('Test');
});

it('should format sematic type', () => {
const message = new SemanticCommitMessage();
message.setSubject('test');
message.setType(' fix ');

expect(message.toString()).toBe('fix: test');
});

it('should format sematic prefix with scope', () => {
const message = new SemanticCommitMessage();
message.setSubject('test');
message.setType(' fix ');
message.setScope(' scope ');

expect(message.toString()).toBe('fix(scope): test');
});

it('should create instance from string without scope', () => {
const instance = SemanticCommitMessage.fromString('feat: ticket 123');
const json = instance.toJSON();

expect(SemanticCommitMessage.is(instance)).toBeTrue();
expect(json.type).toBe('feat');
expect(json.scope).toBeUndefined();
expect(json.subject).toBe('ticket 123');
});

it('should create instance from string with scope', () => {
const instance = SemanticCommitMessage.fromString(
'fix(dashboard): ticket 123'
);
const json = instance.toJSON();

expect(SemanticCommitMessage.is(instance)).toBeTrue();
expect(json.type).toBe('fix');
expect(json.scope).toBe('dashboard');
expect(json.subject).toBe('ticket 123');
});

it('should create instance from string with empty description', () => {
const instance = SemanticCommitMessage.fromString('fix(deps): ');
const json = instance.toJSON();

expect(SemanticCommitMessage.is(instance)).toBeTrue();
expect(json.type).toBe('fix');
expect(json.scope).toBe('deps');
expect(json.subject).toBe('');
});
});
Loading

0 comments on commit 91bbca5

Please sign in to comment.