Skip to content

Commit 4592269

Browse files
committed
Updates to commitlint-github-utils largely to enhance the parser to better support different WIP formats
1 parent 6c6701f commit 4592269

File tree

8 files changed

+471
-49
lines changed

8 files changed

+471
-49
lines changed

packages/commitlint-github-utils/@types/index.d.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
export interface Rules {
22
issueNumberMissing: string;
33
issueNumberFormat: string;
4+
issueNumberDuplicate: string;
5+
6+
wipAllowed: string;
7+
8+
subjectEmpty: string;
9+
subjectCase: string;
10+
subjectFullStop: string;
11+
subjectMinLength: string;
12+
subjectMaxLength: string;
13+
subjectSeparator: string;
14+
15+
typeCase: string;
16+
typeEmpty: string;
17+
typeEnum: string;
18+
typeMaxLength: string;
19+
typeMinLength: string;
420
}
521

622
export interface CommitlintGitHubConstants {
@@ -27,19 +43,44 @@ export type ParsedCommitMessage = {
2743
rawIssueNumbers?: string;
2844
isWip: boolean;
2945
type?: string;
46+
subjectSeparator?: string;
3047
subject?: string;
3148
body: string[];
3249
};
3350

34-
export type CommitParser = (unparsedCommitMessage: string, parserOptions?: ParserOptions) => ParsedCommitMessage;
51+
export const enum When {
52+
ALWAYS = 'always',
53+
NEVER = 'never',
54+
IGNORED = 'ignored', // For when the rule doesn't vary based on the 'when' value passed in
55+
}
56+
57+
// TODO: Remove once @commitlint/types is published
58+
export type TargetCaseType =
59+
| 'camel-case'
60+
| 'kebab-case'
61+
| 'snake-case'
62+
| 'pascal-case'
63+
| 'start-case'
64+
| 'upper-case'
65+
| 'uppercase'
66+
| 'sentence-case'
67+
| 'sentencecase'
68+
| 'lower-case'
69+
| 'lowercase'
70+
| 'lowerCase';
71+
72+
export type WipHandledResult = {
73+
isWipValidated: boolean;
74+
when?: When;
75+
};
3576

3677
export interface CommitlintGitHubUtils {
37-
parseCommitMessage: CommitParser;
3878
commitlintGitHubConstants: CommitlintGitHubConstants;
39-
}
4079

41-
export const commitlintGitHubConstants: CommitlintGitHubConstants;
42-
export const parseCommitMessage: CommitParser;
80+
parseCommitMessage(unparsedCommitMessage: string, parserOptions?: ParserOptions): ParsedCommitMessage;
81+
isNegated(when?: string): boolean;
82+
handleWipCommits(commitMessage: ParsedCommitMessage, whenPassedIn?: When): WipHandledResult;
83+
}
4384

4485
declare const commitlintGitHubUtils: CommitlintGitHubUtils;
4586
export default commitlintGitHubUtils;
Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
11
export const GITHUB_RULES = {
22
issueNumberMissing: 'github-issue-number-missing',
33
issueNumberFormat: 'github-issue-number-format',
4-
typeOrWip: 'github-type-or-wip',
5-
subjectCase: 'github-subject-case',
4+
issueNumberDuplicate: 'github-issue-number-duplicate',
5+
6+
wipAllowed: 'wip-allowed',
7+
8+
// The following should match the base rule strings in @commitlint/rules:
9+
10+
subjectEmpty: 'subject-empty',
11+
subjectCase: 'subject-case',
12+
subjectFullStop: 'subject-full-stop',
13+
subjectMinLength: 'subject-min-length',
14+
subjectMaxLength: 'subject-max-length',
15+
subjectSeparator: 'subject-separator',
16+
17+
typeEmpty: 'type-empty',
18+
typeCase: 'type-case',
19+
typeEnum: 'type-enum',
20+
typeMaxLength: 'type-max-length',
21+
typeMinLength: 'type-min-length',
622
};
723

