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
13 changes: 10 additions & 3 deletions lib/src/lint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<LintOutcome> lint(String message, Map<String, Rule> rules,
{bool? defaultIgnores, Iterable<String>? ignores}) async {
Future<LintOutcome> lint(
String message,
Map<String, Rule> rules, {
ParserOptions? parserOptions,
bool? defaultIgnores,
Iterable<String>? 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
Expand Down
12 changes: 8 additions & 4 deletions lib/src/load.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

///
Expand All @@ -31,11 +32,14 @@ Future<CommitLint> 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);
Expand Down
65 changes: 25 additions & 40 deletions lib/src/parse.dart
Original file line number Diff line number Diff line change
@@ -1,35 +1,14 @@
import 'types/commit.dart';
import 'types/parser.dart';

///
/// Parse Commit Message String to Convensional Commit
///

final _kHeaderPattern =
RegExp(r'^(?<type>\w*?)(\((?<scope>.*)\))?!?: (?<subject>.+)$');
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"?(?<header>[\s\S]+?)"?\s*This reverts commit (?<hash>\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.');
}
Expand All @@ -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) {
Expand All @@ -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 = <String, String?>{};
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) {
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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,
Expand Down
11 changes: 10 additions & 1 deletion lib/src/types/commitlint.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
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<String, Rule> rules;

final bool? defaultIgnores;

final Iterable<String>? ignores;

final ParserOptions? parser;

CommitLint inherit(CommitLint other) {
return CommitLint(
rules: {
Expand All @@ -20,6 +28,7 @@ class CommitLint {
...?other.ignores,
...?ignores,
],
parser: parser ?? other.parser,
);
}
}
82 changes: 82 additions & 0 deletions lib/src/types/parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:yaml/yaml.dart';

const _kHeaderPattern =
r'^(?<type>\w*)(?:\((?<scope>.*)\))?!?: (?<subject>.*)$';
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"?(?<header>[\s\S]+?)"?\s*This reverts commit (?<hash>\w*)\.';
const _kRevertCorrespondence = ['header', 'hash'];

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

class ParserOptions {
final List<String> issuePrefixes;
final List<String> noteKeywords;
final List<String> referenceActions;
final String headerPattern;
final List<String> headerCorrespondence;
final String revertPattern;
final List<String> 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<String>.from(yaml['issuePrefixes'] ?? _kIssuePrefixes),
noteKeywords: List<String>.from(yaml['noteKeywords'] ?? _kNoteKeywords),
referenceActions:
List<String>.from(yaml['referenceActions'] ?? _kReferenceActions),
headerPattern: yaml['headerPattern'] ?? _kHeaderPattern,
headerCorrespondence: List<String>.from(
yaml['headerCorrespondence'] ?? _kHeaderCorrespondence),
revertPattern: yaml['revertPattern'] ?? _kRevertPattern,
revertCorrespondence: List<String>.from(
yaml['revertCorrespondence'] ?? _kRevertCorrespondence),
mergePattern: yaml['mergePattern'] ?? _kMergePattern,
mentionsPattern: yaml['mentionsPattern'] ?? _kMentionsPattern,
);
}
}
17 changes: 17 additions & 0 deletions test/parse_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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' +
Expand Down
Loading