tiny-conventional-commits-parser
Version:
A tiny conventional commits parser
90 lines (85 loc) • 3.01 kB
JavaScript
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) || void 0;
}
function getCurrentGitTag() {
return execCommand("git tag --points-at HEAD") || void 0;
}
const GIT_LOG_FORMAT = "%h|%s|%an|%ae|%ad|%b[GIT_LOG_COMMIT_END]";
function getGitLog(from, to = "HEAD", path) {
return execCommand(`git --no-pager log "${from ? `${from}...${to}` : to}" --pretty="${GIT_LOG_FORMAT}" ${path ? `-- ${path}` : ""}`).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, path) {
return getGitLog(from, to, path).map(parseRawCommit).map(parseCommit);
}
function getRecentCommits(from, to, path) {
if (!from)
from = getLastGitTag();
if (!to)
to = "HEAD";
return getCommits(from, to, path);
}
export { getCommits, getCurrentGitTag, getGitLog, getLastGitTag, getRecentCommits, parseCommit, parseRawCommit };