diff --git a/dev-infra/commit-message/BUILD.bazel b/dev-infra/commit-message/BUILD.bazel index 2c6d3be371..333c62ba99 100644 --- a/dev-infra/commit-message/BUILD.bazel +++ b/dev-infra/commit-message/BUILD.bazel @@ -6,6 +6,7 @@ ts_library( srcs = [ "cli.ts", "config.ts", + "parse.ts", "validate.ts", "validate-file.ts", "validate-range.ts", @@ -23,9 +24,12 @@ ts_library( ) ts_library( - name = "validate-test", + name = "test_lib", testonly = True, - srcs = ["validate.spec.ts"], + srcs = [ + "parse.spec.ts", + "validate.spec.ts", + ], deps = [ ":commit-message", "//dev-infra/utils", @@ -40,7 +44,6 @@ jasmine_node_test( name = "test", bootstrap = ["//tools/testing:node_no_angular_es5"], deps = [ - ":commit-message", - ":validate-test", + "test_lib", ], ) diff --git a/dev-infra/commit-message/parse.spec.ts b/dev-infra/commit-message/parse.spec.ts new file mode 100644 index 0000000000..71f4d6875b --- /dev/null +++ b/dev-infra/commit-message/parse.spec.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {parseCommitMessage, ParsedCommitMessage} from './parse'; + + +const commitValues = { + prefix: '', + type: 'fix', + scope: 'changed-area', + summary: 'This is a short summary of the change', + body: 'This is a longer description of the change Closes #1', +}; + +function buildCommitMessage(params = {}) { + const {prefix, type, scope, summary, body} = {...commitValues, ...params}; + return `${prefix}${type}${scope ? '(' + scope + ')' : ''}: ${summary}\n\n${body}`; +} + + +describe('commit message parsing:', () => { + it('parses the scope', () => { + const message = buildCommitMessage(); + expect(parseCommitMessage(message).scope).toBe(commitValues.scope); + }); + + it('parses the type', () => { + const message = buildCommitMessage(); + expect(parseCommitMessage(message).type).toBe(commitValues.type); + }); + + it('parses the header', () => { + const message = buildCommitMessage(); + expect(parseCommitMessage(message).header) + .toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`); + }); + + it('parses the body', () => { + const message = buildCommitMessage(); + expect(parseCommitMessage(message).body).toBe(commitValues.body); + }); + + it('parses the body without Github linking', () => { + const body = 'This has linking\nCloses #1'; + const message = buildCommitMessage({body}); + expect(parseCommitMessage(message).bodyWithoutLinking).toBe('This has linking\n'); + }); + + it('parses the subject', () => { + const message = buildCommitMessage(); + expect(parseCommitMessage(message).subject).toBe(commitValues.summary); + }); + + it('identifies if a commit is a fixup', () => { + const message1 = buildCommitMessage(); + expect(parseCommitMessage(message1).isFixup).toBe(false); + + const message2 = buildCommitMessage({prefix: 'fixup! '}); + expect(parseCommitMessage(message2).isFixup).toBe(true); + }); + + it('identifies if a commit is a revert', () => { + const message1 = buildCommitMessage(); + expect(parseCommitMessage(message1).isRevert).toBe(false); + + const message2 = buildCommitMessage({prefix: 'revert: '}); + expect(parseCommitMessage(message2).isRevert).toBe(true); + + const message3 = buildCommitMessage({prefix: 'revert '}); + expect(parseCommitMessage(message3).isRevert).toBe(true); + }); + + it('identifies if a commit is a squash', () => { + const message1 = buildCommitMessage(); + expect(parseCommitMessage(message1).isSquash).toBe(false); + + const message2 = buildCommitMessage({prefix: 'squash! '}); + expect(parseCommitMessage(message2).isSquash).toBe(true); + }); +}); diff --git a/dev-infra/commit-message/parse.ts b/dev-infra/commit-message/parse.ts new file mode 100644 index 0000000000..f8acd0fb60 --- /dev/null +++ b/dev-infra/commit-message/parse.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** A parsed commit message. */ +export interface ParsedCommitMessage { + header: string; + body: string; + bodyWithoutLinking: string; + type: string; + scope: string; + subject: string; + isFixup: boolean; + isSquash: boolean; + isRevert: boolean; +} + +/** Regex determining if a commit is a fixup. */ +const FIXUP_PREFIX_RE = /^fixup! /i; +/** Regex finding all github keyword links. */ +const GITHUB_LINKING_RE = /((closed?s?)|(fix(es)?(ed)?)|(resolved?s?))\s\#(\d+)/ig; +/** Regex determining if a commit is a squash. */ +const SQUASH_PREFIX_RE = /^squash! /i; +/** Regex determining if a commit is a revert. */ +const REVERT_PREFIX_RE = /^revert:? /i; +/** Regex determining the scope of a commit if provided. */ +const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/; +/** Regex determining the entire header line of the commit. */ +const COMMIT_HEADER_RE = /^(.*)/i; +/** Regex determining the body of the commit. */ +const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/; + +/** Parse a full commit message into its composite parts. */ +export function parseCommitMessage(commitMsg: string): ParsedCommitMessage { + let header = ''; + let body = ''; + let bodyWithoutLinking = ''; + let type = ''; + let scope = ''; + let subject = ''; + + if (COMMIT_HEADER_RE.test(commitMsg)) { + header = COMMIT_HEADER_RE.exec(commitMsg)![1] + .replace(FIXUP_PREFIX_RE, '') + .replace(SQUASH_PREFIX_RE, ''); + } + if (COMMIT_BODY_RE.test(commitMsg)) { + body = COMMIT_BODY_RE.exec(commitMsg)![1]; + bodyWithoutLinking = body.replace(GITHUB_LINKING_RE, ''); + } + + if (TYPE_SCOPE_RE.test(header)) { + const parsedCommitHeader = TYPE_SCOPE_RE.exec(header)!; + type = parsedCommitHeader[1]; + scope = parsedCommitHeader[2]; + subject = parsedCommitHeader[3]; + } + return { + header, + body, + bodyWithoutLinking, + type, + scope, + subject, + isFixup: FIXUP_PREFIX_RE.test(commitMsg), + isSquash: SQUASH_PREFIX_RE.test(commitMsg), + isRevert: REVERT_PREFIX_RE.test(commitMsg), + }; +} diff --git a/dev-infra/commit-message/validate-range.ts b/dev-infra/commit-message/validate-range.ts index 485fa06b2f..2b29cf8382 100644 --- a/dev-infra/commit-message/validate-range.ts +++ b/dev-infra/commit-message/validate-range.ts @@ -8,7 +8,8 @@ import {info} from '../utils/console'; import {exec} from '../utils/shelljs'; -import {parseCommitMessage, validateCommitMessage, ValidateCommitMessageOptions} from './validate'; +import {parseCommitMessage} from './parse'; +import {validateCommitMessage, ValidateCommitMessageOptions} from './validate'; // Whether the provided commit is a fixup commit. const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup; diff --git a/dev-infra/commit-message/validate.ts b/dev-infra/commit-message/validate.ts index 7108a63cac..fdc2f006c4 100644 --- a/dev-infra/commit-message/validate.ts +++ b/dev-infra/commit-message/validate.ts @@ -8,6 +8,7 @@ import {error} from '../utils/console'; import {getCommitMessageConfig} from './config'; +import {parseCommitMessage} from './parse'; /** Options for commit message validation. */ export interface ValidateCommitMessageOptions { @@ -15,53 +16,9 @@ export interface ValidateCommitMessageOptions { nonFixupCommitHeaders?: string[]; } -const FIXUP_PREFIX_RE = /^fixup! /i; -const GITHUB_LINKING_RE = /((closed?s?)|(fix(es)?(ed)?)|(resolved?s?))\s\#(\d+)/ig; -const SQUASH_PREFIX_RE = /^squash! /i; -const REVERT_PREFIX_RE = /^revert:? /i; -const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/; -const COMMIT_HEADER_RE = /^(.*)/i; -const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/; +/** Regex matching a URL for an entire commit body line. */ const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/; -/** Parse a full commit message into its composite parts. */ -export function parseCommitMessage(commitMsg: string) { - let header = ''; - let body = ''; - let bodyWithoutLinking = ''; - let type = ''; - let scope = ''; - let subject = ''; - - if (COMMIT_HEADER_RE.test(commitMsg)) { - header = COMMIT_HEADER_RE.exec(commitMsg)![1] - .replace(FIXUP_PREFIX_RE, '') - .replace(SQUASH_PREFIX_RE, ''); - } - if (COMMIT_BODY_RE.test(commitMsg)) { - body = COMMIT_BODY_RE.exec(commitMsg)![1]; - bodyWithoutLinking = body.replace(GITHUB_LINKING_RE, ''); - } - - if (TYPE_SCOPE_RE.test(header)) { - const parsedCommitHeader = TYPE_SCOPE_RE.exec(header)!; - type = parsedCommitHeader[1]; - scope = parsedCommitHeader[2]; - subject = parsedCommitHeader[3]; - } - return { - header, - body, - bodyWithoutLinking, - type, - scope, - subject, - isFixup: FIXUP_PREFIX_RE.test(commitMsg), - isSquash: SQUASH_PREFIX_RE.test(commitMsg), - isRevert: REVERT_PREFIX_RE.test(commitMsg), - }; -} - /** Validate a commit message against using the local repo's config. */ export function validateCommitMessage( commitMsg: string, options: ValidateCommitMessageOptions = {}) { diff --git a/dev-infra/pr/merge/strategies/api-merge.ts b/dev-infra/pr/merge/strategies/api-merge.ts index f52b768e00..42afdd1c6f 100644 --- a/dev-infra/pr/merge/strategies/api-merge.ts +++ b/dev-infra/pr/merge/strategies/api-merge.ts @@ -9,7 +9,7 @@ import {PullsListCommitsResponse, PullsMergeParams} from '@octokit/rest'; import {prompt} from 'inquirer'; -import {parseCommitMessage} from '../../../commit-message/validate'; +import {parseCommitMessage} from '../../../commit-message/parse'; import {GitClient} from '../../../utils/git'; import {GithubApiMergeMethod} from '../config'; import {PullRequestFailure} from '../failures';