From ab89dfebe73e6fff68e64fed04c45bfb528af896 Mon Sep 17 00:00:00 2001 From: hyiso Date: Sun, 26 Mar 2023 22:38:25 +0800 Subject: [PATCH] feat!: refactor parse --- .github/workflows/pr_title.yml | 2 +- lib/src/parse.dart | 201 ++++++++-------- lib/src/types/commit.dart | 14 ++ pubspec.yaml | 1 + test/parse_test.dart | 410 ++++++++++++++++++++++++--------- 5 files changed, 429 insertions(+), 199 deletions(-) diff --git a/.github/workflows/pr_title.yml b/.github/workflows/pr_title.yml index 63a70ba..2d0736d 100644 --- a/.github/workflows/pr_title.yml +++ b/.github/workflows/pr_title.yml @@ -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 \ No newline at end of file + run: echo ${{ github.event.pull_request.title }} | dart bin/commitlint_cli.dart --config lib/commitlint.yaml \ No newline at end of file diff --git a/lib/src/parse.dart b/lib/src/parse.dart index 81bd6f3..c397c75 100644 --- a/lib/src/parse.dart +++ b/lib/src/parse.dart @@ -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'^(?\w*?)(\((?.*)\))?!?: (?.+)$'); +const _kHeaderCorrespondence = ['type', 'scope', 'subject']; -const _kDefaultReferenceActions = [ +const _kReferenceActions = [ 'close', 'closes', 'closed', @@ -19,28 +20,18 @@ 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 headerCorrespondence = _kDefaultHeaderCorrespondence, - List referenceActions = _kDefaultReferenceActions, - List issuePrefixes = _kDefaultIssuePrefixes, - List noteKeywords = _kDefaultNoteKeywords, - String fieldPattern = _kDefaultFieldPattern, - String revertPattern = _kDefaultRevertPattern, - List 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"?(?
[\s\S]+?)"?\s*This reverts commit (?\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; @@ -48,44 +39,56 @@ Commit parse(String raw, List notes = []; List references = []; Map? 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 = {}; 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, @@ -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); } @@ -155,7 +172,7 @@ final _kMatchAll = RegExp(r'()(.+)', caseSensitive: false); const _kScissor = '# ------------------------ >8 ------------------------'; -List truncateToScissor(List lines) { +List _truncateToScissor(List lines) { final scissorIndex = lines.indexOf(_kScissor); if (scissorIndex == -1) { @@ -165,28 +182,22 @@ List truncateToScissor(List lines) { return lines.sublist(0, scissorIndex); } -List getReferences( +List _getReferences( String input, { - required Pattern referencesPattern, - required Pattern referencePartsPattern, + required RegExp referencesPattern, + required RegExp referencePartsPattern, }) { final references = []; - 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) { @@ -194,19 +205,23 @@ List getReferences( 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 referenceActions) { +RegExp _getReferenceRegex(Iterable referenceActions) { if (referenceActions.isEmpty) { // matches everything return RegExp(r'()(.+)', caseSensitive: false); //gi @@ -217,7 +232,7 @@ Pattern getReferenceRegex(Iterable referenceActions) { caseSensitive: false); } -Pattern getReferencePartsRegex( +RegExp _getReferencePartsRegex( List issuePrefixes, bool issuePrefixesCaseSensitive) { if (issuePrefixes.isEmpty) { return RegExp(r'(?!.*)'); @@ -227,17 +242,19 @@ Pattern getReferencePartsRegex( caseSensitive: issuePrefixesCaseSensitive); } -Pattern getNotesRegex(List noteKeywords) { +RegExp _getNotesRegex(List 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; diff --git a/lib/src/types/commit.dart b/lib/src/types/commit.dart index dd7d16e..ad413f5 100644 --- a/lib/src/types/commit.dart +++ b/lib/src/types/commit.dart @@ -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; } diff --git a/pubspec.yaml b/pubspec.yaml index 50eb338..b835922 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/parse_test.dart b/test/parse_test.dart index 7b1ba97..34a1e4c 100644 --- a/test/parse_test.dart +++ b/test/parse_test.dart @@ -1,115 +1,313 @@ +// ignore_for_file: prefer_interpolation_to_compose_strings, prefer_adjacent_string_concatenation + +import 'package:collection/collection.dart'; import 'package:commitlint_cli/src/parse.dart'; +import 'package:commitlint_cli/src/types/commit.dart'; import 'package:test/test.dart'; void main() { - test('throws when called with empty message', () { - expect(() => parse(''), throwsArgumentError); - }); - test('returns object with expected keys', () { - final message = 'message'; - final commit = parse(message); - expect(commit.body, equals(null)); - expect(commit.footer, equals(null)); - expect(commit.header, equals('message')); - expect(commit.mentions, equals([])); - expect(commit.notes, equals([])); - expect(commit.references, equals([])); - expect(commit.type, equals(null)); - expect(commit.scope, equals(null)); - expect(commit.subject, equals(null)); - }); - test('uses default options', () { - final message = 'type(scope): subject'; - final commit = parse(message); - expect(commit.body, equals(null)); - expect(commit.footer, equals(null)); - expect(commit.header, equals('type(scope): subject')); - expect(commit.mentions, equals([])); - expect(commit.notes, equals([])); - expect(commit.references, equals([])); - expect(commit.type, equals('type')); - expect(commit.scope, equals('scope')); - expect(commit.subject, equals('subject')); - }); + group('parse', () { + test('throws when called with empty message', () { + expect(() => parse(''), throwsArgumentError); + expect(() => parse('\n'), throwsArgumentError); + expect(() => parse(' '), throwsArgumentError); + }); + test('supports plain message', () { + final message = 'message'; + final commit = parse(message); + expect(commit.body, equals(null)); + expect(commit.footer, equals(null)); + expect(commit.header, equals('message')); + expect(commit.mentions, equals([])); + expect(commit.notes, equals([])); + expect(commit.references, equals([])); + expect(commit.type, equals(null)); + expect(commit.scope, equals(null)); + expect(commit.subject, equals(null)); + }); + test('supports `type(scope): subject`', () { + final message = 'type(scope): subject'; + final commit = parse(message); + expect(commit.body, equals(null)); + expect(commit.footer, equals(null)); + expect(commit.header, equals('type(scope): subject')); + expect(commit.mentions, equals([])); + expect(commit.notes, equals([])); + expect(commit.references, equals([])); + expect(commit.type, equals('type')); + expect(commit.scope, equals('scope')); + expect(commit.subject, equals('subject')); + }); + test('supports `type!: subject`', () { + final message = 'type!: subject'; + final commit = parse(message); + expect(commit.body, equals(null)); + expect(commit.footer, equals(null)); + expect(commit.header, equals('type!: subject')); + expect(commit.mentions, equals([])); + expect(commit.notes, equals([])); + expect(commit.references, equals([])); + expect(commit.type, equals('type')); + expect(commit.scope, equals(null)); + expect(commit.subject, equals('subject')); + }); + test('supports `type(scope)!: subject`', () { + final message = 'type(scope)!: subject'; + final commit = parse(message); + expect(commit.body, equals(null)); + expect(commit.footer, equals(null)); + expect(commit.header, equals('type(scope)!: subject')); + expect(commit.mentions, equals([])); + expect(commit.notes, equals([])); + expect(commit.references, equals([])); + expect(commit.type, equals('type')); + expect(commit.scope, equals('scope')); + expect(commit.subject, equals('subject')); + }); + test('supports scopes with /', () { + const message = 'type(some/scope): subject'; + final commit = parse(message); + expect(commit.scope, equals('some/scope')); + expect(commit.subject, equals('subject')); + }); - test('uses custom opts parser', () { - final message = 'type(scope)-subject'; - final commit = parse(message, headerPattern: r'^(\w*)(?:\((.*)\))?-(.*)$'); - expect(commit.body, equals(null)); - expect(commit.footer, equals(null)); - expect(commit.header, equals('type(scope)-subject')); - expect(commit.mentions, equals([])); - expect(commit.notes, equals([])); - expect(commit.references, equals([])); - expect(commit.type, equals('type')); - expect(commit.scope, equals('scope')); - expect(commit.subject, equals('subject')); - }); + test('keep -side notes- in the body section', () { + final header = "type(some/scope): subject"; + final body = + "CI on master branch caught this:\n\n```\nUnhandled Exception:\nSystem.AggregateException: One or more errors occurred. (Some problem when connecting to 'api.mycryptoapi.com/eth')\n\n--- End of stack trace from previous location where exception was thrown ---\n\nat GWallet.Backend.FSharpUtil.ReRaise (System.Exception ex) [0x00000] in /Users/runner/work/geewallet/geewallet/src/GWallet.Backend/FSharpUtil.fs:206\n...\n```"; + final message = "$header\n\n$body"; + final commit = parse(message); + expect(commit.body, equals(body)); + }); + test('parses references leading subject', () { + const message = '#1 some subject'; + final commit = parse(message); + expect(commit.references.first.issue, equals('1')); + }); + test('works with chinese scope by default', () { + const message = 'fix(面试评价): 测试'; + final commit = parse(message); + expect(commit.subject, isNotNull); + expect(commit.scope, isNotNull); + }); + test('should trim extra newlines', () { + final commit = parse( + '\n\n\n\n\n\n\nfeat(scope): broadcast destroy event on scope destruction\n\n\n' + + '\n\n\nperf testing shows that in chrome this change adds 5-15% overhead\n' + + '\n\n\nwhen destroying 10k nested scopes where each scope has a destroy listener\n\n' + + '\n\n\n\nBREAKING CHANGE: some breaking change\n' + + '\n\n\n\nBREAKING CHANGE: An awesome breaking change\n\n\n```\ncode here\n```' + + '\n\nfixes #1\n' + + '\n\n\nfixed #25\n\n\n\n\n'); + expect(commit.merge, equals(null)); + expect(commit.revert, equals(null)); + expect(commit.header, + equals('feat(scope): broadcast destroy event on scope destruction')); + expect(commit.type, equals('feat')); + expect(commit.scope, equals('scope')); + expect(commit.subject, + equals('broadcast destroy event on scope destruction')); + expect( + commit.body, + equals( + 'perf testing shows that in chrome this change adds 5-15% overhead\n\n\n\nwhen destroying 10k nested scopes where each scope has a destroy listener')); + expect( + commit.footer, + equals( + 'BREAKING CHANGE: some breaking change\n\n\n\n\nBREAKING CHANGE: An awesome breaking change\n\n\n```\ncode here\n```\n\nfixes #1\n\n\n\nfixed #25')); + expect(commit.mentions, equals([])); - test('does not merge array properties with custom opts', () { - final message = 'type: subject'; - final commit = parse(message, - headerPattern: r'^(.*):\s(.*)$', - headerCorrespondence: ['type', 'subject']); - expect(commit.body, equals(null)); - expect(commit.footer, equals(null)); - expect(commit.header, equals('type: subject')); - expect(commit.mentions, equals([])); - expect(commit.notes, equals([])); - expect(commit.references, equals([])); - expect(commit.type, equals('type')); - expect(commit.scope, equals(null)); - expect(commit.subject, equals('subject')); - }); - test('supports scopes with /', () { - const message = 'type(some/scope): subject'; - final commit = parse(message); - expect(commit.scope, equals('some/scope')); - expect(commit.subject, equals('subject')); - }); - test('registers inline #', () { - const message = - 'type(some/scope): subject #reference\n# some comment\nthings #reference'; - final commit = parse(message, commentChar: '#'); - expect(commit.subject, equals('subject #reference')); - expect(commit.body, equals('things #reference')); - }); + expect(commit.notes.first.title, equals('BREAKING CHANGE')); + expect(commit.notes.first.text, equals('some breaking change')); + expect(commit.notes.last.title, equals('BREAKING CHANGE')); + expect(commit.notes.last.text, + equals('An awesome breaking change\n\n\n```\ncode here\n```')); + expect( + ListEquality().equals(commit.references, [ + CommitReference( + raw: '#1', + prefix: '#', + action: 'fixes', + owner: null, + repository: null, + issue: '1'), + CommitReference( + raw: '#25', + prefix: '#', + action: 'fixed', + owner: null, + repository: null, + issue: '25'), + ]), + true); + }); - test('keep -side notes- in the body section', () { - final header = "type(some/scope): subject"; - final body = - "CI on master branch caught this:\n\n```\nUnhandled Exception:\nSystem.AggregateException: One or more errors occurred. (Some problem when connecting to 'api.mycryptoapi.com/eth')\n\n--- End of stack trace from previous location where exception was thrown ---\n\nat GWallet.Backend.FSharpUtil.ReRaise (System.Exception ex) [0x00000] in /Users/runner/work/geewallet/geewallet/src/GWallet.Backend/FSharpUtil.fs:206\n...\n```"; - final message = "$header\n\n$body"; - final commit = parse(message); - expect(commit.body, equals(body)); - }); - test('parses references leading subject', () { - const message = '#1 some subject'; - final commit = parse(message); - expect(commit.references.first.issue, equals('1')); - }); - test('parses custom references', () { - final message = '#1 some subject PREFIX-2'; - final commit = parse(message, issuePrefixes: ['PREFIX-']); - expect(commit.references.any((ref) => ref.issue == '1'), false); - final ref = commit.references.firstWhere((ref) => ref.issue == '2'); - expect(ref.prefix, equals('PREFIX-')); - expect(ref.owner, equals(null)); - expect(ref.action, equals('')); - expect(ref.repository, equals(null)); - expect(ref.raw, equals('#1 some subject PREFIX-2')); - }); - test('works with chinese scope by default', () { - const message = 'fix(面试评价): 测试'; - final commit = parse(message, commentChar: '#'); - expect(commit.subject, isNotNull); - expect(commit.scope, isNotNull); - }); - test('does not work with chinese scopes with incompatible pattern', () { - const message = 'fix(面试评价): 测试'; - final commit = - parse(message, headerPattern: r'^(\w*)(?:\(([a-z]*)\))?: (.*)$'); - expect(commit.subject, equals(null)); - expect(commit.scope, equals(null)); + test('should keep spaces', () { + final commit = parse(' feat(scope): broadcast destroy event on scope destruction \n' + + ' perf testing shows that in chrome this change adds 5-15% overhead \n\n' + + ' when destroying 10k nested scopes where each scope has a destroy listener \n' + + ' BREAKING CHANGE: some breaking change \n\n' + + ' BREAKING CHANGE: An awesome breaking change\n\n\n```\ncode here\n```' + + '\n\n fixes #1\n'); + expect(commit.merge, equals(null)); + expect(commit.revert, equals(null)); + expect( + commit.header, + equals( + ' feat(scope): broadcast destroy event on scope destruction ')); + expect(commit.type, equals(null)); + expect(commit.scope, equals(null)); + expect(commit.subject, equals(null)); + expect( + commit.body, + equals( + ' perf testing shows that in chrome this change adds 5-15% overhead \n\n when destroying 10k nested scopes where each scope has a destroy listener ')); + expect( + commit.footer, + equals( + ' BREAKING CHANGE: some breaking change \n\n BREAKING CHANGE: An awesome breaking change\n\n\n```\ncode here\n```\n\n fixes #1')); + expect(commit.mentions, equals([])); + expect(commit.notes.first.title, equals('BREAKING CHANGE')); + expect(commit.notes.first.text, equals('some breaking change ')); + expect(commit.notes.last.title, equals('BREAKING CHANGE')); + expect(commit.notes.last.text, + equals('An awesome breaking change\n\n\n```\ncode here\n```')); + expect( + ListEquality().equals(commit.references, [ + CommitReference( + action: 'fixes', + owner: null, + repository: null, + issue: '1', + raw: '#1', + prefix: '#', + ), + ]), + true); + }); + + test('should ignore gpg signature lines', () { + final commit = parse('gpg: Signature made Thu Oct 22 12:19:30 2020 EDT\n' + + 'gpg: using RSA key ABCDEF1234567890\n' + + 'gpg: Good signature from "Author " [ultimate]\n' + + 'feat(scope): broadcast destroy event on scope destruction\n' + + 'perf testing shows that in chrome this change adds 5-15% overhead\n' + + 'when destroying 10k nested scopes where each scope has a destroy listener\n' + + 'BREAKING CHANGE: some breaking change\n' + + 'fixes #1\n'); + expect(commit.merge, equals(null)); + expect(commit.revert, equals(null)); + expect(commit.header, + equals('feat(scope): broadcast destroy event on scope destruction')); + expect(commit.type, equals('feat')); + expect(commit.scope, equals('scope')); + expect(commit.subject, + equals('broadcast destroy event on scope destruction')); + expect( + commit.body, + equals( + 'perf testing shows that in chrome this change adds 5-15% overhead\nwhen destroying 10k nested scopes where each scope has a destroy listener')); + expect(commit.footer, + equals('BREAKING CHANGE: some breaking change\nfixes #1')); + expect(commit.mentions, equals(['example'])); + expect(commit.notes.single.title, equals('BREAKING CHANGE')); + expect(commit.notes.single.text, equals('some breaking change')); + expect( + ListEquality().equals(commit.references, [ + CommitReference( + action: 'fixes', + owner: null, + repository: null, + issue: '1', + raw: '#1', + prefix: '#', + ), + ]), + true); + }); + + test('should truncate from scissors line', () { + final commit = parse('this is some header before a scissors-line\n' + + '# ------------------------ >8 ------------------------\n' + + 'this is a line that should be truncated\n'); + expect(commit.body, equals(null)); + }); + + test('should keep header before scissor line', () { + final commit = parse('this is some header before a scissors-line\n' + + '# ------------------------ >8 ------------------------\n' + + 'this is a line that should be truncated\n'); + expect( + commit.header, equals('this is some header before a scissors-line')); + }); + + test('should keep body before scissor line', () { + final commit = parse('this is some header before a scissors-line\n' + + 'this is some body before a scissors-line\n' + + '# ------------------------ >8 ------------------------\n' + + 'this is a line that should be truncated\n'); + expect(commit.body, equals('this is some body before a scissors-line')); + }); + + group('merge commits', () { + final githubCommit = parse( + 'Merge pull request #1 from user/feature/feature-name\n' + + '\n' + + 'feat(scope): broadcast destroy event on scope destruction\n' + + '\n' + + 'perf testing shows that in chrome this change adds 5-15% overhead\n' + + 'when destroying 10k nested scopes where each scope has a destroy listener'); + + test('should parse header in GitHub like pull request', () { + expect( + githubCommit.header, + equals( + 'feat(scope): broadcast destroy event on scope destruction')); + }); + + test('should understand header parts in GitHub like pull request', () { + expect(githubCommit.type, equals('feat')); + expect(githubCommit.scope, equals('scope')); + expect(githubCommit.subject, + equals('broadcast destroy event on scope destruction')); + }); + + test('should understand merge parts in GitHub like pull request', () { + expect(githubCommit.merge, + equals('Merge pull request #1 from user/feature/feature-name')); + }); + + final gitlabCommit = parse( + 'Merge branch \'feature/feature-name\' into \'master\'\r\n' + + '\r\n' + + 'feat(scope): broadcast destroy event on scope destruction\r\n' + + '\r\n' + + 'perf testing shows that in chrome this change adds 5-15% overhead\r\n' + + 'when destroying 10k nested scopes where each scope has a destroy listener\r\n' + + '\r\n' + + 'See merge request !1'); + test('should parse header in GitLab like merge request', () { + expect( + gitlabCommit.header, + equals( + 'feat(scope): broadcast destroy event on scope destruction')); + }); + + test('should understand header parts in GitLab like merge request', () { + expect(gitlabCommit.type, equals('feat')); + expect(gitlabCommit.scope, equals('scope')); + expect(gitlabCommit.subject, + equals('broadcast destroy event on scope destruction')); + }); + + test('should understand merge parts in GitLab like merge request', () { + expect(gitlabCommit.merge, + equals('Merge branch \'feature/feature-name\' into \'master\'')); + }); + + test('does not throw if merge commit has no header', () { + parse('Merge branch \'feature\''); + }); + }); }); }