diff --git a/lib/src/lint.dart b/lib/src/lint.dart index 77873d7..4873c3c 100644 --- a/lib/src/lint.dart +++ b/lib/src/lint.dart @@ -3,20 +3,27 @@ import 'parse.dart'; import 'rules.dart'; import 'types/commit.dart'; import 'types/lint.dart'; +import 'types/parser.dart'; import 'types/rule.dart'; /// /// Lint commit [message] with configured [rules] /// -Future lint(String message, Map rules, - {bool? defaultIgnores, Iterable? ignores}) async { +Future lint( + String message, + Map rules, { + ParserOptions? parserOptions, + bool? defaultIgnores, + Iterable? ignores, +}) async { /// Found a wildcard match, skip if (isIgnored(message, defaultIgnores: defaultIgnores, ignores: ignores)) { return LintOutcome(input: message, valid: true, errors: [], warnings: []); } /// Parse the commit message - final commit = message.isEmpty ? Commit.empty() : parse(message); + final commit = + message.isEmpty ? Commit.empty() : parse(message, options: parserOptions); if (commit.header.isEmpty && commit.body == null && commit.footer == null) { /// Commit is empty, skip diff --git a/lib/src/load.dart b/lib/src/load.dart index 307fd2f..0155683 100644 --- a/lib/src/load.dart +++ b/lib/src/load.dart @@ -6,6 +6,7 @@ import 'package:yaml/yaml.dart'; import 'types/case.dart'; import 'types/commitlint.dart'; +import 'types/parser.dart'; import 'types/rule.dart'; /// @@ -31,11 +32,14 @@ Future load( final rules = yaml?['rules'] as YamlMap?; final ignores = yaml?['ignores'] as YamlList?; final defaultIgnores = yaml?['defaultIgnores'] as bool?; + final parser = yaml?['parser'] as YamlMap?; final config = CommitLint( - rules: rules?.map((key, value) => MapEntry(key, _extractRule(value))) ?? - {}, - ignores: ignores?.cast(), - defaultIgnores: defaultIgnores); + rules: + rules?.map((key, value) => MapEntry(key, _extractRule(value))) ?? {}, + ignores: ignores?.cast(), + defaultIgnores: defaultIgnores, + parser: parser != null ? ParserOptions.fromYaml(parser) : null, + ); if (include != null) { final upstream = await load(include, directory: file.parent); return config.inherit(upstream); diff --git a/lib/src/parse.dart b/lib/src/parse.dart index 3a646e3..ff70ed6 100644 --- a/lib/src/parse.dart +++ b/lib/src/parse.dart @@ -1,35 +1,14 @@ import 'types/commit.dart'; +import 'types/parser.dart'; /// /// Parse Commit Message String to Convensional Commit /// - -final _kHeaderPattern = - RegExp(r'^(?\w*?)(\((?.*)\))?!?: (?.+)$'); -const _kHeaderCorrespondence = ['type', 'scope', 'subject']; - -const _kReferenceActions = [ - 'close', - 'closes', - 'closed', - 'fix', - 'fixes', - 'fixed', - 'resolve', - 'resolves', - 'resolved' -]; - -const _kIssuePrefixes = ['#']; -const _kNoteKeywords = ['BREAKING CHANGE', 'BREAKING-CHANGE']; -final _kMergePattern = RegExp(r'^(Merge|merge)\s(.*)$'); -final _kRevertPattern = RegExp( - r'^(?:Revert|revert:)\s"?(?
[\s\S]+?)"?\s*This reverts commit (?\w*)\.'); -const _kRevertCorrespondence = ['header', 'hash']; - -final _kMentionsPattern = RegExp(r'@([\w-]+)'); - -Commit parse(String raw) { +Commit parse( + String raw, { + ParserOptions? options, +}) { + options ??= const ParserOptions(); if (raw.trim().isEmpty) { throw ArgumentError.value(raw, null, 'message raw must have content.'); } @@ -44,7 +23,7 @@ Commit parse(String raw) { 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); + final mergeMatch = RegExp(options.mergePattern).firstMatch(merge); if (mergeMatch != null) { merge = mergeMatch.group(0); if (lines.isNotEmpty) { @@ -58,22 +37,27 @@ Commit parse(String raw) { header = merge; merge = null; } - final headerMatch = _kHeaderPattern.firstMatch(header); + final headerMatch = RegExp(options.headerPattern).firstMatch(header); final headerParts = {}; if (headerMatch != null) { - for (var name in _kHeaderCorrespondence) { - headerParts[name] = headerMatch.namedGroup(name); + for (int i = 0; i < options.headerCorrespondence.length; i++) { + final String key = options.headerCorrespondence[i]; + headerParts[key] = headerMatch.group(i + 1); } + // for (var name in options.headerCorrespondence) { + // headerParts[name] = headerMatch.namedGroup(name); + // } } - final referencesPattern = _getReferenceRegex(_kReferenceActions); - final referencePartsPattern = _getReferencePartsRegex(_kIssuePrefixes, false); + final referencesPattern = _getReferenceRegex(options.referenceActions); + final referencePartsPattern = + _getReferencePartsRegex(options.issuePrefixes, false); references.addAll(_getReferences(header, referencesPattern: referencesPattern, referencePartsPattern: referencePartsPattern)); bool continueNote = false; bool isBody = true; - final notesPattern = _getNotesRegex(_kNoteKeywords); + final notesPattern = _getNotesRegex(options.noteKeywords); /// body or footer for (var line in lines) { @@ -118,18 +102,19 @@ Commit parse(String raw) { } } - Match? mentionsMatch = _kMentionsPattern.firstMatch(raw); + final mentionsRegex = RegExp(options.mentionsPattern); + Match? mentionsMatch = mentionsRegex.firstMatch(raw); while (mentionsMatch != null) { mentions.add(mentionsMatch.group(1)!); - mentionsMatch = _kMentionsPattern.matchAsPrefix(raw, mentionsMatch.end); + mentionsMatch = mentionsRegex.matchAsPrefix(raw, mentionsMatch.end); } // does this commit revert any other commit? - final revertMatch = _kRevertPattern.firstMatch(raw); + final revertMatch = RegExp(options.revertPattern).firstMatch(raw); if (revertMatch != null) { revert = {}; - for (var i = 0; i < _kRevertCorrespondence.length; i++) { - revert[_kRevertCorrespondence[i]] = revertMatch.group(i + 1); + for (var i = 0; i < options.revertCorrespondence.length; i++) { + revert[options.revertCorrespondence[i]] = revertMatch.group(i + 1); } } @@ -141,7 +126,7 @@ Commit parse(String raw) { merge: merge, header: header, type: headerParts['type'], - scopes: headerParts['scope']?.split(RegExp(r'(/|,|\\)')), + scopes: headerParts['scope']?.split(RegExp(r'\/|\\|, ?')), subject: headerParts['subject'], body: body != null ? _trimOffNewlines(body) : null, footer: footer != null ? _trimOffNewlines(footer) : null, diff --git a/lib/src/types/commitlint.dart b/lib/src/types/commitlint.dart index 24d351b..8abd835 100644 --- a/lib/src/types/commitlint.dart +++ b/lib/src/types/commitlint.dart @@ -1,7 +1,13 @@ +import 'parser.dart'; import 'rule.dart'; class CommitLint { - CommitLint({this.rules = const {}, this.defaultIgnores, this.ignores}); + CommitLint({ + this.rules = const {}, + this.defaultIgnores, + this.ignores, + this.parser, + }); final Map rules; @@ -9,6 +15,8 @@ class CommitLint { final Iterable? ignores; + final ParserOptions? parser; + CommitLint inherit(CommitLint other) { return CommitLint( rules: { @@ -20,6 +28,7 @@ class CommitLint { ...?other.ignores, ...?ignores, ], + parser: parser ?? other.parser, ); } } diff --git a/lib/src/types/parser.dart b/lib/src/types/parser.dart new file mode 100644 index 0000000..e81b3e1 --- /dev/null +++ b/lib/src/types/parser.dart @@ -0,0 +1,82 @@ +import 'package:yaml/yaml.dart'; + +const _kHeaderPattern = + r'^(?\w*)(?:\((?.*)\))?!?: (?.*)$'; +const _kHeaderCorrespondence = ['type', 'scope', 'subject']; + +const _kReferenceActions = [ + 'close', + 'closes', + 'closed', + 'fix', + 'fixes', + 'fixed', + 'resolve', + 'resolves', + 'resolved' +]; + +const _kIssuePrefixes = ['#']; +const _kNoteKeywords = ['BREAKING CHANGE', 'BREAKING-CHANGE']; +const _kMergePattern = r'^(Merge|merge)\s(.*)$'; +const _kRevertPattern = + r'^(?:Revert|revert:)\s"?(?
[\s\S]+?)"?\s*This reverts commit (?\w*)\.'; +const _kRevertCorrespondence = ['header', 'hash']; + +const _kMentionsPattern = r'@([\w-]+)'; + +class ParserOptions { + final List issuePrefixes; + final List noteKeywords; + final List referenceActions; + final String headerPattern; + final List headerCorrespondence; + final String revertPattern; + final List revertCorrespondence; + final String mergePattern; + final String mentionsPattern; + + const ParserOptions({ + this.issuePrefixes = _kIssuePrefixes, + this.noteKeywords = _kNoteKeywords, + this.referenceActions = _kReferenceActions, + this.headerPattern = _kHeaderPattern, + this.headerCorrespondence = _kHeaderCorrespondence, + this.revertPattern = _kRevertPattern, + this.revertCorrespondence = _kRevertCorrespondence, + this.mergePattern = _kMergePattern, + this.mentionsPattern = _kMentionsPattern, + }); + + ParserOptions copyWith(ParserOptions? other) { + return ParserOptions( + issuePrefixes: other?.issuePrefixes ?? issuePrefixes, + noteKeywords: other?.noteKeywords ?? noteKeywords, + referenceActions: other?.referenceActions ?? referenceActions, + headerPattern: other?.headerPattern ?? headerPattern, + headerCorrespondence: other?.headerCorrespondence ?? headerCorrespondence, + revertPattern: other?.revertPattern ?? revertPattern, + revertCorrespondence: other?.revertCorrespondence ?? revertCorrespondence, + mergePattern: other?.mergePattern ?? mergePattern, + mentionsPattern: other?.mentionsPattern ?? mentionsPattern, + ); + } + + static ParserOptions fromYaml(YamlMap yaml) { + return ParserOptions( + issuePrefixes: + List.from(yaml['issuePrefixes'] ?? _kIssuePrefixes), + noteKeywords: List.from(yaml['noteKeywords'] ?? _kNoteKeywords), + referenceActions: + List.from(yaml['referenceActions'] ?? _kReferenceActions), + headerPattern: yaml['headerPattern'] ?? _kHeaderPattern, + headerCorrespondence: List.from( + yaml['headerCorrespondence'] ?? _kHeaderCorrespondence), + revertPattern: yaml['revertPattern'] ?? _kRevertPattern, + revertCorrespondence: List.from( + yaml['revertCorrespondence'] ?? _kRevertCorrespondence), + mergePattern: yaml['mergePattern'] ?? _kMergePattern, + mentionsPattern: yaml['mentionsPattern'] ?? _kMentionsPattern, + ); + } +} diff --git a/test/parse_test.dart b/test/parse_test.dart index 692131b..1cace6f 100644 --- a/test/parse_test.dart +++ b/test/parse_test.dart @@ -3,6 +3,7 @@ import 'package:collection/collection.dart'; import 'package:commitlint_cli/src/parse.dart'; import 'package:commitlint_cli/src/types/commit.dart'; +import 'package:commitlint_cli/src/types/parser.dart'; import 'package:test/test.dart'; void main() { @@ -255,6 +256,22 @@ void main() { expect(commit.body, equals('this is some body before a scissors-line')); }); + test('should use custom parser options with headerPattern', () { + final commit = parse('type(scope)-subject', + options: ParserOptions(headerPattern: r'^(\w*)(?:\((.*)\))?-(.*)$')); + expect(commit.header, equals('type(scope)-subject')); + expect(commit.scopes, equals(['scope'])); + expect(commit.subject, equals('subject')); + expect(commit.type, equals('type')); + }); + + test('should use custom parser options with custom issuePrefixes', () { + final commit = parse('fix: change git convention to fix CD workflow sv-4', + options: ParserOptions(issuePrefixes: ['sv-'])); + expect(commit.type, equals('fix')); + expect(commit.references.first.issue, equals('4')); + }); + group('merge commits', () { final githubCommit = parse( 'Merge pull request #1 from user/feature/feature-name\n' +