| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							|  |  |  |  * Copyright Google Inc. 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 {getAngularDevConfig} from '../utils/config'; | 
					
						
							|  |  |  | import {CommitMessageConfig} from './config'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-20 12:24:12 -07:00
										 |  |  | /** Options for commit message validation. */ | 
					
						
							|  |  |  | export interface ValidateCommitMessageOptions { | 
					
						
							|  |  |  |   disallowSquash?: boolean; | 
					
						
							|  |  |  |   nonFixupCommitHeaders?: string[]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 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; | 
					
						
							| 
									
										
										
										
											2020-04-14 15:25:05 -07:00
										 |  |  | const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/; | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** 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)) { | 
					
						
							| 
									
										
										
										
											2020-03-20 12:24:12 -07:00
										 |  |  |     header = COMMIT_HEADER_RE.exec(commitMsg)![1] | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |                  .replace(FIXUP_PREFIX_RE, '') | 
					
						
							|  |  |  |                  .replace(SQUASH_PREFIX_RE, ''); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (COMMIT_BODY_RE.test(commitMsg)) { | 
					
						
							| 
									
										
										
										
											2020-03-20 12:24:12 -07:00
										 |  |  |     body = COMMIT_BODY_RE.exec(commitMsg)![1]; | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |     bodyWithoutLinking = body.replace(GITHUB_LINKING_RE, ''); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (TYPE_SCOPE_RE.test(header)) { | 
					
						
							| 
									
										
										
										
											2020-03-20 12:24:12 -07:00
										 |  |  |     const parsedCommitHeader = TYPE_SCOPE_RE.exec(header)!; | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |     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( | 
					
						
							| 
									
										
										
										
											2020-03-20 12:24:12 -07:00
										 |  |  |     commitMsg: string, options: ValidateCommitMessageOptions = {}) { | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |   function error(errorMessage: string) { | 
					
						
							|  |  |  |     console.error( | 
					
						
							|  |  |  |         `INVALID COMMIT MSG: \n` + | 
					
						
							|  |  |  |         `${'─'.repeat(40)}\n` + | 
					
						
							|  |  |  |         `${commitMsg}\n` + | 
					
						
							|  |  |  |         `${'─'.repeat(40)}\n` + | 
					
						
							|  |  |  |         `ERROR: \n` + | 
					
						
							|  |  |  |         `  ${errorMessage}` + | 
					
						
							|  |  |  |         `\n\n` + | 
					
						
							|  |  |  |         `The expected format for a commit is: \n` + | 
					
						
							|  |  |  |         `<type>(<scope>): <subject>\n\n<body>`); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const config = getAngularDevConfig<'commitMessage', CommitMessageConfig>().commitMessage; | 
					
						
							|  |  |  |   const commit = parseCommitMessage(commitMsg); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 15:25:05 -07:00
										 |  |  |   ////////////////////////////////////
 | 
					
						
							|  |  |  |   // Checking revert, squash, fixup //
 | 
					
						
							|  |  |  |   ////////////////////////////////////
 | 
					
						
							| 
									
										
										
										
											2020-04-06 13:18:59 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // All revert commits are considered valid.
 | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |   if (commit.isRevert) { | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-06 13:18:59 -07:00
										 |  |  |   // All squashes are considered valid, as the commit will be squashed into another in
 | 
					
						
							|  |  |  |   // the git history anyway, unless the options provided to not allow squash commits.
 | 
					
						
							|  |  |  |   if (commit.isSquash) { | 
					
						
							|  |  |  |     if (options.disallowSquash) { | 
					
						
							|  |  |  |       error('The commit must be manually squashed into the target commit'); | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return true; | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-06 13:18:59 -07:00
										 |  |  |   // Fixups commits are considered valid, unless nonFixupCommitHeaders are provided to check
 | 
					
						
							|  |  |  |   // against. If `nonFixupCommitHeaders` is not empty, we check whether there is a corresponding
 | 
					
						
							|  |  |  |   // non-fixup commit (i.e. a commit whose header is identical to this commit's header after
 | 
					
						
							|  |  |  |   // stripping the `fixup! ` prefix), otherwise we assume this verification will happen in another
 | 
					
						
							|  |  |  |   // check.
 | 
					
						
							|  |  |  |   if (commit.isFixup) { | 
					
						
							|  |  |  |     if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) { | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |       error( | 
					
						
							|  |  |  |           'Unable to find match for fixup commit among prior commits: ' + | 
					
						
							| 
									
										
										
										
											2020-03-20 12:24:12 -07:00
										 |  |  |           (options.nonFixupCommitHeaders.map(x => `\n      ${x}`).join('') || '-')); | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 15:25:05 -07:00
										 |  |  |   ////////////////////////////
 | 
					
						
							|  |  |  |   // Checking commit header //
 | 
					
						
							|  |  |  |   ////////////////////////////
 | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |   if (commit.header.length > config.maxLineLength) { | 
					
						
							|  |  |  |     error(`The commit message header is longer than ${config.maxLineLength} characters`); | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!commit.type) { | 
					
						
							|  |  |  |     error(`The commit message header does not match the expected format.`); | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!config.types.includes(commit.type)) { | 
					
						
							|  |  |  |     error(`'${commit.type}' is not an allowed type.\n => TYPES: ${config.types.join(', ')}`); | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (commit.scope && !config.scopes.includes(commit.scope)) { | 
					
						
							|  |  |  |     error(`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`); | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 15:25:05 -07:00
										 |  |  |   //////////////////////////
 | 
					
						
							|  |  |  |   // Checking commit body //
 | 
					
						
							|  |  |  |   //////////////////////////
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |   if (commit.bodyWithoutLinking.trim().length < config.minBodyLength) { | 
					
						
							| 
									
										
										
										
											2020-03-20 12:24:12 -07:00
										 |  |  |     error(`The commit message body does not meet the minimum length of ${ | 
					
						
							|  |  |  |         config.minBodyLength} characters`);
 | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const bodyByLine = commit.body.split('\n'); | 
					
						
							|  |  |  |   if (bodyByLine.some(line => line.length > config.maxLineLength)) { | 
					
						
							|  |  |  |     error( | 
					
						
							|  |  |  |         `The commit messsage body contains lines greater than ${config.maxLineLength} characters`); | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return true; | 
					
						
							|  |  |  | } |