824
export const ISSUE_NUMBER_PREFIX = '#';
925
export const ISSUE_NUMBERS_SEPARATOR = ',';
1026
export const ISSUE_NUMBER_PATTERN = /^#(?<issueNumber>\d+)$/;
11-
export const ISSUE_NUMBERS_PATTERN = /^\((?<issues>.*?)\) (?:(?<type>.+?):)?(?<description>.*)/;
12-
export const WIP_WITHOUT_ISSUE_NUMBER_PATTERN = /^WIP:(?<description>.*)/;
27+
28+
// Exclude matching 'WIP' as the start of a description using a negative look-ahead
29+
// Instead should match WIP_WITH_JUST_ISSUE_NUMBERS_PATTERN below
30+
export const ISSUE_NUMBERS_PATTERN = /^\((?<issues>.*?)\)(?: (?<type>.+?):)?(?!\s*WIP)(?<description>.*)/;
31+
32+
// Allow WIPs to avoid specifying issue number as should only exist on feature branches which are per issue
33+
// Allow either 'WIP: ...', 'WIP - ...', 'WIP2', 'WIP 2', 'WIP 2: ...', 'WIP 2 - ...' etc.
34+
export const WIP_WITHOUT_ISSUE_NUMBER_PATTERN = /^WIP(?:\s*\d+)?(?:(?:\.|:|(?:\s*-))(?<description>.*))?$/;
35+
export const WIP_WITH_JUST_ISSUE_NUMBERS_PATTERN = /^\((?<issues>.*?)\) WIP\b/;
36+
export const SUBJECT_PATTERN = /^(?<subjectSeparator>\s*)?(?<subject>.*)/;
1337
export const WIP_TYPE = 'WIP';
1438
export const TYPE_SEPARATOR = ':';
1539
export const COMMIT_DESCRIPTION_SEPARATOR = '\n';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ParsedCommitMessage, When, WipHandledResult } from '../@types';
2+
3+
/**
4+
* Handles short-circuiting WIP commits to immediately return as valid. Previsouly it also converted the if the when passed in
5+
* when we had custom When values to allow rules to be configured to only be applicable to non-WIPs (e.g. NON_WIPS_ALWAYS, NON_WIPS_NEVER).
6+
* However commitlint currently restricts When values in the top-level configuration to just 'always' and 'never' and rejects configuration
7+
* with any other value.
8+
*
9+
* Therefore we have stripped out support for custom When values until a time when they may be supported (or not):
10+
* See: https://github.com/conventional-changelog/commitlint/issues/1003
11+
*
12+
* @param commitMessage the parsed commit message
13+
* @param whenPassedIn the When passed in
14+
* @returns { isWipValidated: true } if the When given only applies to non-WIP commits and the commit is a WIP;
15+
* or { isWipValidated: false, when } with the When to apply to the non-WIP, see description above.
16+
*/
17+
function handleWipCommits(commitMessage: ParsedCommitMessage, when?: When): WipHandledResult {
18+
if (commitMessage.isWip) {
19+
return { isWipValidated: true };
20+
}
21+
22+
return { isWipValidated: false, when };
23+
}
24+
25+
export default handleWipCommits;
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import { CommitlintGitHubUtils, When } from '../@types';
12
import * as commitlintGitHubConstants from './commitlintGitHubConstants';
23
import parseCommitMessage from './parseCommitMessage';
3-
import { CommitlintGitHubUtils } from '../@types';
4+
import handleWipCommits from './handleWipCommits';
5+
6+
const isNegated = (when?: When): boolean => when === When.NEVER;
47

58
const commitlintGitHubUtils: CommitlintGitHubUtils = {
69
commitlintGitHubConstants,
710
parseCommitMessage,
11+
isNegated,
12+
handleWipCommits,
813
};
914

10-
export { commitlintGitHubConstants, parseCommitMessage };
1115
export default commitlintGitHubUtils;
Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
1-
import { CommitParser, ParsedCommitMessage } from '../@types';
1+
import { ParsedCommitMessage } from '../@types';
22
import {
33
ISSUE_NUMBER_PATTERN,
44
ISSUE_NUMBERS_PATTERN,
55
WIP_WITHOUT_ISSUE_NUMBER_PATTERN,
6+
WIP_WITH_JUST_ISSUE_NUMBERS_PATTERN,
7+
SUBJECT_PATTERN,
68
ISSUE_NUMBERS_SEPARATOR,
79
WIP_TYPE,
810
COMMIT_DESCRIPTION_SEPARATOR,
911
} from './commitlintGitHubConstants';
1012

