|
6 | 6 | * found in the LICENSE file at https://angular.io/license
|
7 | 7 | */
|
8 | 8 |
|
| 9 | +import {Commit as ParsedCommit, Options, sync as parse} from 'conventional-commits-parser'; |
| 10 | + |
9 | 11 | import {exec} from '../utils/shelljs';
|
10 | 12 |
|
11 |
| -/** A parsed commit message. */ |
12 |
| -export interface ParsedCommitMessage { |
| 13 | + |
| 14 | +/** A parsed commit, containing the information needed to validate the commit. */ |
| 15 | +export interface Commit { |
| 16 | + /** The full raw text of the commit. */ |
| 17 | + fullText: string; |
| 18 | + /** The header line of the commit, will be used in the changelog entries. */ |
13 | 19 | header: string;
|
| 20 | + /** The full body of the commit, not including the footer. */ |
14 | 21 | body: string;
|
15 |
| - bodyWithoutLinking: string; |
| 22 | + /** The footer of the commit, containing issue references and note sections. */ |
| 23 | + footer: string; |
| 24 | + /** A list of the references to other issues made throughout the commit message. */ |
| 25 | + references: ParsedCommit.Reference[]; |
| 26 | + /** The type of the commit message. */ |
16 | 27 | type: string;
|
| 28 | + /** The scope of the commit message. */ |
17 | 29 | scope: string;
|
| 30 | + /** The npm scope of the commit message. */ |
| 31 | + npmScope: string; |
| 32 | + /** The subject of the commit message. */ |
18 | 33 | subject: string;
|
| 34 | + /** A list of breaking change notes in the commit message. */ |
| 35 | + breakingChanges: ParsedCommit.Note[]; |
| 36 | + /** A list of deprecation notes in the commit message. */ |
| 37 | + deprecations: ParsedCommit.Note[]; |
| 38 | + /** Whether the commit is a fixup commit. */ |
19 | 39 | isFixup: boolean;
|
| 40 | + /** Whether the commit is a squash commit. */ |
20 | 41 | isSquash: boolean;
|
| 42 | + /** Whether the commit is a revert commit. */ |
21 | 43 | isRevert: boolean;
|
22 | 44 | }
|
23 | 45 |
|
| 46 | +/** Markers used to denote the start of a note section in a commit. */ |
| 47 | +enum NoteSections { |
| 48 | + BREAKING_CHANGE = 'BREAKING CHANGE', |
| 49 | + DEPRECATED = 'DEPRECATED', |
| 50 | +} |
24 | 51 | /** Regex determining if a commit is a fixup. */
|
25 | 52 | const FIXUP_PREFIX_RE = /^fixup! /i;
|
26 |
| -/** Regex finding all github keyword links. */ |
27 |
| -const GITHUB_LINKING_RE = /((closed?s?)|(fix(es)?(ed)?)|(resolved?s?))\s\#(\d+)/ig; |
28 | 53 | /** Regex determining if a commit is a squash. */
|
29 | 54 | const SQUASH_PREFIX_RE = /^squash! /i;
|
30 | 55 | /** Regex determining if a commit is a revert. */
|
31 | 56 | const REVERT_PREFIX_RE = /^revert:? /i;
|
32 |
| -/** Regex determining the scope of a commit if provided. */ |
33 |
| -const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/; |
34 |
| -/** Regex determining the entire header line of the commit. */ |
35 |
| -const COMMIT_HEADER_RE = /^(.*)/i; |
36 |
| -/** Regex determining the body of the commit. */ |
37 |
| -const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/; |
| 57 | +/** |
| 58 | + * Regex pattern for parsing the header line of a commit. |
| 59 | + * |
| 60 | + * Several groups are being matched to be used in the parsed commit object, being mapped to the |
| 61 | + * `headerCorrespondence` object. |
| 62 | + * |
| 63 | + * The pattern can be broken down into component parts: |
| 64 | + * - `(\w+)` - a capturing group discovering the type of the commit. |
| 65 | + * - `(?:\((?:([^/]+)\/)?([^)]+)\))?` - a pair of capturing groups to capture the scope and, |
| 66 | + * optionally the npmScope of the commit. |
| 67 | + * - `(.*)` - a capturing group discovering the subject of the commit. |
| 68 | + */ |
| 69 | +const headerPattern = /^(\w+)(?:\((?:([^/]+)\/)?([^)]+)\))?: (.*)$/; |
| 70 | +/** |
| 71 | + * The property names used for the values extracted from the header via the `headerPattern` regex. |
| 72 | + */ |
| 73 | +const headerCorrespondence = ['type', 'npmScope', 'scope', 'subject']; |
| 74 | +/** |
| 75 | + * Configuration options for the commit parser. |
| 76 | + * |
| 77 | + * NOTE: An extended type from `Options` must be used because the current |
| 78 | + * @types/conventional-commits-parser version does not include the `notesPattern` field. |
| 79 | + */ |
| 80 | +const parseOptions: Options&{notesPattern: (keywords: string) => RegExp} = { |
| 81 | + commentChar: '#', |
| 82 | + headerPattern, |
| 83 | + headerCorrespondence, |
| 84 | + noteKeywords: [NoteSections.BREAKING_CHANGE, NoteSections.DEPRECATED], |
| 85 | + notesPattern: (keywords: string) => new RegExp(`(${keywords})(?:: ?)(.*)`), |
| 86 | +}; |
38 | 87 |
|
39 |
| -/** Parse a full commit message into its composite parts. */ |
40 |
| -export function parseCommitMessage(commitMsg: string): ParsedCommitMessage { |
41 |
| - // Ignore comments (i.e. lines starting with `#`). Comments are automatically removed by git and |
42 |
| - // should not be considered part of the final commit message. |
43 |
| - commitMsg = commitMsg.split('\n').filter(line => !line.startsWith('#')).join('\n'); |
44 | 88 |
|
45 |
| - let header = ''; |
46 |
| - let body = ''; |
47 |
| - let bodyWithoutLinking = ''; |
48 |
| - let type = ''; |
49 |
| - let scope = ''; |
50 |
| - let subject = ''; |
| 89 | +/** Parse a full commit message into its composite parts. */ |
| 90 | +export function parseCommitMessage(fullText: string): Commit { |
| 91 | + /** The commit message text with the fixup and squash markers stripped out. */ |
| 92 | + const strippedCommitMsg = fullText.replace(FIXUP_PREFIX_RE, '') |
| 93 | + .replace(SQUASH_PREFIX_RE, '') |
| 94 | + .replace(REVERT_PREFIX_RE, ''); |
| 95 | + /** The initially parsed commit. */ |
| 96 | + const commit = parse(strippedCommitMsg, parseOptions); |
| 97 | + /** A list of breaking change notes from the commit. */ |
| 98 | + const breakingChanges: ParsedCommit.Note[] = []; |
| 99 | + /** A list of deprecation notes from the commit. */ |
| 100 | + const deprecations: ParsedCommit.Note[] = []; |
51 | 101 |
|
52 |
| - if (COMMIT_HEADER_RE.test(commitMsg)) { |
53 |
| - header = COMMIT_HEADER_RE.exec(commitMsg)![1] |
54 |
| - .replace(FIXUP_PREFIX_RE, '') |
55 |
| - .replace(SQUASH_PREFIX_RE, ''); |
56 |
| - } |
57 |
| - if (COMMIT_BODY_RE.test(commitMsg)) { |
58 |
| - body = COMMIT_BODY_RE.exec(commitMsg)![1]; |
59 |
| - bodyWithoutLinking = body.replace(GITHUB_LINKING_RE, ''); |
60 |
| - } |
| 102 | + // Extract the commit message notes by marked types into their respective lists. |
| 103 | + commit.notes.forEach((note: ParsedCommit.Note) => { |
| 104 | + if (note.title === NoteSections.BREAKING_CHANGE) { |
| 105 | + return breakingChanges.push(note); |
| 106 | + } |
| 107 | + if (note.title === NoteSections.DEPRECATED) { |
| 108 | + return deprecations.push(note); |
| 109 | + } |
| 110 | + }); |
61 | 111 |
|
62 |
| - if (TYPE_SCOPE_RE.test(header)) { |
63 |
| - const parsedCommitHeader = TYPE_SCOPE_RE.exec(header)!; |
64 |
| - type = parsedCommitHeader[1]; |
65 |
| - scope = parsedCommitHeader[2]; |
66 |
| - subject = parsedCommitHeader[3]; |
67 |
| - } |
68 | 112 | return {
|
69 |
| - header, |
70 |
| - body, |
71 |
| - bodyWithoutLinking, |
72 |
| - type, |
73 |
| - scope, |
74 |
| - subject, |
75 |
| - isFixup: FIXUP_PREFIX_RE.test(commitMsg), |
76 |
| - isSquash: SQUASH_PREFIX_RE.test(commitMsg), |
77 |
| - isRevert: REVERT_PREFIX_RE.test(commitMsg), |
| 113 | + fullText, |
| 114 | + breakingChanges, |
| 115 | + deprecations, |
| 116 | + body: commit.body || '', |
| 117 | + footer: commit.footer || '', |
| 118 | + header: commit.header || '', |
| 119 | + references: commit.references, |
| 120 | + scope: commit.scope || '', |
| 121 | + subject: commit.subject || '', |
| 122 | + type: commit.type || '', |
| 123 | + npmScope: commit.npmScope || '', |
| 124 | + isFixup: FIXUP_PREFIX_RE.test(fullText), |
| 125 | + isSquash: SQUASH_PREFIX_RE.test(fullText), |
| 126 | + isRevert: REVERT_PREFIX_RE.test(fullText), |
78 | 127 | };
|
79 | 128 | }
|
80 | 129 |
|
81 | 130 | /** Retrieve and parse each commit message in a provide range. */
|
82 |
| -export function parseCommitMessagesForRange(range: string): ParsedCommitMessage[] { |
| 131 | +export function parseCommitMessagesForRange(range: string): Commit[] { |
83 | 132 | /** A random number used as a split point in the git log result. */
|
84 | 133 | const randomValueSeparator = `${Math.random()}`;
|
85 | 134 | /**
|
|
0 commit comments