refactor(dev-infra): update commit message validation to return validation result (#38703)

Previously, the validateCommitMessage function ran validation and logged the results.
The validateCommitMessage function now returns an object containing the validation
results and the cli action functions are instead responsible for logging the results.

This is being done as a prefactor for a change which allows for commit message
hook validation to be either a blocking error or a warning.

PR Close #38703
This commit is contained in:
Joey Perrott 2020-09-03 14:54:31 -07:00 committed by atscott
parent c6ebb77cec
commit f9421184ef
4 changed files with 268 additions and 232 deletions

View File

@ -9,19 +9,25 @@ import {readFileSync} from 'fs';
import {resolve} from 'path';
import {getRepoBaseDir} from '../utils/config';
import {info} from '../utils/console';
import {error, green, info, red} from '../utils/console';
import {deleteCommitMessageDraft, saveCommitMessageDraft} from './commit-message-draft';
import {validateCommitMessage} from './validate';
import {printValidationErrors, validateCommitMessage} from './validate';
/** Validate commit message at the provided file path. */
export function validateFile(filePath: string) {
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
if (validateCommitMessage(commitMessage)) {
info('√ Valid commit message');
const {valid, errors} = validateCommitMessage(commitMessage);
if (valid) {
info(`${green('√')} Valid commit message`);
deleteCommitMessageDraft(filePath);
return;
}
error(`${red('✘')} Invalid commit message`);
printValidationErrors(errors);
error('Aborting commit attempt due to invalid commit message.');
// On all invalid commit messages, the commit message should be saved as a draft to be
// restored on the next commit attempt.
saveCommitMessageDraft(filePath, commitMessage);

View File

@ -5,11 +5,11 @@
* 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 {info} from '../utils/console';
import {error, info} from '../utils/console';
import {exec} from '../utils/shelljs';
import {parseCommitMessage} from './parse';
import {validateCommitMessage, ValidateCommitMessageOptions} from './validate';
import {printValidationErrors, validateCommitMessage, ValidateCommitMessageOptions} from './validate';
// Whether the provided commit is a fixup commit.
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
@ -19,11 +19,20 @@ const extractCommitHeader = (m: string) => parseCommitMessage(m).header;
/** Validate all commits in a provided git commit range. */
export function validateCommitRange(range: string) {
// A random value is used as a string to allow for a definite split point in the git log result.
/**
* A random value is used as a string to allow for a definite split point in the git log result.
*/
const randomValueSeparator = `${Math.random()}`;
// Custom git log format that provides the commit header and body, separated as expected with
// the custom separator as the trailing value.
/**
* Custom git log format that provides the commit header and body, separated as expected with the
* custom separator as the trailing value.
*/
const gitLogFormat = `%s%n%n%b${randomValueSeparator}`;
/**
* A list of tuples containing a commit header string and the list of error messages for the
* commit.
*/
const errors: [commitHeader: string, errors: string[]][] = [];
// Retrieve the commits in the provided range.
const result = exec(`git log --reverse --format=${gitLogFormat} ${range}`);
@ -45,12 +54,22 @@ export function validateCommitRange(range: string) {
undefined :
commits.slice(0, i).filter(isNonFixup).map(extractCommitHeader)
};
return validateCommitMessage(m, options);
const {valid, errors: localErrors, commit} = validateCommitMessage(m, options);
if (localErrors.length) {
errors.push([commit.header, localErrors]);
}
return valid;
});
if (allCommitsInRangeValid) {
info('√ All commit messages in range valid.');
} else {
error('✘ Invalid commit message');
errors.forEach(([header, validationErrors]) => {
error.group(header);
printValidationErrors(validationErrors);
error.groupEnd();
});
// Exit with a non-zero exit code if invalid commit messages have
// been discovered.
process.exit(1);

View File

@ -8,7 +8,7 @@
// Imports
import * as validateConfig from './config';
import {validateCommitMessage} from './validate';
import {validateCommitMessage, ValidateCommitMessageResult} from './validate';
type CommitMessageConfig = validateConfig.CommitMessageConfig;
@ -31,44 +31,35 @@ const SCOPES = config.commitMessage.scopes.join(', ');
const INVALID = false;
const VALID = true;
function expectValidationResult(
validationResult: ValidateCommitMessageResult, valid: boolean, errors: string[] = []) {
expect(validationResult).toEqual(jasmine.objectContaining({valid, errors}));
}
// TODO(josephperrott): Clean up tests to test script rather than for
// specific commit messages we want to use.
describe('validate-commit-message.js', () => {
let lastError: string = '';
beforeEach(() => {
lastError = '';
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
spyOn(validateConfig, 'getCommitMessageConfig')
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
});
describe('validateMessage()', () => {
it('should be valid', () => {
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('fixup! fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('squash! fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('Revert: "fix(packaging): something"')).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage('feat(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('fixup! fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('squash! fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('Revert: "fix(packaging): something"'), VALID);
});
it('should validate max length', () => {
const msg =
'fix(compiler): something super mega extra giga tera long, maybe even longer and longer and longer and longer and longer and longer...';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`The commit message header is longer than ${
config.commitMessage.maxLineLength} characters`);
expectValidationResult(validateCommitMessage(msg), INVALID, [
`The commit message header is longer than ${config.commitMessage.maxLineLength} characters`
]);
});
it('should skip max length limit for URLs', () => {
@ -77,49 +68,56 @@ describe('validate-commit-message.js', () => {
'limit. For more details see the following super long URL:\n\n' +
'https://github.com/angular/components/commit/e2ace018ddfad10608e0e32932c43dcfef4095d7#diff-9879d6db96fd29134fc802214163b95a';
expect(validateCommitMessage(msg)).toBe(VALID);
expectValidationResult(validateCommitMessage(msg), VALID);
});
it('should validate "<type>(<scope>): <subject>" format', () => {
const msg = 'not correct format';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`The commit message header does not match the expected format.`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`The commit message header does not match the expected format.`]);
});
it('should fail when type is invalid', () => {
const msg = 'weird(core): something';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`'weird' is not an allowed type.\n => TYPES: ${TYPES}`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'weird' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
it('should fail when scope is invalid', () => {
const errorMessageFor = (scope: string, header: string) =>
`'${scope}' is not an allowed scope.\n => SCOPES: ${SCOPES}`;
expect(validateCommitMessage('fix(Compiler): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('Compiler', 'fix(Compiler): something'));
expectValidationResult(
validateCommitMessage('fix(Compiler): something'), INVALID,
[errorMessageFor('Compiler', 'fix(Compiler): something')]);
expect(validateCommitMessage('feat(bah): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('bah', 'feat(bah): something'));
expectValidationResult(
validateCommitMessage('feat(bah): something'), INVALID,
[errorMessageFor('bah', 'feat(bah): something')]);
expect(validateCommitMessage('fix(webworker): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('webworker', 'fix(webworker): something'));
expectValidationResult(
validateCommitMessage('fix(webworker): something'), INVALID,
[errorMessageFor('webworker', 'fix(webworker): something')]);
expect(validateCommitMessage('refactor(security): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
expectValidationResult(
validateCommitMessage('refactor(security): something'), INVALID,
[errorMessageFor('security', 'refactor(security): something')]);
expect(validateCommitMessage('refactor(docs): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('docs', 'refactor(docs): something'));
expectValidationResult(
validateCommitMessage('refactor(docs): something'), INVALID,
[errorMessageFor('docs', 'refactor(docs): something')]);
expect(validateCommitMessage('feat(angular): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('angular', 'feat(angular): something'));
expectValidationResult(
validateCommitMessage('feat(angular): something'), INVALID,
[errorMessageFor('angular', 'feat(angular): something')]);
});
it('should allow empty scope', () => {
expect(validateCommitMessage('build: blablabla')).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage('build: blablabla'), VALID);
});
// We do not want to allow WIP. It is OK to fail the PR build in this case to show that there is
@ -127,30 +125,25 @@ describe('validate-commit-message.js', () => {
it('should not allow "WIP: ..." syntax', () => {
const msg = 'WIP: fix: something';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
describe('(revert)', () => {
it('should allow valid "revert: ..." syntaxes', () => {
expect(validateCommitMessage('revert: anything')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('Revert: "anything"')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('revert anything')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('rEvErT anything')).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage('revert: anything'), VALID);
expectValidationResult(validateCommitMessage('Revert: "anything"'), VALID);
expectValidationResult(validateCommitMessage('revert anything'), VALID);
expectValidationResult(validateCommitMessage('rEvErT anything'), VALID);
});
it('should not allow "revert(scope): ..." syntax', () => {
const msg = 'revert(compiler): reduce generated code payload size by 65%';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`'revert' is not an allowed type.\n => TYPES: ${TYPES}`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'revert' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
// https://github.com/angular/angular/issues/23479
@ -158,28 +151,26 @@ describe('validate-commit-message.js', () => {
const msg =
'Revert "fix(compiler): Pretty print object instead of [Object object] (#22689)" (#23442)';
expect(validateCommitMessage(msg)).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage(msg), VALID);
});
});
describe('(squash)', () => {
describe('without `disallowSquash`', () => {
it('should return commits as valid', () => {
expect(validateCommitMessage('squash! feat(core): add feature')).toBe(VALID);
expect(validateCommitMessage('squash! fix: a bug')).toBe(VALID);
expect(validateCommitMessage('squash! fix a typo')).toBe(VALID);
expectValidationResult(validateCommitMessage('squash! feat(core): add feature'), VALID);
expectValidationResult(validateCommitMessage('squash! fix: a bug'), VALID);
expectValidationResult(validateCommitMessage('squash! fix a typo'), VALID);
});
});
describe('with `disallowSquash`', () => {
it('should fail', () => {
expect(validateCommitMessage('fix(core): something', {disallowSquash: true})).toBe(VALID);
expect(validateCommitMessage('squash! fix(core): something', {
disallowSquash: true
})).toBe(INVALID);
expect(lastError).toContain(
'The commit must be manually squashed into the target commit');
expectValidationResult(
validateCommitMessage('fix(core): something', {disallowSquash: true}), VALID);
expectValidationResult(
validateCommitMessage('squash! fix(core): something', {disallowSquash: true}),
INVALID, ['The commit must be manually squashed into the target commit']);
});
});
});
@ -187,9 +178,9 @@ describe('validate-commit-message.js', () => {
describe('(fixup)', () => {
describe('without `nonFixupCommitHeaders`', () => {
it('should return commits as valid', () => {
expect(validateCommitMessage('fixup! feat(core): add feature')).toBe(VALID);
expect(validateCommitMessage('fixup! fix: a bug')).toBe(VALID);
expect(validateCommitMessage('fixup! fixup! fix: a bug')).toBe(VALID);
expectValidationResult(validateCommitMessage('fixup! feat(core): add feature'), VALID);
expectValidationResult(validateCommitMessage('fixup! fix: a bug'), VALID);
expectValidationResult(validateCommitMessage('fixup! fixup! fix: a bug'), VALID);
});
});
@ -197,36 +188,39 @@ describe('validate-commit-message.js', () => {
it('should check that the fixup commit matches a non-fixup one', () => {
const msg = 'fixup! foo';
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}))
.toBe(VALID);
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}))
.toBe(VALID);
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}))
.toBe(VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}),
VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}),
VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}),
VALID);
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}))
.toBe(INVALID);
expect(lastError).toContain(
'Unable to find match for fixup commit among prior commits: \n' +
' qux\n' +
' quux\n' +
' quuux');
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}),
INVALID,
['Unable to find match for fixup commit among prior commits: \n' +
' qux\n' +
' quux\n' +
' quuux']);
});
it('should fail if `nonFixupCommitHeaders` is empty', () => {
expect(validateCommitMessage('refactor(core): make reactive', {
disallowSquash: false,
nonFixupCommitHeaders: []
})).toBe(VALID);
expect(validateCommitMessage(
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}))
.toBe(INVALID);
expect(lastError).toContain(
`Unable to find match for fixup commit among prior commits: -`);
expectValidationResult(
validateCommitMessage(
'refactor(core): make reactive',
{disallowSquash: false, nonFixupCommitHeaders: []}),
VALID);
expectValidationResult(
validateCommitMessage(
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}),
INVALID, [`Unable to find match for fixup commit among prior commits: -`]);
});
});
});
@ -246,24 +240,27 @@ describe('validate-commit-message.js', () => {
});
it('should fail validation if the body is shorter than `minBodyLength`', () => {
expect(validateCommitMessage(
'fix(core): something\n\n Explanation of the motivation behind this change'))
.toBe(VALID);
expect(validateCommitMessage('fix(core): something\n\n too short')).toBe(INVALID);
expect(lastError).toContain(
'The commit message body does not meet the minimum length of 30 characters');
expect(validateCommitMessage('fix(core): something')).toBe(INVALID);
expect(lastError).toContain(
'The commit message body does not meet the minimum length of 30 characters');
expectValidationResult(
validateCommitMessage(
'fix(core): something\n\n Explanation of the motivation behind this change'),
VALID);
expectValidationResult(
validateCommitMessage('fix(core): something\n\n too short'), INVALID,
['The commit message body does not meet the minimum length of 30 characters']);
expectValidationResult(validateCommitMessage('fix(core): something'), INVALID, [
'The commit message body does not meet the minimum length of 30 characters'
]);
});
it('should pass validation if the body is shorter than `minBodyLength` but the commit type is in the `minBodyLengthTypeExclusions` list',
() => {
expect(validateCommitMessage('docs: just fixing a typo')).toBe(VALID);
expect(validateCommitMessage('docs(core): just fixing a typo')).toBe(VALID);
expect(validateCommitMessage(
'docs(core): just fixing a typo\n\nThis was just a silly typo.'))
.toBe(VALID);
expectValidationResult(validateCommitMessage('docs: just fixing a typo'), VALID);
expectValidationResult(validateCommitMessage('docs(core): just fixing a typo'), VALID);
expectValidationResult(
validateCommitMessage(
'docs(core): just fixing a typo\n\nThis was just a silly typo.'),
VALID);
});
});
});

