| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							| 
									
										
										
										
											2020-05-19 12:08:49 -07:00
										 |  |  |  * Copyright Google LLC All Rights Reserved. | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |  * | 
					
						
							|  |  |  |  * 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
 | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2020-05-20 14:22:29 -07:00
										 |  |  | import {error} from '../utils/console'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-12 09:36:59 -07:00
										 |  |  | import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config'; | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  | import {parseCommitMessage, ParsedCommitMessage} from './parse'; | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-20 12:24:12 -07:00
										 |  |  | /** Options for commit message validation. */ | 
					
						
							|  |  |  | export interface ValidateCommitMessageOptions { | 
					
						
							|  |  |  |   disallowSquash?: boolean; | 
					
						
							|  |  |  |   nonFixupCommitHeaders?: string[]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  | /** The result of a commit message validation check. */ | 
					
						
							|  |  |  | export interface ValidateCommitMessageResult { | 
					
						
							|  |  |  |   valid: boolean; | 
					
						
							|  |  |  |   errors: string[]; | 
					
						
							|  |  |  |   commit: ParsedCommitMessage; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-12 09:40:37 -07:00
										 |  |  | /** Regex matching a URL for an entire commit body line. */ | 
					
						
							| 
									
										
										
										
											2020-07-07 15:58:11 +02:00
										 |  |  | const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/; | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** Validate a commit message against using the local repo's config. */ | 
					
						
							|  |  |  | export function validateCommitMessage( | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     commitMsg: string, options: ValidateCommitMessageOptions = {}): ValidateCommitMessageResult { | 
					
						
							| 
									
										
										
										
											2020-05-08 14:51:29 -07:00
										 |  |  |   const config = getCommitMessageConfig().commitMessage; | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |   const commit = parseCommitMessage(commitMsg); | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |   const errors: string[] = []; | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |   /** Perform the validation checks against the parsed commit. */ | 
					
						
							|  |  |  |   function validateCommitAndCollectErrors() { | 
					
						
							|  |  |  |     // TODO(josephperrott): Remove early return calls when commit message errors are found
 | 
					
						
							| 
									
										
										
										
											2020-04-06 13:18:59 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     ////////////////////////////////////
 | 
					
						
							|  |  |  |     // Checking revert, squash, fixup //
 | 
					
						
							|  |  |  |     ////////////////////////////////////
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // All revert commits are considered valid.
 | 
					
						
							|  |  |  |     if (commit.isRevert) { | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // 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) { | 
					
						
							|  |  |  |         errors.push('The commit must be manually squashed into the target commit'); | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -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)) { | 
					
						
							|  |  |  |         errors.push( | 
					
						
							|  |  |  |             'Unable to find match for fixup commit among prior commits: ' + | 
					
						
							|  |  |  |             (options.nonFixupCommitHeaders.map(x => `\n      ${x}`).join('') || '-')); | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ////////////////////////////
 | 
					
						
							|  |  |  |     // Checking commit header //
 | 
					
						
							|  |  |  |     ////////////////////////////
 | 
					
						
							|  |  |  |     if (commit.header.length > config.maxLineLength) { | 
					
						
							|  |  |  |       errors.push(`The commit message header is longer than ${config.maxLineLength} characters`); | 
					
						
							| 
									
										
										
										
											2020-04-06 13:18:59 -07:00
										 |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     if (!commit.type) { | 
					
						
							|  |  |  |       errors.push(`The commit message header does not match the expected format.`); | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     if (COMMIT_TYPES[commit.type] === undefined) { | 
					
						
							|  |  |  |       errors.push(`'${commit.type}' is not an allowed type.\n => TYPES: ${ | 
					
						
							|  |  |  |           Object.keys(COMMIT_TYPES).join(', ')}`);
 | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     /** The scope requirement level for the provided type of the commit message. */ | 
					
						
							|  |  |  |     const scopeRequirementForType = COMMIT_TYPES[commit.type].scope; | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) { | 
					
						
							|  |  |  |       errors.push(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${ | 
					
						
							|  |  |  |           commit.scope}' was provided.`);
 | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) { | 
					
						
							|  |  |  |       errors.push( | 
					
						
							|  |  |  |           `Scopes are required for commits with type '${commit.type}', but no scope was provided.`); | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-08-12 09:36:59 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     if (commit.scope && !config.scopes.includes(commit.scope)) { | 
					
						
							|  |  |  |       errors.push( | 
					
						
							|  |  |  |           `'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`); | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-08-12 09:36:59 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     // Commits with the type of `release` do not require a commit body.
 | 
					
						
							|  |  |  |     if (commit.type === 'release') { | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-08-12 09:36:59 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     //////////////////////////
 | 
					
						
							|  |  |  |     // Checking commit body //
 | 
					
						
							|  |  |  |     //////////////////////////
 | 
					
						
							| 
									
										
										
										
											2020-08-12 09:36:59 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     if (!config.minBodyLengthTypeExcludes?.includes(commit.type) && | 
					
						
							|  |  |  |         commit.bodyWithoutLinking.trim().length < config.minBodyLength) { | 
					
						
							|  |  |  |       errors.push(`The commit message body does not meet the minimum length of ${ | 
					
						
							|  |  |  |           config.minBodyLength} characters`);
 | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-08-12 09:36:59 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     const bodyByLine = commit.body.split('\n'); | 
					
						
							|  |  |  |     const lineExceedsMaxLength = bodyByLine.some(line => { | 
					
						
							|  |  |  |       // Check if any line exceeds the max line length limit. The limit is ignored for
 | 
					
						
							|  |  |  |       // lines that just contain an URL (as these usually cannot be wrapped or shortened).
 | 
					
						
							|  |  |  |       return line.length > config.maxLineLength && !COMMIT_BODY_URL_LINE_RE.test(line); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |     if (lineExceedsMaxLength) { | 
					
						
							|  |  |  |       errors.push( | 
					
						
							|  |  |  |           `The commit message body contains lines greater than ${config.maxLineLength} characters`); | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-14 09:34:30 -07:00
										 |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  |   return {valid: validateCommitAndCollectErrors(), errors, commit}; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-03 14:54:31 -07:00
										 |  |  | /** Print the error messages from the commit message validation to the console. */ | 
					
						
							|  |  |  | export function printValidationErrors(errors: string[], print = error) { | 
					
						
							|  |  |  |   print.group(`Error${errors.length === 1 ? '' : 's'}:`); | 
					
						
							|  |  |  |   errors.forEach(line => print(line)); | 
					
						
							|  |  |  |   print.groupEnd(); | 
					
						
							|  |  |  |   print(); | 
					
						
							|  |  |  |   print('The expected format for a commit is: '); | 
					
						
							|  |  |  |   print('<type>(<scope>): <summary>'); | 
					
						
							|  |  |  |   print(); | 
					
						
							|  |  |  |   print('<body>'); | 
					
						
							|  |  |  |   print(); | 
					
						
							| 
									
										
										
										
											2020-03-10 10:29:44 -07:00
										 |  |  | } |