Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: improve commit message functionality #13328

Merged
merged 34 commits into from
May 2, 2022
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
91bbca5
refactor: improve commit message functionality
pret-a-porter Jan 2, 2022
2d75ab1
refactor: fix test coverage
pret-a-porter Jan 2, 2022
840f3ae
Merge branch 'main' into refactor/commit-message
pret-a-porter Jan 9, 2022
53e0368
refactor: fix by comments
pret-a-porter Jan 9, 2022
79ac242
Merge branch 'main' into refactor/commit-message
pret-a-porter Jan 11, 2022
156b2ab
Merge branch 'main' into refactor/commit-message
pret-a-porter Jan 13, 2022
646736d
Merge branch 'main' into refactor/commit-message
pret-a-porter Jan 19, 2022
6794b09
Merge branch 'main' into refactor/commit-message
pret-a-porter Jan 23, 2022
67d5983
Merge branch 'main' into refactor/commit-message
pret-a-porter Jan 23, 2022
7fbad8d
Merge branch 'main' into refactor/commit-message
pret-a-porter Jan 23, 2022
1bbfbd1
Merge branch 'main' into refactor/commit-message
pret-a-porter Mar 10, 2022
711674a
refactor: fix build
pret-a-porter Mar 10, 2022
b152d3d
Merge branch 'main' into refactor/commit-message
pret-a-porter Mar 25, 2022
e6f23be
refactor: fix linting
pret-a-porter Mar 25, 2022
19ebf3d
Merge branch 'main' into refactor/commit-message
pret-a-porter Mar 25, 2022
0c9743e
Merge branch 'main' into refactor/commit-message
pret-a-porter Mar 27, 2022
336358b
refactor: fix export type
pret-a-porter Mar 27, 2022
c3d4b76
Merge branch 'main' into refactor/commit-message
pret-a-porter Mar 31, 2022
b023147
refactor: js private fields
pret-a-porter Mar 31, 2022
851ff3d
refactor: static private fields
pret-a-porter Mar 31, 2022
a515966
fix: lint
pret-a-porter Apr 2, 2022
5500bae
refactor: fix tsconfig
pret-a-porter Apr 5, 2022
fa4e02a
Merge branch 'main' into refactor/commit-message
pret-a-porter Apr 8, 2022
8caf960
Merge branch 'main' into refactor/commit-message
pret-a-porter Apr 9, 2022
52e740a
refactor: implement method normalizeInput
pret-a-porter Apr 9, 2022
e221a95
refactor: fix by comments
pret-a-porter Apr 11, 2022
065cf37
Update lib/workers/repository/model/commit-message.ts
viceice Apr 19, 2022
f0ad3d7
Merge branch 'main' into refactor/commit-message
viceice Apr 25, 2022
bcd1a39
Merge branch 'main' into refactor/commit-message
pret-a-porter Apr 30, 2022
c61ac98
refactor: fix by comments
pret-a-porter Apr 30, 2022
1c9188d
Merge branch 'main' into refactor/commit-message
pret-a-porter May 1, 2022
5823bbf
refactor: use private typescript fields again
pret-a-porter May 1, 2022
3da60d8
refactor: fix by comments
pret-a-porter May 1, 2022
59c50b6
Merge branch 'main' into refactor/commit-message
viceice May 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
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.

87 changes: 60 additions & 27 deletions lib/workers/repository/model/commit-message.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,87 @@
export class CommitMessage {
public static readonly SEPARATOR: string = ':';
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 {
static readonly #SEPARATOR: string = ':';
pret-a-porter marked this conversation as resolved.
Show resolved Hide resolved
static readonly #EXTRA_WHITESPACES = /\s+/g;

private prefix = '';
#body = '';
#footer = '';
#subject = '';

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

public static formatPrefix(prefix: string): string {
static formatPrefix(prefix: string): string {
if (!prefix) {
return '';
}

if (prefix.endsWith(CommitMessage.SEPARATOR)) {
if (prefix.endsWith(CommitMessage.#SEPARATOR)) {
return prefix;
}

return `${prefix}${CommitMessage.SEPARATOR}`;
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(Boolean).join('\n\n');
pret-a-porter marked this conversation as resolved.
Show resolved Hide resolved
}

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): string {
viceice marked this conversation as resolved.
Show resolved Hide resolved
return value?.trim() ?? '';
}
}
51 changes: 51 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,51 @@
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', '', '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)(
pret-a-porter marked this conversation as resolved.
Show resolved Hide resolved
'given %p and %p as arguments, returns %p',
(subject, prefix, result) => {
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 {
#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