11-
const parseRegex = (stringToParse: string, regex: RegExp): { [key: string]: string } | undefined => {
13+
function parseRegex(stringToParse: string, regex: RegExp): { [key: string]: string } | undefined {
1214
return regex.exec(stringToParse)?.groups;
13-
};
15+
}
1416

15-
const parseIssue = (issueString: string): number => {
17+
function parseIssue(issueString: string): number {
1618
const groupsMatched = ISSUE_NUMBER_PATTERN.exec(issueString.trim())?.groups;
1719

1820
if (groupsMatched) {
1921
return parseInt(groupsMatched.issueNumber, 10);
2022
}
2123

2224
return -1;
23-
};
25+
}
2426

25-
const parseIssues = (issuesString: string): number[] => {
27+
function parseIssues(issuesString: string): number[] {
2628
if (!issuesString) {
2729
return [];
2830
}
@@ -37,44 +39,66 @@ const parseIssues = (issuesString: string): number[] => {
3739

3840
// Otherwise return an empty array
3941
return [];
40-
};
42+
}
4143

42-
const parseDescription = (groups: { [key: string]: string }): [string, string[]] => {
44+
function parseDescription(groups: { [key: string]: string }): [string, string, string[]] {
4345
const descriptionLines = groups.description.split(COMMIT_DESCRIPTION_SEPARATOR);
4446

45-
const subject = descriptionLines[0].trim();
47+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
48+
const subjectGroups = parseRegex(descriptionLines[0], SUBJECT_PATTERN)!;
49+
50+
const { subjectSeparator } = subjectGroups;
51+
const subject = subjectGroups.subject.trim(); // trims any trailing whitespace
4652
const body = descriptionLines.slice(1);
4753

48-
return [subject, body];
49-
};
54+
return [subjectSeparator, subject, body];
55+
}
5056

51-
const parseCommitMessage: CommitParser = (rawCommitMessage: string): ParsedCommitMessage => {
57+
function parseCommitMessage(rawCommitMessage: string): ParsedCommitMessage {
5258
let issueNumbers: number[] = [];
5359
let rawIssueNumbers: string | undefined;
5460
let type: string | undefined;
5561
let isWip = false;
62+
let subjectSeparator: string | undefined;
5663
let subject: string | undefined;
5764
let body: string[] = [];
5865

5966
const issueNumbersWithPossibleTypeGroups = parseRegex(rawCommitMessage, ISSUE_NUMBERS_PATTERN);
6067

6168
if (issueNumbersWithPossibleTypeGroups) {
62-
// console.log(`description: '${issueNumbersWithPossibleTypeGroups.description}'; raw: ${rawCommitMessage}`);
6369
rawIssueNumbers = issueNumbersWithPossibleTypeGroups.issues.trim();
6470
issueNumbers = parseIssues(rawIssueNumbers);
6571

6672
// eslint-disable-next-line prefer-destructuring
6773
type = issueNumbersWithPossibleTypeGroups.type;
6874
isWip = type === WIP_TYPE;
6975

70-
[subject, body] = parseDescription(issueNumbersWithPossibleTypeGroups);
76+
// Clear the type as WIP is not a valid type and can't easily be added since types are validated to be lowercase (WIP is the exception)
77+
if (isWip) {
78+
type = undefined;
79+
}
80+
81+
[subjectSeparator, subject, body] = parseDescription(issueNumbersWithPossibleTypeGroups);
7182
} else {
83+
// As well as WIP commits with a description
7284
const wipCommitGroups = parseRegex(rawCommitMessage, WIP_WITHOUT_ISSUE_NUMBER_PATTERN);
85+
7386
if (wipCommitGroups) {
7487
isWip = true;
75-
type = WIP_TYPE;
7688

77-
[subject, body] = parseDescription(wipCommitGroups);
89+
// WIP Commits may not have a description but may just be WIP placeholder commits such as 'WIP', 'WIP 2' etc., so guard the parsing
90+
if (wipCommitGroups.description) {
91+
[subjectSeparator, subject, body] = parseDescription(wipCommitGroups);
92+
}
93+
} else {
94+
const wipWithIssueNumbersGroups = parseRegex(rawCommitMessage, WIP_WITH_JUST_ISSUE_NUMBERS_PATTERN);
95+
96+
if (wipWithIssueNumbersGroups) {
97+
isWip = true;
98+
99+
rawIssueNumbers = wipWithIssueNumbersGroups.issues.trim();
100+
issueNumbers = parseIssues(rawIssueNumbers);
101+
}
78102
}
79103
}
80104

@@ -83,9 +107,10 @@ const parseCommitMessage: CommitParser = (rawCommitMessage: string): ParsedCommi
83107
rawIssueNumbers,
84108
isWip,
85109
type,
110+
subjectSeparator,
86111
subject,
87112
body,
88113
};
89-
};
114+
}
90115

