(c: RawCommit)
| 30 | const CONV_RE = /^(?<type>[a-z]+)(?:\((?<scope>[^)]+)\))?(?<bang>!)?:\s*(?<desc>.+)$/i; |
| 31 | |
| 32 | export function classifyCommit(c: RawCommit): ClassifiedCommit { |
| 33 | const m = CONV_RE.exec(c.subject.trim()); |
| 34 | const bodyLower = c.body.toLowerCase(); |
| 35 | const hasBreakingTrailer = /\bbreaking[- ]change\b/i.test(c.body); |
| 36 | |
| 37 | if (m?.groups) { |
| 38 | const type = m.groups.type!.toLowerCase(); |
| 39 | const scope = m.groups.scope; |
| 40 | const bang = m.groups.bang === '!'; |
| 41 | if (bang || hasBreakingTrailer) { |
| 42 | return { ...c, category: 'breaking', scope, reason: `conventional ${type}!${scope ? `(${scope})` : ''}` }; |
| 43 | } |
| 44 | switch (type) { |
| 45 | case 'feat': |
| 46 | case 'feature': |
| 47 | return { ...c, category: 'features', scope, reason: `conventional feat` }; |
| 48 | case 'fix': |
| 49 | case 'bugfix': |
| 50 | case 'hotfix': |
| 51 | return { ...c, category: 'fixes', scope, reason: `conventional fix` }; |
| 52 | case 'perf': |
| 53 | return { ...c, category: 'perf', scope, reason: `conventional perf` }; |
| 54 | case 'docs': |
| 55 | case 'doc': |
| 56 | return { ...c, category: 'docs', scope, reason: `conventional docs` }; |
| 57 | case 'refactor': |
| 58 | case 'chore': |
| 59 | case 'test': |
| 60 | case 'tests': |
| 61 | case 'ci': |
| 62 | case 'build': |
| 63 | case 'style': |
| 64 | return { ...c, category: 'internal', scope, reason: `conventional ${type}` }; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | if (hasBreakingTrailer) { |
| 69 | return { ...c, category: 'breaking', reason: 'BREAKING CHANGE trailer in body' }; |
| 70 | } |
| 71 | |
| 72 | // Free-form heuristic fallback. Order matters — earlier wins. |
| 73 | const subj = c.subject.toLowerCase(); |
| 74 | if (/\b(add|new|introduce|implement|support)\b/.test(subj)) { |
| 75 | return { ...c, category: 'features', reason: 'subject suggests addition' }; |
| 76 | } |
| 77 | if (/\b(fix|resolve|correct|patch|repair|prevent)\b/.test(subj)) { |
| 78 | return { ...c, category: 'fixes', reason: 'subject suggests fix' }; |
| 79 | } |
| 80 | if (/\b(refactor|cleanup|rename|reorganize|move|extract)\b/.test(subj)) { |
| 81 | return { ...c, category: 'internal', reason: 'subject suggests refactor' }; |
| 82 | } |
| 83 | if (/\b(doc|docs|readme|comment)\b/.test(subj)) { |
| 84 | return { ...c, category: 'docs', reason: 'subject suggests docs' }; |
| 85 | } |
| 86 | if (/\b(perf|performance|optimi[sz]e|speed)\b/.test(subj)) { |
| 87 | return { ...c, category: 'perf', reason: 'subject suggests perf' }; |
| 88 | } |
| 89 | if (bodyLower.includes('breaking')) { |
no outgoing calls
no test coverage detected