/** * @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 {Commit as ParsedCommit, Options, sync as parse} from 'conventional-commits-parser'; /** A parsed commit, containing the information needed to validate the commit. */ export interface Commit { /** The full raw text of the commit. */ fullText: string; /** The header line of the commit, will be used in the changelog entries. */ header: string; /** The full body of the commit, not including the footer. */ body: string; /** The footer of the commit, containing issue references and note sections. */ footer: string; /** A list of the references to other issues made throughout the commit message. */ references: ParsedCommit.Reference[]; /** The type of the commit message. */ type: string; /** The scope of the commit message. */ scope: string; /** The npm scope of the commit message. */ npmScope: string; /** The subject of the commit message. */ subject: string; /** A list of breaking change notes in the commit message. */ breakingChanges: ParsedCommit.Note[]; /** A list of deprecation notes in the commit message. */ deprecations: ParsedCommit.Note[]; /** Whether the commit is a fixup commit. */ isFixup: boolean; /** Whether the commit is a squash commit. */ isSquash: boolean; /** Whether the commit is a revert commit. */ isRevert: boolean; } /** A parsed commit which originated from a Git Log entry */ export interface CommitFromGitLog extends Commit { author: string; hash: string; shortHash: string; } /** * A list of tuples expressing the fields to extract from each commit log entry. The tuple contains * two values, the first is the key for the property and the second is the template shortcut for the * git log command. */ const commitFields = { hash: '%H', shortHash: '%h', author: '%aN', }; /** The additional fields to be included in commit log entries for parsing. */ export type CommitFields = typeof commitFields; /** The commit fields described as git log format entries for parsing. */ export const commitFieldsAsFormat = (fields: CommitFields) => { return Object.entries(fields).map(([key, value]) => `%n-${key}-%n${value}`).join(''); }; /** * The git log format template to create git log entries for parsing. * * The conventional commits parser expects to parse the standard git log raw body (%B) into its * component parts. Additionally it will parse additional fields with keys defined by * `-{key name}-` separated by new lines. * */ export const gitLogFormatForParsing = `%B${commitFieldsAsFormat(commitFields)}`; /** Markers used to denote the start of a note section in a commit. */ enum NoteSections { BREAKING_CHANGE = 'BREAKING CHANGE', DEPRECATED = 'DEPRECATED', } /** Regex determining if a commit is a fixup. */ const FIXUP_PREFIX_RE = /^fixup! /i; /** 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 pattern for parsing the header line of a commit. * * Several groups are being matched to be used in the parsed commit object, being mapped to the * `headerCorrespondence` object. * * The pattern can be broken down into component parts: * - `(\w+)` - a capturing group discovering the type of the commit. * - `(?:\((?:([^/]+)\/)?([^)]+)\))?` - a pair of capturing groups to capture the scope and, * optionally the npmScope of the commit. * - `(.*)` - a capturing group discovering the subject of the commit. */ const headerPattern = /^(\w+)(?:\((?:([^/]+)\/)?([^)]+)\))?: (.*)$/; /** * The property names used for the values extracted from the header via the `headerPattern` regex. */ const headerCorrespondence = ['type', 'npmScope', 'scope', 'subject']; /** * Configuration options for the commit parser. * * NOTE: An extended type from `Options` must be used because the current * @types/conventional-commits-parser version does not include the `notesPattern` field. */ const parseOptions: Options&{notesPattern: (keywords: string) => RegExp} = { commentChar: '#', headerPattern, headerCorrespondence, noteKeywords: [NoteSections.BREAKING_CHANGE, NoteSections.DEPRECATED], notesPattern: (keywords: string) => new RegExp(`(${keywords}): ?(.*)`), }; /** Parse a commit message into its composite parts. */ export const parseCommitMessage: (fullText: string) => Commit = parseInternal; /** Parse a commit message from a git log entry into its composite parts. */ export const parseCommitFromGitLog: (fullText: Buffer) => CommitFromGitLog = parseInternal; /** Parse a full commit message into its composite parts. */ function parseInternal(fullText: string): Commit; function parseInternal(fullText: Buffer): CommitFromGitLog; function parseInternal(fullText: string|Buffer): CommitFromGitLog|Commit { // Ensure the fullText symbol is a `string`, even if a Buffer was provided. fullText = fullText.toString(); /** The commit message text with the fixup and squash markers stripped out. */ const strippedCommitMsg = fullText.replace(FIXUP_PREFIX_RE, '') .replace(SQUASH_PREFIX_RE, '') .replace(REVERT_PREFIX_RE, ''); /** The initially parsed commit. */ const commit = parse(strippedCommitMsg, parseOptions); /** A list of breaking change notes from the commit. */ const breakingChanges: ParsedCommit.Note[] = []; /** A list of deprecation notes from the commit. */ const deprecations: ParsedCommit.Note[] = []; // Extract the commit message notes by marked types into their respective lists. commit.notes.forEach((note: ParsedCommit.Note) => { if (note.title === NoteSections.BREAKING_CHANGE) { return breakingChanges.push(note); } if (note.title === NoteSections.DEPRECATED) { return deprecations.push(note); } }); return { fullText, breakingChanges, deprecations, body: commit.body || '', footer: commit.footer || '', header: commit.header || '', references: commit.references, scope: commit.scope || '', subject: commit.subject || '', type: commit.type || '', npmScope: commit.npmScope || '', isFixup: FIXUP_PREFIX_RE.test(fullText), isSquash: SQUASH_PREFIX_RE.test(fullText), isRevert: REVERT_PREFIX_RE.test(fullText), author: commit.author || undefined, hash: commit.hash || undefined, shortHash: commit.shortHash || undefined, }; }