import consola from 'consola' import execa from 'execa' import groupBy from 'lodash/groupBy' import sortBy from 'lodash/sortBy' import uniq from 'lodash/uniq' import { writeFile } from 'fs-extra' const types = { fix: { title: '🐛 Bug Fix' }, feat: { title: '🚀 Features' }, refactor: { title: '💅 Refactors' }, perf: { title: '🔥 Performance' }, examples: { title: '📝 Examples' }, chore: { title: '🏡 Chore' }, test: { title: '👓 Tests' } } const knownAuthors = [ 'chopin', 'parsa', 'clark', 'galvez', 'lichter', 'molotkov', 'marrec', 'pim' ] const isKnownAuthor = name => Boolean(knownAuthors.find(n => name.toLowerCase().includes(n))) const allowedTypes = Object.keys(types) async function main() { // Get last git tag const lastGitTag = await getLastGitTag() // Get current branch const currentGitBranch = await getCurrentGitBranch() // Get all commits from last release to current branch consola.log(`${currentGitBranch}...${lastGitTag}`) let commits = await getGitDiff(currentGitBranch, lastGitTag) // Parse commits as conventional commits commits = parseCommits(commits) // Filter commits commits = commits.filter(c => allowedTypes.includes(c.type) && c.scope !== 'deps' ) // Generate markdown const markdown = generateMarkDown(commits) process.stdout.write('\n\n' + markdown + '\n\n') await writeFile('CHANGELOG.md', markdown, 'utf-8') } function execCommand(cmd, args) { return execa(cmd, args).then(r => r.stdout) } async function getLastGitTag() { const r = await execCommand('git', ['--no-pager', 'tag', '-l']).then(r => r.split('\n')) return r[r.length - 1] } async function getCurrentGitBranch() { const r = await execCommand('git', ['rev-parse', '--abbrev-ref', 'HEAD']) return r } async function getGitDiff(from, to) { // # https://git-scm.com/docs/pretty-formats const r = await execCommand('git', ['--no-pager', 'log', `${from}...${to}`, '--pretty=%s|%h|%an|%ae']) return r.split('\n').map((line) => { const [message, commit, authorName, authorEmail] = line.split('|') return { message, commit, authorName, authorEmail } }) } function parseCommits(commits) { return commits.filter(c => c.message.includes(':')).map((commit) => { let [type, ...message] = commit.message.split(':') message = message.join(':') // Extract references from message message = message.replace(/\((fixes) #\d+\)/g, '') const references = [] const referencesRegex = /#[0-9]+/g let m while (m = referencesRegex.exec(message)) { // eslint-disable-line no-cond-assign references.push(m[0]) } // Remove references and normalize message = message.replace(referencesRegex, '').replace(/\(\)/g, '').trim() // Extract scope from type let scope = type.match(/\((.*)\)/) if (scope) { scope = scope[1] } if (!scope) { scope = 'general' } type = type.split('(')[0] return { ...commit, message, type, scope, references } }) } function generateMarkDown(commits) { const typeGroups = groupBy(commits, 'type') let markdown = '' for (const type of allowedTypes) { const group = typeGroups[type] if (!group || !group.length) { continue } const { title } = types[type] markdown += '\n\n' + '#### ' + title + '\n\n' const scopeGroups = groupBy(group, 'scope') for (const scopeName in scopeGroups) { markdown += '- `' + scopeName + '`' + '\n' for (const commit of scopeGroups[scopeName]) { markdown += ' - ' + commit.references.join(', ') + ' ' + commit.message.replace(/^(.)/, v => v.toUpperCase()) + '\n' } } } const authors = sortBy(uniq(commits.map(commit => commit.authorName).filter(an => !isKnownAuthor(an)))) if (authors.length) { markdown += '\n\n' + '#### ' + '💖 Thanks to' + '\n\n' markdown += authors.map(name => '- ' + name).join('\n') } return markdown.trim() } main().catch(consola.error)