@dev-build-deploy/commit-it
Version:
(Conventional) Commits library
210 lines (209 loc) • 7.2 kB
JavaScript
/*
* SPDX-FileCopyrightText: 2023 Kevin de Jong <monkaii@hotmail.com>
* SPDX-License-Identifier: MIT
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseCommitMessage = exports.getFooterElementsFromParagraph = exports.Commit = void 0;
const git = __importStar(require("./git"));
const TRAILER_REGEX = /^((BREAKING CHANGE:)|([\w-]+(:| #))|([ \t]+)\w*)/i;
/**
* Git Commit
* @class Commit
* @member author The commit author and date
* @member commiter The commit commiter and date
* @member hash The commit hash
* @member subject The commit subject
* @member body The commit body
* @member footer The commit footer
* @member raw The commit message
*/
class Commit {
_commit;
constructor(commit) {
this._commit = commit;
}
/**
* Retrieves the commit information from git using the provided hash
* @param props The commit hash and root path
* @returns The commit object
*/
static fromHash(props) {
const commit = git.getCommitFromHash(props.hash, props.rootPath ?? process.cwd());
return new Commit(commit);
}
/**
* Creates a Commit object from the provided string
* @param props The commit hash, author, committer and message
* @returns The commit object
*/
static fromString(props) {
// Git will trim all comments (lines starting with #), so we do the same
// to ensure the commit message is parsed correctly.
const trimmedMessage = props.message
.split(/\r?\n/)
.filter(line => !line.startsWith("#"))
.join("\n");
const commit = {
hash: props.hash,
...parseCommitMessage(trimmedMessage),
author: props.author,
committer: props.committer,
raw: props.message,
};
return new Commit(commit);
}
get author() {
return this._commit.author;
}
get committer() {
return this._commit.committer;
}
get hash() {
return this._commit.hash;
}
get subject() {
return this._commit.subject;
}
get body() {
return this._commit.body;
}
get footer() {
return this._commit.footer;
}
get raw() {
return this._commit.raw;
}
get isFixupCommit() {
return this._commit.attributes.isFixup;
}
get isMergeCommit() {
return this._commit.attributes.isMerge;
}
toJSON() {
return this._commit;
}
}
exports.Commit = Commit;
/**
* Returns a dictionary containing key-value pairs extracted from the footer of the provided commit message.
* The key must either be:
* - a single word (optional, using - as a seperator) followed by a colon (:)
* - BREAKING CHANGE:
*
* The value is either:
* - the remainder of the line
* - the remainder of the line + anything that follows on the next lines which is indented by at least one space
*
* @internal
*/
function getFooterElementsFromParagraph(footer) {
const footerLines = footer.split(/\r?\n/);
const result = [];
for (let lineNr = 0; lineNr < footerLines.length; lineNr++) {
const line = footerLines[lineNr];
const match = TRAILER_REGEX.exec(line);
if (match === null)
continue;
let key = match[1].replace(/:$/, "");
let value = line.substring(match[1].length).trim();
if (match[1].endsWith(" #")) {
key = match[1].substring(0, match[1].length - 2);
value = `#${value}`;
}
const matchLine = lineNr;
// Check if the value continues on the next line
while (lineNr + 1 < footerLines.length &&
(/^\s/.test(footerLines[lineNr + 1]) || footerLines[lineNr + 1].length === 0)) {
lineNr++;
value += "\n" + footerLines[lineNr].trim();
}
result.push({
lineNumber: matchLine + 1,
key,
value,
});
}
return Object.keys(result).length > 0 ? result : undefined;
}
exports.getFooterElementsFromParagraph = getFooterElementsFromParagraph;
/**
* Checks if the provided subject is a common (default) merge pattern.
* Currently supported:
* - GitHub
* - BitBucket
* - GitLab
*
* @param subject The subject to check
* @returns True if the subject is a common merge pattern, false otherwise
*/
function subjectIsMergePattern(subject) {
const githubMergeRegex = /^Merge pull request #(\d+) from '?([a-zA-Z0-9_./-]+)'?$/;
const bitbucketMergeRegex = /^Merged in '?([a-zA-Z0-9_./-]+)'? \(pull request #(\d+)\)$/;
const gitlabMergeRegex = /^Merge( remote-tracking)? branch '?([a-zA-Z0-9_./-]+)'?? into '?([a-zA-Z0-9_./-]+)'?$/;
return githubMergeRegex.test(subject) || bitbucketMergeRegex.test(subject) || gitlabMergeRegex.test(subject);
}
/**
* Parses the provided commit message (full message, not just the subject) into
* a Commit object.
* @param message The commit message
* @returns The parsed commit object
* @internal
*/
function parseCommitMessage(message) {
const isTrailerOnly = (message) => message.split(/\r?\n/).every(line => {
const match = TRAILER_REGEX.exec(line);
return match !== null;
});
const paragraphs = message.split(/^\r?\n/m);
let footer = undefined;
let body = undefined;
if (paragraphs.length > 1 && isTrailerOnly(paragraphs[paragraphs.length - 1])) {
footer = paragraphs[paragraphs.length - 1].trim();
paragraphs.pop();
}
if (paragraphs.length > 1) {
body = paragraphs.splice(1).join("\n").trim();
if (body === "")
body = undefined;
}
const subject = paragraphs[0].trim();
const isFixup = subject.toLowerCase().startsWith("fixup!");
const isMerge = subjectIsMergePattern(subject);
return {
subject,
body,
footer: getFooterElementsFromParagraph(footer ?? "")?.reduce((acc, cur) => {
acc[cur.key] = cur.value;
return acc;
}, {}),
attributes: {
isFixup,
isMerge,
},
};
}
exports.parseCommitMessage = parseCommitMessage;
;