Skip to content

Commit

Permalink
refactor: improve commit message functionality (#13328)
Browse files Browse the repository at this point in the history
* refactor: improve commit message functionality

* refactor: fix test coverage

* refactor: fix by comments

* refactor: fix build

* refactor: fix linting

* refactor: fix export type

* refactor: js private fields

* refactor: static private fields

* fix: lint

* refactor: fix tsconfig

* refactor: implement method normalizeInput

* refactor: fix by comments

* Update lib/workers/repository/model/commit-message.ts

* refactor: fix by comments

* refactor: use private typescript fields again

* refactor: fix by comments

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
  • Loading branch information
pret-a-porter and viceice authored May 2, 2022
1 parent be9902e commit ed73d38
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 112 deletions.
5 changes: 5 additions & 0 deletions lib/types/commit-message-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface CommitMessageJSON {
body?: string;
footer?: string;
subject?: string;
}
1 change: 1 addition & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type { CommitMessageJSON } from './commit-message-json';
export * from './host-rules';
export * from './skip-reason';
export * from './versioning';
Expand Down
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.type = this._config.semanticCommitType ?? '';
message.scope = this._config.semanticCommitScope ?? '';

return message;
}

private createCustomCommitMessage(): CustomCommitMessage {
const message = new CustomCommitMessage();
message.prefix = this._config.commitMessagePrefix ?? '';

return message;
}

private get areSemanticCommitsEnabled(): boolean {
return (
!this._config.commitMessagePrefix &&
this._config.semanticCommits === 'enabled'
);
}
}
57 changes: 0 additions & 57 deletions lib/workers/repository/model/commit-message.spec.ts

This file was deleted.

84 changes: 59 additions & 25 deletions lib/workers/repository/model/commit-message.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
export class CommitMessage {
public static readonly SEPARATOR: string = ':';
import is from '@sindresorhus/is';
import type { CommitMessageJSON } from '../../../types';

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

private prefix = '';
private _body = '';
private _footer = '';
private _subject = '';

constructor(message = '') {
this.setMessage(message);
}

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

public setMessage(message: string): void {
this.message = (message || '').trim();
toJSON(): CommitMessageJSON {
return {
body: this._body,
footer: this._footer,
subject: this._subject,
};
}

public setCustomPrefix(prefix?: string): void {
this.prefix = (prefix ?? '').trim();
toString(): string {
const parts: ReadonlyArray<string | undefined> = [
this.title,
this._body,
this._footer,
];

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

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

if (scope?.trim()) {
this.prefix += `(${scope.trim()})`;
}
set body(value: string) {
this._body = this.normalizeInput(value);
}

public toString(): string {
const prefix = CommitMessage.formatPrefix(this.prefix);
const message = this.formatMessage();
set footer(value: string) {
this._footer = this.normalizeInput(value);
}

return [prefix, message].join(' ').trim();
set subject(value: string) {
this._subject = this.normalizeInput(value);
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;

protected normalizeInput(value: string | null | undefined): string {
return value?.trim() ?? '';
}
}
52 changes: 52 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,52 @@
import { CustomCommitMessage } from './custom-commit-message';

describe('workers/repository/model/custom-commit-message', () => {
describe('CustomCommitMessage', () => {
it.each`
subject | prefix | result
${'test'} | ${''} | ${'Test'}
${' test '} | ${' '} | ${'Test'}
${'test'} | ${'fix'} | ${'fix: test'}
${'test'} | ${'fix:'} | ${'fix: test'}
${'Message With Extra Whitespaces '} | ${' refactor '} | ${'refactor: message With Extra Whitespaces'}
`(
'given subject $subject and prefix $prefix as arguments, returns $result',
({
subject,
prefix,
result,
}: {
subject: string;
prefix: string;
result: string;
}) => {
const commitMessage = new CustomCommitMessage();
commitMessage.subject = subject;
commitMessage.prefix = prefix;

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

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

expect(commitMessage.toJSON()).toEqual({
body: 'body',
footer: 'footer',
prefix: '',
subject: 'subject',
});
expect(commitMessage.toString()).toBe('Subject\n\nbody\n\nfooter');
});

it('should remove empty subject by default', () => {
const commitMessage = new CustomCommitMessage();

expect(commitMessage.formatSubject()).toBe('');
});
});
});
27 changes: 27 additions & 0 deletions lib/workers/repository/model/custom-commit-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { CommitMessageJSON } from '../../../types';
import { CommitMessage } from './commit-message';

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

export class CustomCommitMessage extends CommitMessage {
private _prefix = '';

get prefix(): string {
return this._prefix;
}

set prefix(value: string) {
this._prefix = this.normalizeInput(value);
}

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

return {
...json,
prefix: this._prefix,
};
}
}
73 changes: 73 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,73 @@
import { SemanticCommitMessage } from './semantic-commit-message';

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

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

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

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

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

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

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

expect(SemanticCommitMessage.is(instance)).toBeTrue();
expect(instance.toJSON()).toEqual({
body: '',
footer: '',
scope: '',
subject: 'ticket 123',
type: 'feat',
});
});

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

expect(SemanticCommitMessage.is(instance)).toBeTrue();
expect(instance.toJSON()).toEqual({
body: '',
footer: '',
scope: 'dashboard',
subject: 'ticket 123',
type: 'fix',
});
});

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

expect(SemanticCommitMessage.is(instance)).toBeTrue();
expect(instance.toJSON()).toEqual({
body: '',
footer: '',
scope: 'deps',
subject: '',
type: 'fix',
});
});

it('should return undefined for invalid string', () => {
const instance = SemanticCommitMessage.fromString('test');
expect(instance).toBeUndefined();
});
});
Loading

0 comments on commit ed73d38

Please sign in to comment.