Skip to content

Commit 46b2cb9

Browse files
committed
feat: add linting of existing commit messages
1 parent 3cd2fbc commit 46b2cb9

File tree

5 files changed

+94
-12
lines changed

5 files changed

+94
-12
lines changed

src/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const Config = t.type({
3333

3434
export type Config = t.TypeOf<typeof Config>;
3535

36+
export function getScopes(type: string, config: Config): string[] {
37+
return config.scopeOverrides[type] ?? config.scopes;
38+
}
39+
3640
export function parseConfig(path: string | undefined): Config {
3741
const json = path
3842
? JSON.parse(readFileSync(path, { encoding: 'utf-8' }))

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { resolve } from 'path';
44

55
import { parseConfig } from './config';
66
import { createCommitMessage } from './prompt';
7+
import { checkMessage } from './lint';
78
import { renderMessage, parseMessage } from './message';
89

910
const program = new Command();
@@ -49,7 +50,8 @@ function lintMessage(): void {
4950
encoding: 'utf-8',
5051
});
5152
const parsed = parseMessage(msg, config);
52-
console.log(parsed);
53+
checkMessage(parsed, config);
54+
process.exitCode = 0;
5355
}
5456

5557
function runInteractive(): void {

src/lint.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Config, getScopes, ticketSeperatorRegex } from './config';
2+
import { Message } from './message';
3+
4+
export function checkMessage(msg: Message, config: Config): void {
5+
if (config.types.indexOf(msg.type) === -1) {
6+
throw new Error(
7+
`Expected 'type' to be one of '${config.types.join(', ')}', got '${
8+
msg.type
9+
}'`
10+
);
11+
}
12+
13+
const allowedScopes = getScopes(msg.type, config);
14+
if (allowedScopes.length > 0 && !msg.scope) {
15+
throw new Error('Expected a scope, but got none');
16+
}
17+
18+
if (
19+
msg.scope &&
20+
!config.allowCustomScopes &&
21+
allowedScopes.indexOf(msg.scope) === -1
22+
) {
23+
throw new Error(
24+
`Expected 'scope' to be one of '${allowedScopes.join(
25+
', '
26+
)}', got '${msg.scope}'`
27+
);
28+
}
29+
30+
if (msg.body) {
31+
checkWrap(msg.body, config.bodyWrap);
32+
}
33+
if (msg.breaking) {
34+
if (
35+
config.allowBreakingChanges.length > 0 &&
36+
config.allowBreakingChanges.indexOf(msg.type) === -1
37+
) {
38+
throw new Error(
39+
`A BREAKING CHANGE is only allowed for the types '${config.allowBreakingChanges.join(
40+
', '
41+
)}', not for '${msg.type}'`
42+
);
43+
}
44+
checkWrap(msg.breaking, config.bodyWrap);
45+
}
46+
47+
if (msg.issuesClosed) {
48+
for (const issue of msg.issuesClosed
49+
.split(ticketSeperatorRegex(config))
50+
.map(s => s.trim())) {
51+
if (!issue.startsWith(config.ticketNumberPrefix)) {
52+
throw new Error(
53+
`Expected ticket number to start with '${config.ticketNumberPrefix}', got '${issue}'`
54+
);
55+
}
56+
const ticketNumber = issue.slice(
57+
config.ticketNumberPrefix.length,
58+
issue.length
59+
);
60+
if (isNaN(parseInt(ticketNumber))) {
61+
throw new Error(
62+
`Expected ticket number to be a number, got '${ticketNumber}'`
63+
);
64+
}
65+
}
66+
}
67+
}
68+
69+
function checkWrap(block: string, wrap: number) {
70+
for (const line of block.split('\n')) {
71+
if (line.length > wrap) {
72+
throw new Error(
73+
`Expected body to wrap after ${wrap} characters, but '${line}' has ${line.length}`
74+
);
75+
}
76+
}
77+
}

src/message.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function parseMessage(msg: string, config: Config): Message {
2222
if (!matches) {
2323
throw new Error(
2424
`expected subject to either have form '${config.typePrefix}<type>${config.typeSuffix}: <subject>' ` +
25-
`or form '${config.typeSuffix}<type>${config.typeSuffix}(<scope>): <subject>'`
25+
`or form '${config.typeSuffix}<type>${config.typeSuffix}(<scope>): <subject>', got '${lines[0]}'`
2626
);
2727
}
2828

@@ -42,9 +42,9 @@ export function parseMessage(msg: string, config: Config): Message {
4242
} else if (lines[i].startsWith(config.breakingPrefix)) {
4343
isBody = false;
4444
} else if (isBody) {
45-
body += lines[i] + ' ';
45+
body += lines[i] + '\n';
4646
} else {
47-
breaking += lines[i] + ' ';
47+
breaking += lines[i] + '\n';
4848
}
4949
}
5050

src/prompt.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as inquirer from 'inquirer';
22
import { QuestionCollection, Separator, Answers } from 'inquirer';
33
import { green, red } from 'chalk';
44

5-
import { Config, ticketSeperatorRegex } from './config';
5+
import { Config, getScopes, ticketSeperatorRegex } from './config';
66
import { Message } from './message';
77

88
export function createCommitMessage(config: Config): Promise<Message> {
@@ -79,6 +79,7 @@ export function createCommitMessage(config: Config): Promise<Message> {
7979
choices: config.allowBreakingChanges,
8080
when: answers =>
8181
answers.breaking !== '' &&
82+
config.allowBreakingChanges.length > 0 &&
8283
config.allowBreakingChanges.indexOf(answers.type) === -1,
8384
},
8485
{
@@ -148,11 +149,9 @@ function filterSubject(subject: string, upperCase: boolean): string {
148149

149150
function getChoices(config: Config): (answers: Answers) => string[] {
150151
return ({ type }) =>
151-
config.scopes
152-
.concat(config.scopeOverrides[type] ?? [])
153-
.concat(
154-
config.allowCustomScopes
155-
? [(new Separator() as unknown) as string, 'custom']
156-
: []
157-
);
152+
getScopes(type, config).concat(
153+
config.allowCustomScopes
154+
? [(new Separator() as unknown) as string, 'custom']
155+
: []
156+
);
158157
}

0 commit comments

Comments
 (0)