Skip to content

Commit 7661e4f

Browse files
committed
feat: implement native check command with comprehensive validation
🔍 Enhanced Check Command: - Replaced placeholder message with full commit validation - Integrated conventional commit parsing and validation - Added support for special commits (merge, revert, fixup, initial) - Branded output with CommitWeave styling and helpful error messages - Lazy-loaded dependencies for optimal performance ✅ Features: - Validates commit type against configured types - Checks subject length limits and formatting rules - Provides detailed validation feedback with examples - Handles edge cases (special commits, missing repo, etc.) - Consistent with CommitWeave's visual design 🚀 User Experience: - Direct command: 'commitweave check' now works instantly - No more 'Use: npx tsx scripts/check-commit.ts' placeholder - Clear validation results with actionable feedback - Professional styling matching rest of CLI Performance optimized with lazy imports and efficient validation logic.
1 parent 34b72ce commit 7661e4f

File tree

1 file changed

+157
-3
lines changed

1 file changed

+157
-3
lines changed

bin/index.ts

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -541,9 +541,163 @@ async function editAndCommit(subject: string, body: string) {
541541
}
542542

543543
async function handleCheckCommand() {
544-
console.log('Check command not yet optimized for performance mode.');
545-
console.log('Use: npx tsx scripts/check-commit.ts for now.');
546-
process.exit(0);
544+
const { execSync } = await lazy(() => import('child_process'));
545+
const { readFile } = await lazy(() => import('fs/promises'));
546+
const { join } = await lazy(() => import('path'));
547+
548+
interface ValidationResult {
549+
valid: boolean;
550+
errors: string[];
551+
}
552+
553+
interface ParsedCommit {
554+
type?: string;
555+
scope?: string | undefined;
556+
breaking?: boolean;
557+
subject?: string;
558+
body?: string | undefined;
559+
footer?: string | undefined;
560+
}
561+
562+
// Load configuration
563+
async function loadConfig() {
564+
try {
565+
const configPath = join(process.cwd(), 'glinr-commit.json');
566+
const configFile = await readFile(configPath, 'utf-8');
567+
const configData = JSON.parse(configFile);
568+
const { ConfigSchema } = await lazy(() => import('../src/types/config.js'));
569+
return ConfigSchema.parse(configData);
570+
} catch (error) {
571+
const { defaultConfig } = await lazy(() => import('../src/config/defaultConfig.js'));
572+
return defaultConfig;
573+
}
574+
}
575+
576+
// Get latest commit message
577+
function getLatestCommitMessage(): string {
578+
try {
579+
const message = execSync('git log -1 --pretty=%B', { encoding: 'utf-8' });
580+
return message.trim();
581+
} catch (error) {
582+
console.error(chalk.red('❌ Error: Failed to read commit message from git'));
583+
console.error(chalk.yellow('💡 Make sure you are in a git repository with at least one commit'));
584+
console.error(chalk.gray(' Run: ') + chalk.cyan('git log --oneline -1') + chalk.gray(' to check recent commits'));
585+
process.exit(1);
586+
}
587+
}
588+
589+
// Check if commit is special (merge, revert, etc.)
590+
function isSpecialCommit(message: string): boolean {
591+
const header = message.split('\n')[0] || '';
592+
return header.startsWith('Merge ') ||
593+
header.startsWith('Revert ') ||
594+
header.startsWith('fixup! ') ||
595+
header.startsWith('squash! ') ||
596+
header.toLowerCase().includes('initial commit');
597+
}
598+
599+
// Parse conventional commit message
600+
function parseCommitMessage(message: string): ParsedCommit {
601+
const lines = message.split('\n');
602+
const header = lines[0] || '';
603+
604+
const conventionalPattern = /^(\w+)(\([^)]+\))?(!)?\s*:\s*(.+)$/;
605+
const match = header.match(conventionalPattern);
606+
607+
if (match) {
608+
const [, type, scopeWithParens, breaking, subject] = match;
609+
const scope = scopeWithParens ? scopeWithParens.slice(1, -1) : undefined;
610+
const bodyText = lines.slice(2).join('\n').trim();
611+
612+
return {
613+
type,
614+
scope,
615+
breaking: !!breaking,
616+
subject,
617+
body: bodyText || undefined,
618+
footer: undefined
619+
};
620+
}
621+
622+
return { subject: header };
623+
}
624+
625+
// Validate commit message
626+
function validateCommitMessage(commit: ParsedCommit, config: any): ValidationResult {
627+
const errors: string[] = [];
628+
629+
if (!commit.type) {
630+
errors.push('Missing commit type (e.g., feat, fix, docs)');
631+
return { valid: false, errors };
632+
}
633+
634+
const validTypes = config.commitTypes.map((t: any) => t.type);
635+
if (!validTypes.includes(commit.type)) {
636+
errors.push(`Invalid commit type "${commit.type}". Valid types: ${validTypes.join(', ')}`);
637+
}
638+
639+
if (!commit.subject || commit.subject.length === 0) {
640+
errors.push('Missing commit subject');
641+
} else {
642+
if (commit.subject.length > config.maxSubjectLength) {
643+
errors.push(`Subject too long (${commit.subject.length}/${config.maxSubjectLength} chars)`);
644+
}
645+
646+
if (commit.subject.endsWith('.')) {
647+
errors.push('Subject should not end with a period');
648+
}
649+
650+
if (commit.subject !== commit.subject.toLowerCase()) {
651+
errors.push('Subject should be in lowercase');
652+
}
653+
}
654+
655+
return { valid: errors.length === 0, errors };
656+
}
657+
658+
// Main check logic
659+
try {
660+
const config = await loadConfig();
661+
const commitMessage = getLatestCommitMessage();
662+
663+
console.log(chalk.hex('#8b008b').bold('🔍 Checking latest commit message...\n'));
664+
665+
if (isSpecialCommit(commitMessage)) {
666+
console.log(chalk.yellow('⚠️ Special commit detected (merge/revert/fixup/initial)'));
667+
console.log(chalk.gray(' Skipping conventional commit validation'));
668+
console.log(chalk.green('✅ Check complete'));
669+
return;
670+
}
671+
672+
const parsed = parseCommitMessage(commitMessage);
673+
const validation = validateCommitMessage(parsed, config);
674+
675+
console.log(chalk.gray('Latest commit:'));
676+
console.log(chalk.cyan(`"${commitMessage.split('\n')[0]}"`));
677+
console.log('');
678+
679+
if (validation.valid) {
680+
console.log(chalk.green('✅ Commit message follows conventional commit format'));
681+
if (parsed.type) {
682+
const commitType = config.commitTypes.find((t: any) => t.type === parsed.type);
683+
if (commitType) {
684+
console.log(chalk.gray(` Type: ${commitType.emoji} ${parsed.type} - ${commitType.description}`));
685+
}
686+
}
687+
} else {
688+
console.log(chalk.red('❌ Commit message validation failed:'));
689+
validation.errors.forEach(error => {
690+
console.log(chalk.red(` • ${error}`));
691+
});
692+
console.log('');
693+
console.log(chalk.yellow('💡 Example valid commit:'));
694+
console.log(chalk.green(' feat: add user authentication'));
695+
console.log(chalk.green(' fix(api): resolve login timeout issue'));
696+
}
697+
} catch (error) {
698+
console.error(chalk.red('❌ Validation failed:'), error instanceof Error ? error.message : error);
699+
process.exit(1);
700+
}
547701
}
548702

549703
async function handleConfigSubmenu() {

0 commit comments

Comments
 (0)