UNPKG

commit-parser

Version:

A tiny parser for conventional commits that extracts metadata like type, scope, breaking changes and references

155 lines (151 loc) 4.7 kB
import { execSync } from "node:child_process"; //#region src/git.ts /** @internal */ function execCommand(cmd, cwd) { try { return execSync(cmd, { encoding: "utf8", cwd, stdio: [ "pipe", "pipe", "pipe" ] }).trim(); } catch { return ""; } } /** * The format of git log. * * commit_short_hash | subject | author_name | author_email | author_date | body * * @see {@link https://git-scm.com/docs/pretty-formats | documentation} for details. */ const GIT_LOG_FORMAT = "----%n%h|%s|%an|%ae|%ad|%b"; /** * Retrieves raw git commits between two references. * * @param {GetRawGitCommitStringsOptions} options - Options for fetching raw git commits. * * @returns {string[]} An array of raw git commit strings. */ function getRawGitCommitStrings(options) { const { from, to = "HEAD", cwd, folder } = options; const folderPath = folder ? ` -- ${folder}` : ""; const output = execCommand(`git --no-pager log "${from ? `${from}...${to}` : to}" --pretty="${GIT_LOG_FORMAT}"${folderPath}`, cwd); return output.split("----\n").filter(Boolean); } //#endregion //#region src/parse.ts 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; /** * Parses a raw git commit string into a structured format. * * @param {string} commit - A raw git commit string delimited by '|' character * @returns {RawGitCommit} A structured representation of the git commit */ 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 }; } /** * Extracts references (pull requests and issues) from a commit description * * @param {string} description - The commit description to extract references from * @returns {object} Object containing references array and cleaned description */ function extractReferences(description) { const references = []; for (const match of description.matchAll(PullRequestRE)) references.push({ type: "pull-request", value: match[1] }); for (const match of description.matchAll(IssueRE)) if (!references.some((ref) => ref.value === match[1])) references.push({ type: "issue", value: match[1] }); const cleanedDescription = description.replace(PullRequestRE, "").trim(); return { references, cleanedDescription }; } /** * Extracts co-authors from commit body * * @param {string} body - The commit body to extract co-authors from * @param {GitCommitAuthor} primaryAuthor - The primary commit author * @returns {GitCommitAuthor[]} Array of all authors (primary + co-authors) */ function extractAuthors(body, primaryAuthor) { const authors = [primaryAuthor]; for (const match of body.matchAll(CoAuthoredByRegex)) authors.push({ name: (match.groups?.name || "").trim(), email: (match.groups?.email || "").trim() }); return authors; } /** * Parses a raw git commit into a structured format with additional metadata * * @param {RawGitCommit} rawCommit - The raw git commit to parse * @returns {GitCommit} A structured representation of the git commit with additional metadata */ 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 || ""; const rawDescription = match?.groups?.description || message; const isBreaking = Boolean(match?.groups?.breaking || /breaking[ -]changes?:/i.test(body)); const { references, cleanedDescription } = extractReferences(rawDescription); const authors = extractAuthors(body, rawCommit.author); return { authors, body, data, description: cleanedDescription, isBreaking, isConventional, message, references, scope, shortHash, type }; } //#endregion //#region src/commits.ts /** * Retrieves a list of parsed git commits between two points in history. * * @param {GetCommitsOptions} options - Options for fetching and parsing git commits. * * @returns {GitCommit[]} An array of parsed GitCommit objects. */ function getCommits(options) { return getRawGitCommitStrings({ from: options.from, to: options.to, cwd: options.cwd, folder: options.folder }).map(parseRawCommit).map(parseCommit); } //#endregion export { getCommits, parseCommit, parseRawCommit };