View File

@ -8,7 +8,7 @@
import {error} from '../utils/console';
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
import {parseCommitMessage} from './parse';
import {parseCommitMessage, ParsedCommitMessage} from './parse';
/** Options for commit message validation. */
export interface ValidateCommitMessageOptions {
@ -16,133 +16,147 @@ export interface ValidateCommitMessageOptions {
nonFixupCommitHeaders?: string[];
}
/** The result of a commit message validation check. */
export interface ValidateCommitMessageResult {
valid: boolean;
errors: string[];
commit: ParsedCommitMessage;
}
/** Regex matching a URL for an entire commit body line. */
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
/** Validate a commit message against using the local repo's config. */
export function validateCommitMessage(
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
function printError(errorMessage: string) {
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>`);
}
commitMsg: string, options: ValidateCommitMessageOptions = {}): ValidateCommitMessageResult {
const config = getCommitMessageConfig().commitMessage;
const commit = parseCommitMessage(commitMsg);
const errors: string[] = [];
////////////////////////////////////
// Checking revert, squash, fixup //
////////////////////////////////////
/** Perform the validation checks against the parsed commit. */
function validateCommitAndCollectErrors() {
// TODO(josephperrott): Remove early return calls when commit message errors are found
// All revert commits are considered valid.
if (commit.isRevert) {
return true;
}
////////////////////////////////////
// Checking revert, squash, fixup //
////////////////////////////////////
// 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) {
printError('The commit must be manually squashed into the target commit');
// 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;
}
// 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`);
return false;
}
return true;
}
// 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)) {
printError(
'Unable to find match for fixup commit among prior commits: ' +
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
if (!commit.type) {
errors.push(`The commit message header does not match the expected format.`);
return false;
}
if (COMMIT_TYPES[commit.type] === undefined) {
errors.push(`'${commit.type}' is not an allowed type.\n => TYPES: ${
Object.keys(COMMIT_TYPES).join(', ')}`);
return false;
}
/** The scope requirement level for the provided type of the commit message. */
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
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;
}
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
errors.push(
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
return false;
}
if (commit.scope && !config.scopes.includes(commit.scope)) {
errors.push(
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
return false;
}
// Commits with the type of `release` do not require a commit body.
if (commit.type === 'release') {
return true;
}
//////////////////////////
// Checking commit body //
//////////////////////////
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;
}
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);
});
if (lineExceedsMaxLength) {
errors.push(
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
return false;
}
return true;
}
////////////////////////////
// Checking commit header //
////////////////////////////
if (commit.header.length > config.maxLineLength) {
printError(`The commit message header is longer than ${config.maxLineLength} characters`);
return false;
}
if (!commit.type) {
printError(`The commit message header does not match the expected format.`);
return false;
}
if (COMMIT_TYPES[commit.type] === undefined) {
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${
Object.keys(COMMIT_TYPES).join(', ')}`);
return false;
}
/** The scope requirement level for the provided type of the commit message. */
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
printError(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
commit.scope}' was provided.`);
return false;
}
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
printError(
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
return false;
}
if (commit.scope && !config.scopes.includes(commit.scope)) {
printError(
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
return false;
}
// Commits with the type of `release` do not require a commit body.
if (commit.type === 'release') {
return true;
}
//////////////////////////
// Checking commit body //
//////////////////////////
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
printError(`The commit message body does not meet the minimum length of ${
config.minBodyLength} characters`);
return false;
}
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);
});
if (lineExceedsMaxLength) {
printError(
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
return false;
}
return true;
return {valid: validateCommitAndCollectErrors(), errors, commit};
}
/** 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();
}