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
JavaScript
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 };