bumper-cli
Version:
š A magical release management system with beautiful changelogs and automated workflows
188 lines ⢠6.98 kB
JavaScript
import { execSync } from 'node:child_process';
import chalk from 'chalk';
// Conventional commit regex pattern
const CONVENTIONAL_COMMIT_REGEX = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|security)(\([\w-]+\))?(!)?:\s.+$/;
// Parse commit message
const parseCommitMessage = (message) => {
const match = message.match(CONVENTIONAL_COMMIT_REGEX);
if (!match) {
return {
type: 'invalid',
breaking: false,
subject: message,
};
}
const [, commitType, commitScope, isBreaking] = match;
const subject = message.substring(message.indexOf(':') + 1).trim();
return {
type: commitType || 'invalid',
scope: commitScope ? commitScope.slice(1, -1) : undefined,
breaking: !!isBreaking,
subject,
};
};
// Check for common commit issues
const checkCommonIssues = (message) => {
const warnings = [];
if (message.toLowerCase().includes('wip')) {
warnings.push('Commit message contains "WIP" - consider squashing before release');
}
if (message.toLowerCase().includes('fixup')) {
warnings.push('Commit message contains "fixup" - consider squashing before release');
}
return warnings;
};
// Validate commit structure
const validateCommitStructure = (parsed) => {
const errors = [];
if (parsed.type === 'invalid') {
errors.push('Invalid commit type');
}
if (parsed.subject.length === 0) {
errors.push('Commit subject is empty');
}
return errors;
};
// Validate commit subject
const validateCommitSubject = (parsed) => {
const warnings = [];
if (parsed.subject.endsWith('.')) {
warnings.push('Commit subject ends with a period');
}
return warnings;
};
// Validate single commit message
const validateCommitMessage = (message) => {
const errors = [];
const warnings = [];
// Check if message follows conventional commit format
if (!CONVENTIONAL_COMMIT_REGEX.test(message)) {
errors.push('Commit message does not follow conventional commit format');
}
// Check message length
if (message.length > 72) {
warnings.push('Commit message is longer than 72 characters');
}
// Check for common issues
warnings.push(...checkCommonIssues(message));
// Parse and validate structure
const parsed = parseCommitMessage(message);
errors.push(...validateCommitStructure(parsed));
warnings.push(...validateCommitSubject(parsed));
return {
isValid: errors.length === 0,
errors,
warnings,
};
};
// Parse commit line from git log
const parseCommitLine = (commitLine) => {
const commitParts = commitLine.split('|');
const commitHash = commitParts[0] || '';
const commitMessage = commitParts[1] || '';
return {
hash: commitHash.substring(0, 8),
message: commitMessage,
};
};
// Get commits from git log
const getCommitsFromGitLog = (range) => {
const command = range
? `git log --pretty=format:"%H|%s" ${range}`
: 'git log --pretty=format:"%H|%s"';
const commits = execSync(command, { encoding: 'utf8' }).trim();
if (!commits)
return [];
return commits.split('\n').map(parseCommitLine);
};
// Get commits since last tag
const getCommitsSinceLastTag = () => {
try {
const lastTag = execSync('git describe --tags --abbrev=0', {
encoding: 'utf8',
}).trim();
return getCommitsFromGitLog(`${lastTag}..HEAD`);
}
catch {
// If no tags exist, get all commits
return getCommitsFromGitLog();
}
};
// Display validation result for a single commit
const displayCommitValidation = (hash, message, validationResult) => {
let errors = 0;
let warnings = 0;
if (validationResult.errors.length > 0) {
errors = validationResult.errors.length;
console.log(chalk.red(`ā ${hash}: ${message}`));
for (const errorMessage of validationResult.errors) {
console.log(chalk.red(` - ${errorMessage}`));
}
}
else if (validationResult.warnings.length > 0) {
warnings = validationResult.warnings.length;
console.log(chalk.yellow(`ā ļø ${hash}: ${message}`));
for (const warningMessage of validationResult.warnings) {
console.log(chalk.yellow(` - ${warningMessage}`));
}
}
else {
console.log(chalk.green(`ā
${hash}: ${message}`));
}
return { errors, warnings };
};
// Display validation summary
const displayValidationSummary = (commits, results, totalErrors, totalWarnings) => {
console.log('\nš Validation Summary:');
console.log(`Total commits: ${chalk.blue(commits.length)}`);
console.log(`Valid commits: ${chalk.green(commits.length - results.filter((result) => !result.isValid).length)}`);
console.log(`Invalid commits: ${chalk.red(results.filter((result) => !result.isValid).length)}`);
console.log(`Total errors: ${chalk.red(totalErrors)}`);
console.log(`Total warnings: ${chalk.yellow(totalWarnings)}`);
};
// Display final validation message
const displayFinalMessage = (totalErrors, totalWarnings) => {
if (totalErrors > 0) {
console.log(chalk.red('\nā Validation failed! Please fix the errors above.'));
console.log(chalk.blue('š” Tip: Use "git commit --amend" to fix recent commits'));
}
else if (totalWarnings > 0) {
console.log(chalk.yellow('\nā ļø Validation passed with warnings.'));
}
else {
console.log(chalk.green('\nā
All commits are valid!'));
}
};
// Main validation function
export const validateCommits = async () => {
console.log(chalk.blue('š Validating commit messages...'));
const commits = getCommitsSinceLastTag();
if (commits.length === 0) {
console.log(chalk.yellow('ā ļø No commits found to validate.'));
return { isValid: true, errors: [], warnings: [] };
}
console.log(chalk.green(`š Validating ${commits.length} commits...`));
const results = [];
let totalErrors = 0;
let totalWarnings = 0;
for (const { hash, message } of commits) {
const validationResult = validateCommitMessage(message);
results.push(validationResult);
const { errors, warnings } = displayCommitValidation(hash, message, validationResult);
totalErrors += errors;
totalWarnings += warnings;
}
displayValidationSummary(commits, results, totalErrors, totalWarnings);
const allErrors = results.flatMap((result) => result.errors);
const allWarnings = results.flatMap((result) => result.warnings);
displayFinalMessage(totalErrors, totalWarnings);
return {
isValid: totalErrors === 0,
errors: allErrors,
warnings: allWarnings,
};
};
// Export for use in other modules
export { validateCommitMessage, parseCommitMessage };
//# sourceMappingURL=validateCommits.js.map