Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/pr_title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ jobs:
run: dart pub get

- name: Validate Title of PR
run: echo ${{ github.event.pull_request.title }} | dart bin/commitlint_cli.dart
run: echo ${{ github.event.pull_request.title }} | dart bin/commitlint_cli.dart --config lib/commitlint.yaml
201 changes: 109 additions & 92 deletions lib/src/parse.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import 'types/commit.dart';
/// Parse Commit Message String to Convensional Commit
///

const _kDefaultHeaderPattern = r'^(\w*)(?:\((.*)\))?: (.*)$';
const _kDefaultHeaderCorrespondence = ['type', 'scope', 'subject'];
final _kHeaderPattern =
RegExp(r'^(?<type>\w*?)(\((?<scope>.*)\))?!?: (?<subject>.+)$');
const _kHeaderCorrespondence = ['type', 'scope', 'subject'];

const _kDefaultReferenceActions = [
const _kReferenceActions = [
'close',
'closes',
'closed',
Expand All @@ -19,73 +20,75 @@ const _kDefaultReferenceActions = [
'resolved'
];

const _kDefaultIssuePrefixes = ['#'];
const _kDefaultNoteKeywords = ['BREAKING CHANGE', 'BREAKING-CHANGE'];

const _kDefaultFieldPattern = r'^-(.*?)-$';

const _kDefaultRevertPattern =
r'^(?:Revert|revert:)\s"?([\s\S]+?)"?\s*This reverts commit (\w*)\.';
const _kDefaultRevertCorrespondence = ['header', 'hash'];

Commit parse(String raw,
{String headerPattern = _kDefaultHeaderPattern,
List<String> headerCorrespondence = _kDefaultHeaderCorrespondence,
List<String> referenceActions = _kDefaultReferenceActions,
List<String> issuePrefixes = _kDefaultIssuePrefixes,
List<String> noteKeywords = _kDefaultNoteKeywords,
String fieldPattern = _kDefaultFieldPattern,
String revertPattern = _kDefaultRevertPattern,
List<String> revertCorrespondence = _kDefaultRevertCorrespondence,
String? commentChar}) {
final message = raw.trim();
if (message.isEmpty) {
throw ArgumentError.value(raw, 'raw message', 'must have content.');
const _kIssuePrefixes = ['#'];
const _kNoteKeywords = ['BREAKING CHANGE', 'BREAKING-CHANGE'];
final _kMergePattern = RegExp(r'^(Merge|merge)\s(.*)$');
final _kRevertPattern = RegExp(
r'^(?:Revert|revert:)\s"?(?<header>[\s\S]+?)"?\s*This reverts commit (?<hash>\w*)\.');
const _kRevertCorrespondence = ['header', 'hash'];

final _kMentionsPattern = RegExp(r'@([\w-]+)');

Commit parse(String raw) {
if (raw.trim().isEmpty) {
throw ArgumentError.value(raw, null, 'message raw must have content.');
}
String? body;
String? footer;
List<String> mentions = [];
List<CommitNote> notes = [];
List<CommitReference> references = [];
Map<String, String?>? revert;
final lines = truncateToScissor(message.split(RegExp(r'\r?\n')))
.where(_gpgFilter)
.toList();
if (commentChar != null) {
lines.removeWhere((line) => line.startsWith(commentChar));
String? merge;
String? header;
final rawLines = _trimOffNewlines(raw).split(RegExp(r'\r?\n'));
final lines = _truncateToScissor(rawLines).where(_gpgFilter).toList();
merge = lines.removeAt(0);
final mergeMatch = _kMergePattern.firstMatch(merge);
if (mergeMatch != null) {
merge = mergeMatch.group(0);
if (lines.isNotEmpty) {
header = lines.removeAt(0);
while (header!.trim().isEmpty && lines.isNotEmpty) {
header = lines.removeAt(0);
}
}
header ??= '';
} else {
header = merge;
merge = null;
}
final header = lines.removeAt(0);
final headerMatch = RegExp(headerPattern).matchAsPrefix(header);
final headerMatch = _kHeaderPattern.firstMatch(header);
final headerParts = <String, String?>{};
if (headerMatch != null) {
for (var i = 0; i < headerCorrespondence.length; i++) {
headerParts[headerCorrespondence[i]] = headerMatch.group(i + 1);
for (var name in _kHeaderCorrespondence) {
headerParts[name] = headerMatch.namedGroup(name);
}
}
final referencesPattern = getReferenceRegex(referenceActions);
final referencePartsPattern = getReferencePartsRegex(issuePrefixes, false);
references.addAll(getReferences(header,
final referencesPattern = _getReferenceRegex(_kReferenceActions);
final referencePartsPattern = _getReferencePartsRegex(_kIssuePrefixes, false);
references.addAll(_getReferences(header,
referencesPattern: referencesPattern,
referencePartsPattern: referencePartsPattern));

bool continueNote = false;
bool isBody = true;
final notesPattern = getNotesRegex(noteKeywords);
final notesPattern = _getNotesRegex(_kNoteKeywords);

/// body or footer
for (var line in lines) {
bool referenceMatched = false;
final notesMatch = notesPattern.matchAsPrefix(line);
final notesMatch = notesPattern.firstMatch(line);
if (notesMatch != null) {
continueNote = true;
isBody = false;
footer = append(footer, line);
notes.add(CommitNote(
title: notesMatch.group(1)!, text: notesMatch.group(2)!.trim()));
break;
footer = _append(footer, line);
notes.add(
CommitNote(title: notesMatch.group(1)!, text: notesMatch.group(2)!));
continue;
}

final lineReferences = getReferences(
final lineReferences = _getReferences(
line,
referencesPattern: referencesPattern,
referencePartsPattern: referencePartsPattern,
Expand All @@ -95,58 +98,72 @@ Commit parse(String raw,
isBody = false;
referenceMatched = true;
continueNote = false;
references.addAll(lineReferences);
}

references.addAll(lineReferences);

if (referenceMatched) {
footer = append(footer, line);
break;
footer = _append(footer, line);
continue;
}

if (continueNote) {
notes.last.text = append(notes.last.text, line).trim();
footer = append(footer, line);
break;
notes.last.text = _append(notes.last.text, line);
footer = _append(footer, line);
continue;
}
if (isBody) {
body = append(body, line);
body = _append(body, line);
} else {
footer = append(footer, line);
footer = _append(footer, line);
}
}

Match? mentionsMatch;
final mentionsPattern = RegExp(r'@([\w-]+)');
while ((mentionsMatch =
mentionsPattern.matchAsPrefix(raw, mentionsMatch?.end ?? 0)) !=
null) {
mentions.add(mentionsMatch!.group(1)!);
Match? mentionsMatch = _kMentionsPattern.firstMatch(raw);
while (mentionsMatch != null) {
mentions.add(mentionsMatch.group(1)!);
mentionsMatch = _kMentionsPattern.matchAsPrefix(raw, mentionsMatch.end);
}

// does this commit revert any other commit?
final revertMatch = raw.matchAsPrefix(revertPattern);
final revertMatch = _kRevertPattern.firstMatch(raw);
if (revertMatch != null) {
revert = {};
for (var i = 0; i < revertCorrespondence.length; i++) {
revert[revertCorrespondence[i]] = revertMatch.group(i + 1);
for (var i = 0; i < _kRevertCorrespondence.length; i++) {
revert[_kRevertCorrespondence[i]] = revertMatch.group(i + 1);
}
}

for (var note in notes) {
note.text = _trimOffNewlines(note.text);
}
return Commit(
revert: revert,
merge: merge,
header: header,
type: headerParts['type'],
scope: headerParts['scope'],
subject: headerParts['subject'],
body: body?.trim(),
footer: footer?.trim(),
body: body != null ? _trimOffNewlines(body) : null,
footer: footer != null ? _trimOffNewlines(footer) : null,
notes: notes,
references: references,
mentions: mentions,
revert: revert,
);
}

String _trimOffNewlines(String input) {
final result = RegExp(r'[^\r\n]').firstMatch(input);
if (result == null) {
return '';
}
final firstIndex = result.start;
var lastIndex = input.length - 1;
while (input[lastIndex] == '\r' || input[lastIndex] == '\n') {
lastIndex--;
}
return input.substring(firstIndex, lastIndex + 1);
}

bool _gpgFilter(String line) {
return !RegExp(r'^\s*gpg:').hasMatch(line);
}
Expand All @@ -155,7 +172,7 @@ final _kMatchAll = RegExp(r'()(.+)', caseSensitive: false);

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

List<String> truncateToScissor(List<String> lines) {
List<String> _truncateToScissor(List<String> lines) {
final scissorIndex = lines.indexOf(_kScissor);

if (scissorIndex == -1) {
Expand All @@ -165,48 +182,46 @@ List<String> truncateToScissor(List<String> lines) {
return lines.sublist(0, scissorIndex);
}

List<CommitReference> getReferences(
List<CommitReference> _getReferences(
String input, {
required Pattern referencesPattern,
required Pattern referencePartsPattern,
required RegExp referencesPattern,
required RegExp referencePartsPattern,
}) {
final references = <CommitReference>[];
Match? referenceSentences;
Match? referenceMatch;

final reApplicable = referencesPattern.allMatches(input).isNotEmpty
? referencesPattern
: _kMatchAll;
while ((referenceSentences =
reApplicable.matchAsPrefix(input, referenceSentences?.end ?? 0)) !=
null) {
final action = referenceSentences!.group(1)!;
final sentence = referenceSentences.group(2)!;
while ((referenceMatch = referencePartsPattern.matchAsPrefix(
sentence, referenceMatch?.end ?? 0)) !=
null) {
final reApplicable =
referencesPattern.hasMatch(input) ? referencesPattern : _kMatchAll;
Match? referenceSentences = reApplicable.firstMatch(input);
while (referenceSentences != null) {
final action = referenceSentences.group(1);
final sentence = referenceSentences.group(2);
Match? referenceMatch = referencePartsPattern.firstMatch(sentence!);
while (referenceMatch != null) {
String? owner;
String? repository = referenceMatch!.group(1);
String? repository = referenceMatch.group(1);
final ownerRepo = repository?.split('/') ?? [];

if (ownerRepo.length > 1) {
owner = ownerRepo.removeAt(0);
repository = ownerRepo.join('/');
}
references.add(CommitReference(
raw: referenceMatch.group(0)!,
action: action,
owner: owner,
repository: repository,
issue: referenceMatch.group(3),
raw: referenceMatch.group(0)!,
prefix: referenceMatch.group(2)!,
));
referenceMatch =
referencePartsPattern.matchAsPrefix(sentence, referenceMatch.end);
}
referenceSentences =
reApplicable.matchAsPrefix(input, referenceSentences.end);
}
return references;
}

Pattern getReferenceRegex(Iterable<String> referenceActions) {
RegExp _getReferenceRegex(Iterable<String> referenceActions) {
if (referenceActions.isEmpty) {
// matches everything
return RegExp(r'()(.+)', caseSensitive: false); //gi
Expand All @@ -217,7 +232,7 @@ Pattern getReferenceRegex(Iterable<String> referenceActions) {
caseSensitive: false);
}

Pattern getReferencePartsRegex(
RegExp _getReferencePartsRegex(
List<String> issuePrefixes, bool issuePrefixesCaseSensitive) {
if (issuePrefixes.isEmpty) {
return RegExp(r'(?!.*)');
Expand All @@ -227,17 +242,19 @@ Pattern getReferencePartsRegex(
caseSensitive: issuePrefixesCaseSensitive);
}

Pattern getNotesRegex(List<String> noteKeywords) {
RegExp _getNotesRegex(List<String> noteKeywords) {
if (noteKeywords.isEmpty) {
return RegExp(r'(?!.*)');
}
final noteKeywordsSelection = noteKeywords.join('|');
return RegExp('^[\\s|*]*($noteKeywordsSelection)[:\\s]+(.*)',
caseSensitive: false);
return RegExp(
'^[\\s|*]*($noteKeywordsSelection)[:\\s]+(.*)',
caseSensitive: false,
);
}

String append(String? src, String line) {
if (src != null) {
String _append(String? src, String line) {
if (src != null && src.isNotEmpty) {
return '$src\n$line';
} else {
return line;
Expand Down
14 changes: 14 additions & 0 deletions lib/src/types/commit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,18 @@ class CommitReference {
this.repository,
this.issue,
});

@override
operator ==(other) {
return other is CommitReference &&
raw == other.raw &&
prefix == other.prefix &&
action == other.action &&
owner == other.owner &&
repository == other.repository &&
issue == other.issue;
}

@override
int get hashCode => raw.hashCode;
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies:
yaml: ^3.1.1

dev_dependencies:
collection: ^1.17.1
husky: ^0.1.6
lint_staged: ^0.2.0
lints: ^2.0.0
Expand Down
Loading