UNPKG

tiny-conventional-commits-parser

Version:
90 lines (85 loc) 2.96 kB
import { execSync } from 'node:child_process'; function execCommand(cmd, options) { try { return execSync(cmd, { encoding: "utf8", cwd: options?.cwd }).trim(); } catch (error) { return ""; } } function getLastGitTag() { return execCommand("git describe --tags --abbrev=0")?.split("\n").at(0) || undefined; } function getCurrentGitTag() { return execCommand("git tag --points-at HEAD") || undefined; } const GIT_LOG_FORMAT = "%h|%s|%an|%ae|%ad|%b[GIT_LOG_COMMIT_END]"; function getGitLog(from, to = "HEAD") { return execCommand(`git --no-pager log "${from ? `${from}...${to}` : to}" --pretty="${GIT_LOG_FORMAT}"`).split("[GIT_LOG_COMMIT_END]\n").filter(Boolean); } const ConventionalCommitRegex = /(?<emoji>:.+:|(\uD83C[\uDF00-\uDFFF])|(\uD83D[\uDC00-\uDE4F\uDE80-\uDEFF])|[\u2600-\u2B55])?( *)(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i; const CoAuthoredByRegex = /co-authored-by:\s*(?<name>.+)(<(?<email>.+)>)/gi; const PullRequestRE = /\([ a-z]*(#\d+)\s*\)/g; const IssueRE = /(#\d+)/g; const BreakingRE = /breaking[ -]changes?:/i; function parseRawCommit(commit) { const [shortHash, message, authorName, authorEmail, data, ..._body] = commit.split("|"); const body = _body.filter(Boolean).join("\n"); return { author: { name: authorName, email: authorEmail }, body, data, message, shortHash }; } function parseCommit(rawCommit) { const { shortHash, message, body, data } = rawCommit; const match = message.match(ConventionalCommitRegex); const isConventional = match !== null; const type = match?.groups?.type || ""; const scope = match?.groups?.scope || ""; let description = match?.groups?.description || message; const hasBreakingBody = BreakingRE.test(body); const isBreaking = Boolean(match?.groups?.breaking || hasBreakingBody); const references = []; for (const m of description.matchAll(PullRequestRE)) { references.push({ type: "pull-request", value: m[1] }); } for (const m of description.matchAll(IssueRE)) { if (!references.some((i) => i.value === m[1])) { references.push({ type: "issue", value: m[1] }); } } description = description.replace(PullRequestRE, "").trim(); const authors = [rawCommit.author]; for (const match2 of body.matchAll(CoAuthoredByRegex)) { authors.push({ name: (match2.groups?.name || "").trim(), email: (match2.groups?.email || "").trim() }); } return { authors, body, data, description, isBreaking, isConventional, message, references, scope, shortHash, type }; } function getCommits(from, to) { return getGitLog(from, to).map(parseRawCommit).map(parseCommit); } function getRecentCommits(from, to) { if (!from) from = getLastGitTag(); if (!to) to = "HEAD"; return getCommits(from, to); } export { getCommits, getCurrentGitTag, getGitLog, getLastGitTag, getRecentCommits, parseCommit, parseRawCommit };