91116
export default parseCommitMessage;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import handleWipCommits from '../handleWipCommits';
2+
import { ParsedCommitMessage, When } from '../../@types';
3+
4+
const WIP_COMMIT: ParsedCommitMessage = {
5+
issueNumbers: [],
6+
isWip: true,
7+
body: [],
8+
subject: 'My WIP Commit',
9+
};
10+
11+
const NON_WIP_COMMIT: ParsedCommitMessage = {
12+
issueNumbers: [],
13+
isWip: false,
14+
body: [],
15+
subject: 'My Non-WIP Commit',
16+
};
17+
18+
describe('handleWipCommits', () => {
19+
it('should return validated when passed a WIP Commit regardless of the "When" clause', () => {
20+
expect(handleWipCommits(WIP_COMMIT, When.ALWAYS).isWipValidated).toBe(true);
21+
expect(handleWipCommits(WIP_COMMIT, When.NEVER).isWipValidated).toBe(true);
22+
});
23+
24+
it('should return not validated when passed a non-WIP Commit regardless of the "When" clause', () => {
25+
expect(handleWipCommits(NON_WIP_COMMIT, When.ALWAYS).isWipValidated).toBe(false);
26+
expect(handleWipCommits(NON_WIP_COMMIT, When.NEVER).isWipValidated).toBe(false);
27+
});
28+
29+
it('should return the When passed in when passed a non-WIP Commit regardless of the "When" clause', () => {
30+
expect(handleWipCommits(NON_WIP_COMMIT, When.ALWAYS).when).toBe(When.ALWAYS);
31+
expect(handleWipCommits(NON_WIP_COMMIT, When.NEVER).when).toBe(When.NEVER);
32+
expect(handleWipCommits(NON_WIP_COMMIT, When.IGNORED).when).toBe(When.IGNORED);
33+
});
34+
});
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1-
import { commitlintGitHubConstants, parseCommitMessage } from '../index';
1+
import utils from '../index';
22

33
describe('commitlintPluginGitHub', () => {
44
it('should return a expected exports', () => {
5-
expect(commitlintGitHubConstants).not.toBeNull();
6-
expect(parseCommitMessage).not.toBeNull();
5+
expect(utils).not.toBeNull();
6+
expect(utils.commitlintGitHubConstants).not.toBeNull();
7+
expect(utils.parseCommitMessage).toBeInstanceOf(Function);
8+
expect(utils.handleWipCommits).toBeInstanceOf(Function);
9+
expect(utils.isNegated).toBeInstanceOf(Function);
10+
});
11+
});
12+
13+
describe('isNegated', () => {
14+
it('should return true for "never"', () => {
15+
expect(utils.isNegated('never')).toBe(true);
16+
});
17+
18+
it('should return false for "always"', () => {
19+
expect(utils.isNegated('always')).toBe(false);
720
});
821
});

0 commit comments

Comments
 (0)