Skip to content

Commit d1bcd84

Browse files
committed
feat!: refactor parse
1 parent 34bacad commit d1bcd84

File tree

5 files changed

+437
-197
lines changed

5 files changed

+437
-197
lines changed

.github/workflows/pr_title.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ jobs:
1414
run: dart pub get
1515

1616
- name: Validate Title of PR
17-
run: echo ${{ github.event.pull_request.title }} | dart bin/commitlint_cli.dart
17+
run: echo ${{ github.event.pull_request.title }} | dart bin/commitlint_cli.dart --config lib/commitlint.yaml

lib/src/parse.dart

Lines changed: 110 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import 'types/commit.dart';
44
/// Parse Commit Message String to Convensional Commit
55
///
66
7-
const _kDefaultHeaderPattern = r'^(\w*)(?:\((.*)\))?: (.*)$';
8-
const _kDefaultHeaderCorrespondence = ['type', 'scope', 'subject'];
7+
final _kHeaderPattern =
8+
RegExp(r'^(?<type>\w*?)(\((?<scope>.*)\))?!?: (?<subject>.+)$');
9+
const _kHeaderCorrespondence = ['type', 'scope', 'subject'];
910

10-
const _kDefaultReferenceActions = [
11+
const _kReferenceActions = [
1112
'close',
1213
'closes',
1314
'closed',
@@ -19,73 +20,78 @@ const _kDefaultReferenceActions = [
1920
'resolved'
2021
];
2122

22-
const _kDefaultIssuePrefixes = ['#'];
23-
const _kDefaultNoteKeywords = ['BREAKING CHANGE', 'BREAKING-CHANGE'];
24-
25-
const _kDefaultFieldPattern = r'^-(.*?)-$';
26-
27-
const _kDefaultRevertPattern =
28-
r'^(?:Revert|revert:)\s"?([\s\S]+?)"?\s*This reverts commit (\w*)\.';
29-
const _kDefaultRevertCorrespondence = ['header', 'hash'];
30-
31-
Commit parse(String raw,
32-
{String headerPattern = _kDefaultHeaderPattern,
33-
List<String> headerCorrespondence = _kDefaultHeaderCorrespondence,
34-
List<String> referenceActions = _kDefaultReferenceActions,
35-
List<String> issuePrefixes = _kDefaultIssuePrefixes,
36-
List<String> noteKeywords = _kDefaultNoteKeywords,
37-
String fieldPattern = _kDefaultFieldPattern,
38-
String revertPattern = _kDefaultRevertPattern,
39-
List<String> revertCorrespondence = _kDefaultRevertCorrespondence,
40-
String? commentChar}) {
41-
final message = raw.trim();
42-
if (message.isEmpty) {
43-
throw ArgumentError.value(raw, 'raw message', 'must have content.');
23+
const _kIssuePrefixes = ['#'];
24+
const _kNoteKeywords = ['BREAKING CHANGE', 'BREAKING-CHANGE'];
25+
final _kMergePattern = RegExp(r'^(Merge|merge)\s(.*)$');
26+
final _kRevertPattern = RegExp(
27+
r'^(?:Revert|revert:)\s"?(?<header>[\s\S]+?)"?\s*This reverts commit (?<hash>\w*)\.');
28+
const _kRevertCorrespondence = ['header', 'hash'];
29+
30+
final _kMentionsPattern = RegExp(r'@([\w-]+)');
31+
32+
Commit parse(String raw, {String? commentChar}) {
33+
if (raw.trim().isEmpty) {
34+
throw ArgumentError.value(raw, null, 'message raw must have content.');
4435
}
4536
String? body;
4637
String? footer;
4738
List<String> mentions = [];
4839
List<CommitNote> notes = [];
4940
List<CommitReference> references = [];
5041
Map<String, String?>? revert;
51-
final lines = truncateToScissor(message.split(RegExp(r'\r?\n')))
52-
.where(_gpgFilter)
53-
.toList();
42+
String? merge;
43+
String? header;
44+
final rawLines = _trimOffNewlines(raw).split(RegExp(r'\r?\n'));
45+
final lines = _truncateToScissor(rawLines).where(_gpgFilter).toList();
5446
if (commentChar != null) {
5547
lines.removeWhere((line) => line.startsWith(commentChar));
5648
}
57-
final header = lines.removeAt(0);
58-
final headerMatch = RegExp(headerPattern).matchAsPrefix(header);
49+
merge = lines.removeAt(0);
50+
final mergeMatch = _kMergePattern.firstMatch(merge);
51+
if (mergeMatch != null) {
52+
merge = mergeMatch.group(0);
53+
if (lines.isNotEmpty) {
54+
header = lines.removeAt(0);
55+
while (header!.trim().isEmpty && lines.isNotEmpty) {
56+
header = lines.removeAt(0);
57+
}
58+
}
59+
header ??= '';
60+
} else {
61+
header = merge;
62+
merge = null;
63+
}
64+
final headerMatch = _kHeaderPattern.firstMatch(header);
5965
final headerParts = <String, String?>{};
6066
if (headerMatch != null) {
61-
for (var i = 0; i < headerCorrespondence.length; i++) {
62-
headerParts[headerCorrespondence[i]] = headerMatch.group(i + 1);
67+
for (var name in _kHeaderCorrespondence) {
68+
headerParts[name] = headerMatch.namedGroup(name);
6369
}
6470
}
65-
final referencesPattern = getReferenceRegex(referenceActions);
66-
final referencePartsPattern = getReferencePartsRegex(issuePrefixes, false);
67-
references.addAll(getReferences(header,
71+
final referencesPattern = _getReferenceRegex(_kReferenceActions);
72+
final referencePartsPattern = _getReferencePartsRegex(_kIssuePrefixes, false);
73+
references.addAll(_getReferences(header,
6874
referencesPattern: referencesPattern,
6975
referencePartsPattern: referencePartsPattern));
7076

7177
bool continueNote = false;
7278
bool isBody = true;
73-
final notesPattern = getNotesRegex(noteKeywords);
79+
final notesPattern = _getNotesRegex(_kNoteKeywords);
7480

7581
/// body or footer
7682
for (var line in lines) {
7783
bool referenceMatched = false;
78-
final notesMatch = notesPattern.matchAsPrefix(line);
84+
final notesMatch = notesPattern.firstMatch(line);
7985
if (notesMatch != null) {
8086
continueNote = true;
8187
isBody = false;
82-
footer = append(footer, line);
83-
notes.add(CommitNote(
84-
title: notesMatch.group(1)!, text: notesMatch.group(2)!.trim()));
85-
break;
88+
footer = _append(footer, line);
89+
notes.add(
90+
CommitNote(title: notesMatch.group(1)!, text: notesMatch.group(2)!));
91+
continue;
8692
}
8793

88-
final lineReferences = getReferences(
94+
final lineReferences = _getReferences(
8995
line,
9096
referencesPattern: referencesPattern,
9197
referencePartsPattern: referencePartsPattern,
@@ -95,58 +101,72 @@ Commit parse(String raw,
95101
isBody = false;
96102
referenceMatched = true;
97103
continueNote = false;
104+
references.addAll(lineReferences);
98105
}
99106

100-
references.addAll(lineReferences);
101-
102107
if (referenceMatched) {
103-
footer = append(footer, line);
104-
break;
108+
footer = _append(footer, line);
109+
continue;
105110
}
106111

107112
if (continueNote) {
108-
notes.last.text = append(notes.last.text, line).trim();
109-
footer = append(footer, line);
110-
break;
113+
notes.last.text = _append(notes.last.text, line);
114+
footer = _append(footer, line);
115+
continue;
111116
}
112117
if (isBody) {
113-
body = append(body, line);
118+
body = _append(body, line);
114119
} else {
115-
footer = append(footer, line);
120+
footer = _append(footer, line);
116121
}
117122
}
118123

119-
Match? mentionsMatch;
120-
final mentionsPattern = RegExp(r'@([\w-]+)');
121-
while ((mentionsMatch =
122-
mentionsPattern.matchAsPrefix(raw, mentionsMatch?.end ?? 0)) !=
123-
null) {
124-
mentions.add(mentionsMatch!.group(1)!);
124+
Match? mentionsMatch = _kMentionsPattern.firstMatch(raw);
125+
while (mentionsMatch != null) {
126+
mentions.add(mentionsMatch.group(1)!);
127+
mentionsMatch = _kMentionsPattern.matchAsPrefix(raw, mentionsMatch.end);
125128
}
126129

127130
// does this commit revert any other commit?
128-
final revertMatch = raw.matchAsPrefix(revertPattern);
131+
final revertMatch = _kRevertPattern.firstMatch(raw);
129132
if (revertMatch != null) {
130133
revert = {};
131-
for (var i = 0; i < revertCorrespondence.length; i++) {
132-
revert[revertCorrespondence[i]] = revertMatch.group(i + 1);
134+
for (var i = 0; i < _kRevertCorrespondence.length; i++) {
135+
revert[_kRevertCorrespondence[i]] = revertMatch.group(i + 1);
133136
}
134137
}
135138

139+
for (var note in notes) {
140+
note.text = _trimOffNewlines(note.text);
141+
}
136142
return Commit(
143+
revert: revert,
144+
merge: merge,
137145
header: header,
138146
type: headerParts['type'],
139147
scope: headerParts['scope'],
140148
subject: headerParts['subject'],
141-
body: body?.trim(),
142-
footer: footer?.trim(),
149+
body: body != null ? _trimOffNewlines(body) : null,
150+
footer: footer != null ? _trimOffNewlines(footer) : null,
143151
notes: notes,
144152
references: references,
145153
mentions: mentions,
146-
revert: revert,
147154
);
148155
}
149156

157+
String _trimOffNewlines(String input) {
158+
final result = RegExp(r'[^\r\n]').firstMatch(input);
159+
if (result == null) {
160+
return '';
161+
}
162+
final firstIndex = result.start;
163+
var lastIndex = input.length - 1;
164+
while (input[lastIndex] == '\r' || input[lastIndex] == '\n') {
165+
lastIndex--;
166+
}
167+
return input.substring(firstIndex, lastIndex + 1);
168+
}
169+
150170
bool _gpgFilter(String line) {
151171
return !RegExp(r'^\s*gpg:').hasMatch(line);
152172
}
@@ -155,7 +175,7 @@ final _kMatchAll = RegExp(r'()(.+)', caseSensitive: false);
155175

156176
const _kScissor = '# ------------------------ >8 ------------------------';
157177

158-
List<String> truncateToScissor(List<String> lines) {
178+
List<String> _truncateToScissor(List<String> lines) {
159179
final scissorIndex = lines.indexOf(_kScissor);
160180

161181
if (scissorIndex == -1) {
@@ -165,48 +185,46 @@ List<String> truncateToScissor(List<String> lines) {
165185
return lines.sublist(0, scissorIndex);
166186
}
167187

168-
List<CommitReference> getReferences(
188+
List<CommitReference> _getReferences(
169189
String input, {
170-
required Pattern referencesPattern,
171-
required Pattern referencePartsPattern,
190+
required RegExp referencesPattern,
191+
required RegExp referencePartsPattern,
172192
}) {
173193
final references = <CommitReference>[];
174-
Match? referenceSentences;
175-
Match? referenceMatch;
176-
177-
final reApplicable = referencesPattern.allMatches(input).isNotEmpty
178-
? referencesPattern
179-
: _kMatchAll;
180-
while ((referenceSentences =
181-
reApplicable.matchAsPrefix(input, referenceSentences?.end ?? 0)) !=
182-
null) {
183-
final action = referenceSentences!.group(1)!;
184-
final sentence = referenceSentences.group(2)!;
185-
while ((referenceMatch = referencePartsPattern.matchAsPrefix(
186-
sentence, referenceMatch?.end ?? 0)) !=
187-
null) {
194+
final reApplicable =
195+
referencesPattern.hasMatch(input) ? referencesPattern : _kMatchAll;
196+
Match? referenceSentences = reApplicable.firstMatch(input);
197+
while (referenceSentences != null) {
198+
final action = referenceSentences.group(1);
199+
final sentence = referenceSentences.group(2);
200+
Match? referenceMatch = referencePartsPattern.firstMatch(sentence!);
201+
while (referenceMatch != null) {
188202
String? owner;
189-
String? repository = referenceMatch!.group(1);
203+
String? repository = referenceMatch.group(1);
190204
final ownerRepo = repository?.split('/') ?? [];
191205

192206
if (ownerRepo.length > 1) {
193207
owner = ownerRepo.removeAt(0);
194208
repository = ownerRepo.join('/');
195209
}
196210
references.add(CommitReference(
197-
raw: referenceMatch.group(0)!,
198211
action: action,
199212
owner: owner,
200213
repository: repository,
201214
issue: referenceMatch.group(3),
215+
raw: referenceMatch.group(0)!,
202216
prefix: referenceMatch.group(2)!,
203217
));
218+
referenceMatch =
219+
referencePartsPattern.matchAsPrefix(sentence, referenceMatch.end);
204220
}
221+
referenceSentences =
222+
reApplicable.matchAsPrefix(input, referenceSentences.end);
205223
}
206224
return references;
207225
}
208226

209-
Pattern getReferenceRegex(Iterable<String> referenceActions) {
227+
RegExp _getReferenceRegex(Iterable<String> referenceActions) {
210228
if (referenceActions.isEmpty) {
211229
// matches everything
212230
return RegExp(r'()(.+)', caseSensitive: false); //gi
@@ -217,7 +235,7 @@ Pattern getReferenceRegex(Iterable<String> referenceActions) {
217235
caseSensitive: false);
218236
}
219237

220-
Pattern getReferencePartsRegex(
238+
RegExp _getReferencePartsRegex(
221239
List<String> issuePrefixes, bool issuePrefixesCaseSensitive) {
222240
if (issuePrefixes.isEmpty) {
223241
return RegExp(r'(?!.*)');
@@ -227,17 +245,19 @@ Pattern getReferencePartsRegex(
227245
caseSensitive: issuePrefixesCaseSensitive);
228246
}
229247

230-
Pattern getNotesRegex(List<String> noteKeywords) {
248+
RegExp _getNotesRegex(List<String> noteKeywords) {
231249
if (noteKeywords.isEmpty) {
232250
return RegExp(r'(?!.*)');
233251
}
234252
final noteKeywordsSelection = noteKeywords.join('|');
235-
return RegExp('^[\\s|*]*($noteKeywordsSelection)[:\\s]+(.*)',
236-
caseSensitive: false);
253+
return RegExp(
254+
'^[\\s|*]*($noteKeywordsSelection)[:\\s]+(.*)',
255+
caseSensitive: false,
256+
);
237257
}
238258

239-
String append(String? src, String line) {
240-
if (src != null) {
259+
String _append(String? src, String line) {
260+
if (src != null && src.isNotEmpty) {
241261
return '$src\n$line';
242262
} else {
243263
return line;

lib/src/types/commit.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,18 @@ class CommitReference {
8989
this.repository,
9090
this.issue,
9191
});
92+
93+
@override
94+
operator ==(other) {
95+
return other is CommitReference &&
96+
raw == other.raw &&
97+
prefix == other.prefix &&
98+
action == other.action &&
99+
owner == other.owner &&
100+
repository == other.repository &&
101+
issue == other.issue;
102+
}
103+
104+
@override
105+
int get hashCode => raw.hashCode;
92106
}

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies:
1616
yaml: ^3.1.1
1717

1818
dev_dependencies:
19+
collection: ^1.17.1
1920
husky: ^0.1.6
2021
lint_staged: ^0.2.0
2122
lints: ^2.0.0

0 commit comments

Comments
 (0)