@storm-software/git-tools
Version:
Tools for managing Git repositories within a Nx workspace.
1,489 lines (1,461 loc) • 133 kB
JavaScript
#!/usr/bin/env node
import './chunk-Q3DQKTOI.js';
import { run } from './chunk-G3YPGVPS.js';
import { getConfig, handleProcess, writeSuccess, exitWithSuccess, exitWithError, writeInfo, findWorkspaceRootSafe, writeFatal, getWorkspaceConfig, joinPaths, writeDebug, isVerbose, writeWarning, defu, writeTrace, STORM_DEFAULT_RELEASE_BANNER } from './chunk-GMMJM4AI.js';
import TOML from '@ltd/j-toml';
import { Command } from 'commander';
import '@inquirer/checkbox';
import '@inquirer/editor';
import default4 from '@inquirer/confirm';
import default5 from '@inquirer/input';
import '@inquirer/number';
import '@inquirer/expand';
import '@inquirer/rawlist';
import '@inquirer/password';
import '@inquirer/search';
import default11 from '@inquirer/select';
import shellescape from 'any-shell-escape';
import chalkTemplate from 'chalk-template';
import fs, { readFile as readFile$1, writeFile } from 'node:fs/promises';
import createBasePreset from 'conventional-changelog-conventionalcommits';
import { readCachedProjectGraph, createProjectGraphAsync as createProjectGraphAsync$1, readProjectsConfigurationFromProjectGraph as readProjectsConfigurationFromProjectGraph$1 } from 'nx/src/project-graph/project-graph';
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import childProcess, { execSync } from 'node:child_process';
import defaultRules from '@commitlint/rules';
import 'stream';
import util from 'node:util';
import { existsSync as existsSync$1, rmSync, readdirSync, readFileSync, writeFileSync, statSync, promises } from 'node:fs';
import wrap from 'word-wrap';
import Path, { join as join$1, extname } from 'node:path';
import { createProjectGraphAsync, readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph.js';
import { parse as parse$1, Syntax } from '@textlint/markdown-to-ast';
import anchor from 'anchor-markdown-header';
import { Parser } from 'htmlparser2';
import _ from 'underscore';
import updateSection from 'update-section';
import { readCachedProjectGraph as readCachedProjectGraph$1, createProjectGraphAsync as createProjectGraphAsync$2, readProjectsConfigurationFromProjectGraph as readProjectsConfigurationFromProjectGraph$2, output, joinPathFragments } from '@nx/devkit';
import axios from 'axios';
import { homedir } from 'node:os';
import { defaultCreateReleaseProvider, GithubRemoteReleaseClient } from 'nx/src/command-line/release/utils/remote-release-clients/github';
import { parse } from 'yaml';
import chalk from 'chalk';
import { printAndFlushChanges } from 'nx/src/command-line/release/utils/print-changes';
import { createGitTagValues, handleDuplicateGitTags, createCommitMessageValues, isPrerelease, shouldPreferDockerVersionForReleaseGroup, ReleaseVersion, noDiffInChangelogMessage } from 'nx/src/command-line/release/utils/shared';
import { interpolate } from 'nx/src/tasks-runner/utils';
import { resolveConfig, format } from 'prettier';
import { execCommand } from 'nx/src/command-line/release/utils/exec-command.js';
import { getCommitHash, getLatestGitTagForPattern, getFirstGitCommit, gitPush, getGitDiff, parseCommits, gitAdd } from 'nx/src/command-line/release/utils/git';
import { prerelease, major } from 'semver';
import { ReleaseClient } from 'nx/release';
import { readNxJson } from 'nx/src/config/nx-json';
import { FsTree } from 'nx/src/generators/tree';
import { createFileMapUsingProjectGraph } from 'nx/src/project-graph/file-map-utils';
import DefaultChangelogRenderer from 'nx/release/changelog-renderer';
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from 'nx/src/command-line/release/config/conventional-commits';
function parseCargoToml(cargoString) {
if (!cargoString) {
throw new Error("Cargo.toml is empty");
}
return TOML.parse(cargoString, {
x: { comment: true }
});
}
function stringifyCargoToml(cargoToml) {
const tomlString = TOML.stringify(cargoToml, {
newlineAround: "section"
});
if (Array.isArray(tomlString)) {
return tomlString.join("\n");
}
return tomlString;
}
// ../conventional-changelog/src/commit-types.ts
var DEFAULT_COMMIT_TYPES = {
/* --- Bumps version when selected --- */
"chore": {
"description": "Other changes that don't modify src or test files",
"title": "Chore",
"emoji": "\u2699\uFE0F ",
"semverBump": "patch",
"changelog": {
"title": "Miscellaneous",
"hidden": false
}
},
"fix": {
"description": "A change that resolves an issue previously identified with the package",
"title": "Bug Fix",
"emoji": "\u{1FAB2} ",
"semverBump": "patch",
"changelog": {
"title": "Bug Fixes",
"hidden": false
}
},
"feat": {
"description": "A change that adds a new feature to the package",
"title": "Feature",
"emoji": "\u{1F511} ",
"semverBump": "minor",
"changelog": {
"title": "Features",
"hidden": false
}
},
"ci": {
"description": "Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)",
"title": "Continuous Integration",
"emoji": "\u{1F9F0} ",
"semverBump": "patch",
"changelog": {
"title": "Continuous Integration",
"hidden": false
}
},
"refactor": {
"description": "A code change that neither fixes a bug nor adds a feature",
"title": "Code Refactoring",
"emoji": "\u{1F9EA} ",
"semverBump": "patch",
"changelog": {
"title": "Source Code Improvements",
"hidden": false
}
},
"style": {
"description": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
"title": "Style Improvements",
"emoji": "\u{1F48E} ",
"semverBump": "patch",
"changelog": {
"title": "Style Improvements",
"hidden": false
}
},
"perf": {
"description": "A code change that improves performance",
"title": "Performance Improvement",
"emoji": "\u23F1\uFE0F ",
"semverBump": "patch",
"changelog": {
"title": "Performance Improvements",
"hidden": false
}
},
/* --- Does not bump version when selected --- */
"docs": {
"description": "A change that only includes documentation updates",
"title": "Documentation",
"emoji": "\u{1F4DC} ",
"semverBump": "none",
"changelog": {
"title": "Documentation",
"hidden": false
}
},
"test": {
"description": "Adding missing tests or correcting existing tests",
"title": "Testing",
"emoji": "\u{1F6A8} ",
"semverBump": "none",
"changelog": {
"title": "Testing",
"hidden": true
}
},
/* --- Not included in commitlint but included in changelog --- */
"deps": {
"description": "Changes that add, update, or remove dependencies. This includes devDependencies and peerDependencies",
"title": "Dependencies",
"emoji": "\u{1F4E6} ",
"hidden": true,
"semverBump": "patch",
"changelog": {
"title": "Dependency Upgrades",
"hidden": false
}
},
/* --- Not included in commitlint or changelog --- */
"build": {
"description": "Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)",
"title": "Build",
"emoji": "\u{1F6E0} ",
"hidden": true,
"semverBump": "none",
"changelog": {
"title": "Build",
"hidden": true
}
},
"release": {
"description": "Publishing a commit containing a newly released version",
"title": "Publish Release",
"emoji": "\u{1F680} ",
"hidden": true,
"semverBump": "none",
"changelog": {
"title": "Publish Release",
"hidden": true
}
}
};
// ../conventional-changelog/src/utilities/constants.ts
var CHANGELOG_COMMIT_TYPES_OBJECT = Object.freeze(
Object.entries(DEFAULT_COMMIT_TYPES).reduce(
(ret, [key, commitType]) => {
ret[key] = {
...commitType.changelog,
type: key,
section: commitType.changelog?.title || commitType.title,
hidden: commitType.changelog?.hidden
};
return ret;
},
{}
)
);
var CHANGELOG_COMMIT_TYPES = [
CHANGELOG_COMMIT_TYPES_OBJECT.feat,
CHANGELOG_COMMIT_TYPES_OBJECT.fix,
CHANGELOG_COMMIT_TYPES_OBJECT.chore,
CHANGELOG_COMMIT_TYPES_OBJECT.deps,
CHANGELOG_COMMIT_TYPES_OBJECT.docs,
CHANGELOG_COMMIT_TYPES_OBJECT.style,
CHANGELOG_COMMIT_TYPES_OBJECT.refactor,
CHANGELOG_COMMIT_TYPES_OBJECT.perf,
CHANGELOG_COMMIT_TYPES_OBJECT.build,
CHANGELOG_COMMIT_TYPES_OBJECT.ci,
CHANGELOG_COMMIT_TYPES_OBJECT.test
];
CHANGELOG_COMMIT_TYPES.map(
(entry) => entry.type
);
CHANGELOG_COMMIT_TYPES.map(
(entry) => entry.section
);
// ../conventional-changelog/src/configs/minimal.ts
var changelogs = {
props: {
ignoreCommits: void 0,
types: CHANGELOG_COMMIT_TYPES,
bumpStrict: true,
scope: void 0,
scopeOnly: false
}
};
var commitlint = {
helpUrl: "https://developer.stormsoftware.com/commitlint/minimal",
rules: {
"body-leading-blank": [1 /* Warning */, "always"],
"body-max-length": [2 /* Error */, "always", 600],
"footer-leading-blank": [1 /* Warning */, "always"],
"footer-max-line-length": [2 /* Error */, "always", 150],
"header-max-length": [2 /* Error */, "always", 150],
"header-trim": [2 /* Error */, "always"],
"subject-case": [2 /* Error */, "always", ["sentence-case"]],
"subject-empty": [2 /* Error */, "never"],
"subject-full-stop": [2 /* Error */, "never", "."],
"subject-max-length": [2 /* Error */, "always", 150],
"subject-min-length": [2 /* Error */, "always", 3],
"type-case": [2 /* Error */, "always", "kebab-case"],
"type-empty": [2 /* Error */, "never"],
"type-enum": [
2 /* Error */,
"always",
Object.keys(DEFAULT_COMMIT_TYPES)
],
"type-max-length": [2 /* Error */, "always", 20],
"type-min-length": [2 /* Error */, "always", 3],
"scope-empty": [2 /* Error */, "always"]
},
settings: {
enableMultipleScopes: false,
disableEmoji: true,
breakingChangePrefix: "\u{1F4A3} ",
closedIssuePrefix: "\u2705 ",
format: "{type}: {emoji}{subject}"
}
};
var config = {
types: DEFAULT_COMMIT_TYPES,
changelogs,
commitlint
};
var minimal_default = config;
// ../conventional-changelog/src/configs/monorepo.ts
var changelogs2 = {
props: {
ignoreCommits: void 0,
types: CHANGELOG_COMMIT_TYPES,
bumpStrict: true,
scope: ["monorepo"],
scopeOnly: true
}
};
var commitlint2 = {
helpUrl: "https://developer.stormsoftware.com/commitlint/monorepo",
rules: {
"body-leading-blank": [1 /* Warning */, "always"],
"body-max-length": [2 /* Error */, "always", 600],
"footer-leading-blank": [1 /* Warning */, "always"],
"footer-max-line-length": [2 /* Error */, "always", 150],
"header-max-length": [2 /* Error */, "always", 150],
"header-trim": [2 /* Error */, "always"],
"subject-case": [2 /* Error */, "always", ["sentence-case"]],
"subject-empty": [2 /* Error */, "never"],
"subject-full-stop": [2 /* Error */, "never", "."],
"subject-max-length": [2 /* Error */, "always", 150],
"subject-min-length": [2 /* Error */, "always", 3],
"type-case": [2 /* Error */, "always", "kebab-case"],
"type-empty": [2 /* Error */, "never"],
"type-enum": [
2 /* Error */,
"always",
Object.keys(DEFAULT_COMMIT_TYPES)
],
"type-max-length": [2 /* Error */, "always", 20],
"type-min-length": [2 /* Error */, "always", 3],
"scope-case": [2 /* Error */, "always", ["kebab-case"]],
"scope-empty": [2 /* Error */, "never"]
},
settings: {
enableMultipleScopes: false,
disableEmoji: true,
breakingChangePrefix: "\u{1F4A3} ",
closedIssuePrefix: "\u2705 ",
format: "{type}({scope}): {emoji}{subject}"
}
};
var config2 = {
types: DEFAULT_COMMIT_TYPES,
changelogs: changelogs2,
commitlint: commitlint2
};
var monorepo_default = config2;
// ../conventional-changelog/src/configs/index.ts
var COMMIT_CONFIGS = { minimal: minimal_default, monorepo: monorepo_default };
async function getNxScopes(context) {
let projectGraph;
try {
projectGraph = readCachedProjectGraph();
} catch {
await createProjectGraphAsync$1();
projectGraph = readCachedProjectGraph();
}
if (!projectGraph) {
throw new Error(
"The commit process failed because the project graph is not available. Please run the build command again."
);
}
const projectConfigs = readProjectsConfigurationFromProjectGraph$1(projectGraph);
const result = Object.entries(projectConfigs.projects || {}).map(([name, project]) => ({
name,
...project
})).filter(
(project) => project.name && project.root && project.root !== "." && project.root !== context.config.workspaceRoot && !project.name.includes("e2e")
).filter((project) => project.targets).map((project) => project.name).filter(Boolean).sort((a, b) => a.localeCompare(b));
result.unshift("monorepo");
return result;
}
function getScopeEnum(context) {
return getNxScopes(context);
}
function getRuleFromScopeEnum(scopeEnum) {
if (!scopeEnum?.filter(Boolean).length) {
throw new Error("No scopes found in the Storm workspace.");
}
return [
2 /* Error */,
"always",
scopeEnum.filter(Boolean)
];
}
// ../conventional-changelog/src/index.ts
async function createPreset(variant = "monorepo") {
const workspaceConfig = await getWorkspaceConfig();
if (variant === "minimal") {
return defu(
await createBasePreset({ ...COMMIT_CONFIGS.minimal.changelogs.props }),
{
...COMMIT_CONFIGS.minimal,
commitlint: {
...COMMIT_CONFIGS.minimal.commitlint,
regex: new RegExp(
`(${Object.keys(DEFAULT_COMMIT_TYPES).join("|")})!?:\\s([a-z0-9:\\-\\/\\s])+`
)
}
}
);
}
const nxScopes = await getNxScopes({ config: workspaceConfig });
return defu(
await createBasePreset({
...COMMIT_CONFIGS.monorepo.changelogs.props,
scope: nxScopes
}),
{
...COMMIT_CONFIGS.monorepo,
commitlint: {
...COMMIT_CONFIGS.monorepo.commitlint,
rules: {
...COMMIT_CONFIGS.monorepo.commitlint.rules,
["scope-enum"]: getRuleFromScopeEnum(nxScopes)
},
regex: new RegExp(
`(${Object.keys(DEFAULT_COMMIT_TYPES).join("|")})\\((${nxScopes.join("|")})\\)!?:\\s([a-z0-9:\\-\\/\\s])+`
)
}
}
);
}
// ../../node_modules/.pnpm/conventional-commits-parser@6.2.1/node_modules/conventional-commits-parser/dist/regex.js
var nomatchRegex = /(?!.*)/;
function join(parts, joiner) {
return parts.map((val) => val.trim()).filter(Boolean).join(joiner);
}
function getNotesRegex(noteKeywords, notesPattern) {
if (!noteKeywords) {
return nomatchRegex;
}
const noteKeywordsSelection = join(noteKeywords, "|");
if (!notesPattern) {
return new RegExp(`^[\\s|*]*(${noteKeywordsSelection})[:\\s]+(.*)`, "i");
}
return notesPattern(noteKeywordsSelection);
}
function getReferencePartsRegex(issuePrefixes, issuePrefixesCaseSensitive) {
if (!issuePrefixes) {
return nomatchRegex;
}
const flags = issuePrefixesCaseSensitive ? "g" : "gi";
return new RegExp(`(?:.*?)??\\s*([\\w-\\.\\/]*?)??(${join(issuePrefixes, "|")})([\\w-]+)(?=\\s|$|[,;)\\]])`, flags);
}
function getReferencesRegex(referenceActions) {
if (!referenceActions) {
return /()(.+)/gi;
}
const joinedKeywords = join(referenceActions, "|");
return new RegExp(`(${joinedKeywords})(?:\\s+(.*?))(?=(?:${joinedKeywords})|$)`, "gi");
}
function getParserRegexes(options = {}) {
const notes = getNotesRegex(options.noteKeywords, options.notesPattern);
const referenceParts = getReferencePartsRegex(options.issuePrefixes, options.issuePrefixesCaseSensitive);
const references = getReferencesRegex(options.referenceActions);
return {
notes,
referenceParts,
references,
mentions: /@([\w-]+)/g,
url: /\b(?:https?):\/\/(?:www\.)?([-a-zA-Z0-9@:%_+.~#?&//=])+\b/
};
}
// ../../node_modules/.pnpm/conventional-commits-parser@6.2.1/node_modules/conventional-commits-parser/dist/utils.js
var SCISSOR = "------------------------ >8 ------------------------";
function trimNewLines(input) {
const matches = input.match(/[^\r\n]/);
if (typeof matches?.index !== "number") {
return "";
}
const firstIndex = matches.index;
let lastIndex = input.length - 1;
while (input[lastIndex] === "\r" || input[lastIndex] === "\n") {
lastIndex--;
}
return input.substring(firstIndex, lastIndex + 1);
}
function appendLine(src, line) {
return src ? `${src}
${line || ""}` : line || "";
}
function getCommentFilter(char) {
return char ? (line) => !line.startsWith(char) : () => true;
}
function truncateToScissor(lines, commentChar) {
const scissorIndex = lines.indexOf(`${commentChar} ${SCISSOR}`);
if (scissorIndex === -1) {
return lines;
}
return lines.slice(0, scissorIndex);
}
function gpgFilter(line) {
return !line.match(/^\s*gpg:/);
}
function assignMatchedCorrespondence(target, matches, correspondence) {
const { groups } = matches;
for (let i = 0, len = correspondence.length, key; i < len; i++) {
key = correspondence[i];
target[key] = (groups ? groups[key] : matches[i + 1]) || null;
}
return target;
}
// ../../node_modules/.pnpm/conventional-commits-parser@6.2.1/node_modules/conventional-commits-parser/dist/options.js
var defaultOptions = {
noteKeywords: ["BREAKING CHANGE", "BREAKING-CHANGE"],
issuePrefixes: ["#"],
referenceActions: [
"close",
"closes",
"closed",
"fix",
"fixes",
"fixed",
"resolve",
"resolves",
"resolved"
],
headerPattern: /^(\w*)(?:\(([\w$@.\-*/ ]*)\))?: (.*)$/,
headerCorrespondence: [
"type",
"scope",
"subject"
],
revertPattern: /^Revert\s"([\s\S]*)"\s*This reverts commit (\w*)\./,
revertCorrespondence: ["header", "hash"],
fieldPattern: /^-(.*?)-$/
};
// ../../node_modules/.pnpm/conventional-commits-parser@6.2.1/node_modules/conventional-commits-parser/dist/CommitParser.js
function createCommitObject(initialData = {}) {
return {
merge: null,
revert: null,
header: null,
body: null,
footer: null,
notes: [],
mentions: [],
references: [],
...initialData
};
}
var CommitParser = class {
options;
regexes;
lines = [];
lineIndex = 0;
commit = createCommitObject();
constructor(options = {}) {
this.options = {
...defaultOptions,
...options
};
this.regexes = getParserRegexes(this.options);
}
currentLine() {
return this.lines[this.lineIndex];
}
nextLine() {
return this.lines[this.lineIndex++];
}
isLineAvailable() {
return this.lineIndex < this.lines.length;
}
parseReference(input, action) {
const { regexes } = this;
if (regexes.url.test(input)) {
return null;
}
const matches = regexes.referenceParts.exec(input);
if (!matches) {
return null;
}
let [raw, repository = null, prefix, issue] = matches;
let owner = null;
if (repository) {
const slashIndex = repository.indexOf("/");
if (slashIndex !== -1) {
owner = repository.slice(0, slashIndex);
repository = repository.slice(slashIndex + 1);
}
}
return {
raw,
action,
owner,
repository,
prefix,
issue
};
}
parseReferences(input) {
const { regexes } = this;
const regex = input.match(regexes.references) ? regexes.references : /()(.+)/gi;
const references = [];
let matches;
let action;
let sentence;
let reference;
while (true) {
matches = regex.exec(input);
if (!matches) {
break;
}
action = matches[1] || null;
sentence = matches[2] || "";
while (true) {
reference = this.parseReference(sentence, action);
if (!reference) {
break;
}
references.push(reference);
}
}
return references;
}
skipEmptyLines() {
let line = this.currentLine();
while (line !== void 0 && !line.trim()) {
this.nextLine();
line = this.currentLine();
}
}
parseMerge() {
const { commit, options } = this;
const correspondence = options.mergeCorrespondence || [];
const merge = this.currentLine();
const matches = merge && options.mergePattern ? merge.match(options.mergePattern) : null;
if (matches) {
this.nextLine();
commit.merge = matches[0] || null;
assignMatchedCorrespondence(commit, matches, correspondence);
return true;
}
return false;
}
parseHeader(isMergeCommit) {
if (isMergeCommit) {
this.skipEmptyLines();
}
const { commit, options } = this;
const correspondence = options.headerCorrespondence || [];
const header = commit.header ?? this.nextLine();
let matches = null;
if (header) {
if (options.breakingHeaderPattern) {
matches = header.match(options.breakingHeaderPattern);
}
if (!matches && options.headerPattern) {
matches = header.match(options.headerPattern);
}
}
if (header) {
commit.header = header;
}
if (matches) {
assignMatchedCorrespondence(commit, matches, correspondence);
}
}
parseMeta() {
const { options, commit } = this;
if (!options.fieldPattern || !this.isLineAvailable()) {
return false;
}
let matches;
let field = null;
let parsed = false;
while (this.isLineAvailable()) {
matches = this.currentLine().match(options.fieldPattern);
if (matches) {
field = matches[1] || null;
this.nextLine();
continue;
}
if (field) {
parsed = true;
commit[field] = appendLine(commit[field], this.currentLine());
this.nextLine();
} else {
break;
}
}
return parsed;
}
parseNotes() {
const { regexes, commit } = this;
if (!this.isLineAvailable()) {
return false;
}
const matches = this.currentLine().match(regexes.notes);
let references = [];
if (matches) {
const note = {
title: matches[1],
text: matches[2]
};
commit.notes.push(note);
commit.footer = appendLine(commit.footer, this.currentLine());
this.nextLine();
while (this.isLineAvailable()) {
if (this.parseMeta()) {
return true;
}
if (this.parseNotes()) {
return true;
}
references = this.parseReferences(this.currentLine());
if (references.length) {
commit.references.push(...references);
} else {
note.text = appendLine(note.text, this.currentLine());
}
commit.footer = appendLine(commit.footer, this.currentLine());
this.nextLine();
if (references.length) {
break;
}
}
return true;
}
return false;
}
parseBodyAndFooter(isBody) {
const { commit } = this;
if (!this.isLineAvailable()) {
return isBody;
}
const references = this.parseReferences(this.currentLine());
const isStillBody = !references.length && isBody;
if (isStillBody) {
commit.body = appendLine(commit.body, this.currentLine());
} else {
commit.references.push(...references);
commit.footer = appendLine(commit.footer, this.currentLine());
}
this.nextLine();
return isStillBody;
}
parseBreakingHeader() {
const { commit, options } = this;
if (!options.breakingHeaderPattern || commit.notes.length || !commit.header) {
return;
}
const matches = commit.header.match(options.breakingHeaderPattern);
if (matches) {
commit.notes.push({
title: "BREAKING CHANGE",
text: matches[3]
});
}
}
parseMentions(input) {
const { commit, regexes } = this;
let matches;
for (; ; ) {
matches = regexes.mentions.exec(input);
if (!matches) {
break;
}
commit.mentions.push(matches[1]);
}
}
parseRevert(input) {
const { commit, options } = this;
const correspondence = options.revertCorrespondence || [];
const matches = options.revertPattern ? input.match(options.revertPattern) : null;
if (matches) {
commit.revert = assignMatchedCorrespondence({}, matches, correspondence);
}
}
cleanupCommit() {
const { commit } = this;
if (commit.body) {
commit.body = trimNewLines(commit.body);
}
if (commit.footer) {
commit.footer = trimNewLines(commit.footer);
}
commit.notes.forEach((note) => {
note.text = trimNewLines(note.text);
});
}
/**
* Parse commit message string into an object.
* @param input - Commit message string.
* @returns Commit object.
*/
parse(input) {
if (!input.trim()) {
throw new TypeError("Expected a raw commit");
}
const { commentChar } = this.options;
const commentFilter = getCommentFilter(commentChar);
const rawLines = trimNewLines(input).split(/\r?\n/);
const lines = commentChar ? truncateToScissor(rawLines, commentChar).filter((line) => commentFilter(line) && gpgFilter(line)) : rawLines.filter((line) => gpgFilter(line));
const commit = createCommitObject();
this.lines = lines;
this.lineIndex = 0;
this.commit = commit;
const isMergeCommit = this.parseMerge();
this.parseHeader(isMergeCommit);
if (commit.header) {
commit.references = this.parseReferences(commit.header);
}
let isBody = true;
while (this.isLineAvailable()) {
this.parseMeta();
if (this.parseNotes()) {
isBody = false;
}
if (!this.parseBodyAndFooter(isBody)) {
isBody = false;
}
}
this.parseBreakingHeader();
this.parseMentions(input);
this.parseRevert(input);
this.cleanupCommit();
return commit;
}
};
var buildCommitMessage = ({
header,
body,
footer
}) => {
let message = header;
message = body ? `${message}
${body}` : message;
message = footer ? `${message}
${footer}` : message;
return message || "";
};
async function lint(message, config5) {
const parser = new CommitParser(config5.parser || {});
const parsed = parser.parse(message);
if (parsed.header === null && parsed.body === null && parsed.footer === null) {
return {
valid: true,
errors: [],
warnings: [],
input: message
};
}
const allRules = new Map(Object.entries(defaultRules));
const missing = Object.keys(config5.commitlint.rules).filter(
(name) => typeof allRules.get(name) !== "function"
);
if (missing.length > 0) {
const names = [...allRules.keys()];
throw new RangeError(
[
`Found rules without implementation: ${missing.join(", ")}.`,
`Supported rules are: ${names.join(", ")}.`
].join("\n")
);
}
const invalid = Object.entries(config5.commitlint.rules).map(([name, config6]) => {
if (!Array.isArray(config6)) {
return new Error(
`config for rule ${name} must be array, received ${util.inspect(
config6
)} of type ${typeof config6}`
);
}
const [level] = config6;
if (level === 0 /* Disabled */ && config6.length === 1) {
return null;
}
const [, when] = config6;
if (typeof level !== "number" || isNaN(level)) {
return new Error(
`level for rule ${name} must be number, received ${util.inspect(
level
)} of type ${typeof level}`
);
}
if (config6.length < 2 || config6.length > 3) {
return new Error(
`config for rule ${name} must be 2 or 3 items long, received ${util.inspect(
config6
)} of length ${config6.length}`
);
}
if (level < 0 || level > 2) {
return new RangeError(
`level for rule ${name} must be between 0 and 2, received ${util.inspect(
level
)}`
);
}
if (typeof when !== "string") {
return new Error(
`condition for rule ${name} must be string, received ${util.inspect(
when
)} of type ${typeof when}`
);
}
if (when !== "never" && when !== "always") {
return new Error(
`condition for rule ${name} must be "always" or "never", received ${util.inspect(
when
)}`
);
}
return null;
}).filter((item) => item instanceof Error);
if (invalid.length > 0) {
throw new Error(invalid.map((i) => i.message).join("\n"));
}
const pendingResults = Object.entries(
config5.commitlint.rules
).filter(([, config6]) => !!config6 && config6.length && config6[0] > 0).map(async (entry) => {
const [name, config6] = entry;
const [level, when, value] = config6;
const rule = allRules.get(name);
if (!rule) {
throw new Error(`Could not find rule implementation for ${name}`);
}
const executableRule = rule;
const [valid2, message2] = await executableRule(parsed, when, value);
return {
level,
valid: valid2,
name,
message: message2
};
});
const results = (await Promise.all(pendingResults)).filter(
(result) => result !== null
);
const errors = results.filter(
(result) => result.level === 2 /* Error */ && !result.valid
);
const warnings = results.filter(
(result) => result.level === 1 /* Warning */ && !result.valid
);
const valid = errors.length === 0;
return {
valid,
errors,
warnings,
input: buildCommitMessage(parsed)
};
}
// src/commitlint/run.ts
var COMMIT_EDITMSG_PATH = ".git/COMMIT_EDITMSG";
async function runCommitLint(workspaceConfig, options) {
writeInfo(
"\u{1F4DD} Validating git commit message aligns with the Storm Software specification",
workspaceConfig
);
let commitMessage;
if (options.message && options.message !== COMMIT_EDITMSG_PATH) {
commitMessage = options.message;
} else {
const commitFile = joinPaths(
workspaceConfig.workspaceRoot,
options.file || options.message || COMMIT_EDITMSG_PATH
);
if (existsSync(commitFile)) {
commitMessage = (await readFile(commitFile, "utf8"))?.trim();
}
}
if (!commitMessage) {
let gitLogCmd = "git log -1 --no-merges";
const gitRemotes = childProcess.execSync("git remote -v").toString().trim().split("\n");
const upstreamRemote = gitRemotes.find(
(remote) => remote.includes(`${workspaceConfig.name}.git`)
);
if (upstreamRemote) {
const upstreamRemoteIdentifier = upstreamRemote.split(" ")[0]?.trim();
if (!upstreamRemoteIdentifier) {
writeWarning(
`No upstream remote found for ${workspaceConfig.name}.git. Skipping comparison.`,
workspaceConfig
);
return;
}
writeDebug(`Comparing against remote ${upstreamRemoteIdentifier}`);
const currentBranch = childProcess.execSync("git branch --show-current").toString().trim();
gitLogCmd = gitLogCmd + ` ${currentBranch} ^${upstreamRemoteIdentifier}/main`;
} else {
writeWarning(
`No upstream remote found for ${workspaceConfig.name}.git. Skipping comparison against upstream main.`,
workspaceConfig
);
return;
}
commitMessage = childProcess.execSync(gitLogCmd).toString().trim();
if (!commitMessage) {
writeWarning(
"No commits found. Skipping commit message validation.",
workspaceConfig
);
return;
}
}
const preset = await createPreset(workspaceConfig.variant);
const report = await lint(commitMessage, preset);
if (!preset.commitlint.regex.test(commitMessage) || report.errors.length || report.warnings.length) {
writeSuccess(
`Commit was processing completed successfully!`,
workspaceConfig
);
} else {
let errorMessage = " Oh no! Your commit message: \n-------------------------------------------------------------------\n" + commitMessage + `
-------------------------------------------------------------------
Does not follow the \`${workspaceConfig.variant}\` commit message convention specified by the ${(typeof workspaceConfig.organization === "string" ? workspaceConfig.organization : workspaceConfig.organization?.name) || "Storm Software"} team.`;
errorMessage += preset.changelogs.props.scope?.length ? "\ntype(scope): subject \n BLANK LINE \n body" : "\ntype: subject \n BLANK LINE \n body";
errorMessage += "\n";
errorMessage += `
Possible types: ${preset.changelogs.props.types.map(
(type) => `${type.section} (${type.type})`
)}`;
if (preset.changelogs.props.scope?.length) {
errorMessage += `
Possible scopes: ${preset.changelogs.props.scope} (if unsure use "monorepo")`;
}
errorMessage += "\n\nEXAMPLE: \nfeat(my-lib): add an option to generate lazy-loadable modules\nfix(monorepo)!: breaking change should have exclamation mark\n";
errorMessage += `
CommitLint Errors: ${report.errors.length ? report.errors.map((error) => ` - ${error.message}`).join("\n") : "None"}`;
errorMessage += `
CommitLint Warnings: ${report.warnings.length ? report.warnings.map((warning) => ` - ${warning.message}`).join("\n") : "None"}`;
errorMessage += "\n\nPlease fix the commit message and rerun storm-commit.";
errorMessage += `
More details about the Storm Software commit message specification can be found at: ${preset.commitlint.helpUrl}`;
throw new Error(errorMessage);
}
return report.input;
}
// src/types.ts
var DEFAULT_COMMIT_PROMPT_MESSAGES = {
skip: "press enter to skip",
max: "must be %d chars at most",
min: "must be %d chars at least",
emptyWarning: "can not be empty",
upperLimitWarning: "%s is %d characters longer than the upper limit",
lowerLimitWarning: "%s is %d characters less than the lower limit",
closedIssueMessage: "Closes: "
};
// src/commit/config/minimal.ts
var DEFAULT_MINIMAL_COMMIT_QUESTIONS = {
type: {
type: "select",
title: "Commit Type",
description: "Select the commit type that best describes your changes",
enum: Object.keys(DEFAULT_COMMIT_TYPES).filter(
(type) => DEFAULT_COMMIT_TYPES[type].hidden !== true
).reduce((ret, type) => {
ret[type] = DEFAULT_COMMIT_TYPES[type];
return ret;
}, {}),
defaultValue: "chore",
maxLength: 20,
minLength: 3
},
subject: {
type: "input",
title: "Commit Subject",
description: "Write a short, imperative tense description of the change",
maxLength: 150,
minLength: 3
},
body: {
type: "input",
title: "Commit Body",
description: "Provide a longer description of the change",
maxLength: 600
},
isBreaking: {
type: "confirm",
title: "Breaking Changes",
description: "Are there any breaking changes as a result of this commit?",
defaultValue: false
},
breakingBody: {
type: "input",
title: "Breaking Changes (Details)",
description: "A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself",
when: (answers) => answers.isBreaking === true,
maxLength: 600,
minLength: 3
},
isIssueAffected: {
type: "confirm",
title: "Open Issue Affected",
description: "Does this change impact any open issues?",
defaultValue: false
},
issuesBody: {
type: "input",
title: "Open Issue Affected (Details)",
description: "If issues are closed, the commit requires a body. Please enter a longer description of the commit itself",
when: (answers) => answers.isIssueAffected === true,
maxLength: 600,
minLength: 3
}
};
var config3 = {
settings: COMMIT_CONFIGS.minimal.commitlint.settings,
messages: DEFAULT_COMMIT_PROMPT_MESSAGES,
questions: DEFAULT_MINIMAL_COMMIT_QUESTIONS,
types: DEFAULT_COMMIT_TYPES
};
var minimal_default2 = config3;
// src/commit/config/monorepo.ts
var DEFAULT_MONOREPO_COMMIT_QUESTIONS = {
type: DEFAULT_MINIMAL_COMMIT_QUESTIONS.type,
scope: {
type: "select",
title: "Commit Scope",
description: "Select the project that's the most impacted by this change",
enum: {},
defaultValue: "monorepo",
maxLength: 50,
minLength: 1
},
subject: DEFAULT_MINIMAL_COMMIT_QUESTIONS.subject,
body: DEFAULT_MINIMAL_COMMIT_QUESTIONS.body,
isBreaking: DEFAULT_MINIMAL_COMMIT_QUESTIONS.isBreaking,
breakingBody: DEFAULT_MINIMAL_COMMIT_QUESTIONS.breakingBody,
isIssueAffected: DEFAULT_MINIMAL_COMMIT_QUESTIONS.isIssueAffected,
issuesBody: DEFAULT_MINIMAL_COMMIT_QUESTIONS.issuesBody
};
var config4 = {
settings: COMMIT_CONFIGS.monorepo.commitlint.settings,
messages: DEFAULT_COMMIT_PROMPT_MESSAGES,
questions: DEFAULT_MONOREPO_COMMIT_QUESTIONS,
types: DEFAULT_COMMIT_TYPES
};
var monorepo_default2 = config4;
// src/commit/commit-state.ts
function getGitDir() {
const devNull = process.platform === "win32" ? " nul" : "/dev/null";
const dir = execSync(`git rev-parse --absolute-git-dir 2>${devNull}`).toString().trim();
return dir;
}
function getGitRootDir() {
const devNull = process.platform === "win32" ? " nul" : "/dev/null";
const dir = execSync(`git rev-parse --show-toplevel 2>${devNull}`).toString().trim();
return dir;
}
async function createState(workspaceConfig, configPath) {
let root;
try {
root = getGitRootDir();
} catch (_2) {
throw new Error("Could not find Git root folder.");
}
const state = {
variant: workspaceConfig.variant,
config: workspaceConfig.variant === "minimal" ? minimal_default2 : monorepo_default2,
root,
answers: {}
};
if (state.config.questions.type && state.config.questions.type.enum) {
state.config.questions.type.enum = Object.keys(
state.config.questions.type.enum
).reduce((ret, key) => {
if (state.config.questions.type.enum) {
ret[key] = {
...state.config.questions.type.enum[key],
title: chalkTemplate`${state.config.questions.type.enum[key]?.emoji ? `${state.config.questions.type.enum[key]?.emoji} ` : ""}{bold ${key}} ${state.config.questions.type.enum[key]?.title && state.config.questions.type.enum[key]?.title !== key ? `- ${state.config.questions.type.enum[key]?.title}` : ""}${state.config.questions.type.enum[key]?.semverBump ? ` (version bump: ${state.config.questions.type.enum[key]?.semverBump})` : ""}`,
hidden: false
};
}
return ret;
}, {});
}
if (workspaceConfig.variant === "monorepo" && (!state.config.questions?.scope || !state.config.questions?.scope.enum || Object.keys(
state.config.questions?.scope.enum
).length === 0)) {
const scopes = await getScopeEnum({
config: workspaceConfig
});
for (const scope of scopes) {
if (scope === "monorepo") {
state.config.questions.scope.enum[scope] = {
title: chalkTemplate`{bold monorepo} - workspace root`,
description: "The base workspace package (workspace root)",
hidden: false,
projectRoot: "/"
};
} else {
let projectGraph;
try {
projectGraph = readCachedProjectGraph();
} catch {
await createProjectGraphAsync$1();
projectGraph = readCachedProjectGraph();
}
if (!projectGraph) {
throw new Error(
"Failed to load the project graph. Please run `nx reset`, then run the `storm-git commit` command again."
);
}
const projectConfigurations = readProjectsConfigurationFromProjectGraph$1(projectGraph);
if (!projectConfigurations?.projects?.[scope]) {
throw new Error(
`Failed to load the project configuration for project ${scope}. Please run \`nx reset\`, then run the \`storm-git commit\` command again.`
);
}
const project = projectConfigurations.projects[scope];
if (project) {
let description = `${project.name} - ${project.root}`;
const packageJsonPath = joinPaths(project.root, "package.json");
if (existsSync$1(packageJsonPath)) {
const packageJsonFile = await readFile$1(packageJsonPath, "utf8");
const packageJson = JSON.parse(packageJsonFile);
description = packageJson.description || description;
}
state.config.questions.scope.enum[scope] = {
title: chalkTemplate`{bold ${project.name}} - ${project.root}`,
description,
hidden: false,
projectRoot: project.root
};
}
}
}
}
state.answers = Object.keys(state.config.questions).reduce(
(ret, key) => {
ret[key] = "";
return ret;
},
{}
);
return state;
}
var MAX_LINE_WIDTH = 72;
var formatCommitMessage = (state, workspaceConfig) => {
const { config: config5, answers } = state;
const wrapOptions = {
indent: "",
trim: true,
width: MAX_LINE_WIDTH
};
if (typeof answers.type !== "string") {
throw new Error("Invalid commit type.");
}
if (typeof answers.subject !== "string") {
throw new Error("Invalid subject type.");
}
if (workspaceConfig.variant !== "minimal" && typeof answers.scope !== "string") {
throw new Error("Invalid commit scope.");
}
const emoji = answers.type?.[answers.type]?.emoji ? answers.type[answers.type].emoji : "";
const scope = workspaceConfig.variant !== "minimal" && typeof answers.scope === "string" && answers.scope ? answers.scope.trim() : "";
const subject = answers.subject?.trim();
const type = answers.type;
const format2 = config5.settings.format || (workspaceConfig.variant !== "minimal" ? "{type}({scope}): {emoji}{subject}" : "{type}: {emoji}{subject}");
const body = answers.body && typeof answers.body === "string" ? wrap(answers.body || "", wrapOptions) : "";
const breaking = answers.breakingBody && typeof answers.breakingBody === "string" ? wrap(answers.breakingBody || "", wrapOptions) : "";
const issues = answers.issuesBody && typeof answers.issuesBody === "string" ? wrap(answers.issuesBody || "", wrapOptions) : "";
const head = format2.replace(/\{emoji\}/g, config5.settings.disableEmoji ? "" : `${emoji} `).replace(/\{scope\}/g, scope).replace(/\{subject\}/g, subject || "").replace(/\{type\}/g, type || "");
let msg = head;
if (body) {
msg += `
${body}`;
}
if (breaking) {
const breakingEmoji = config5.settings.disableEmoji ? "" : config5.settings.breakingChangePrefix;
msg += `
BREAKING CHANGE: ${breakingEmoji}${breaking}`;
}
if (issues) {
const closedIssueEmoji = config5.settings.disableEmoji ? "" : config5.settings.closedIssuePrefix;
msg += `
${closedIssueEmoji}${config5.settings.closedIssueMessage}${issues}`;
}
return msg;
};
// src/commit/run.ts
async function runCommit(commitizenFile, dryRun = false) {
const workspaceConfig = await getWorkspaceConfig();
const state = await createState(workspaceConfig);
if (dryRun) {
writeInfo("Running in dry mode.", workspaceConfig);
}
console.log(chalkTemplate`
{bold.#999999 ----------------------------------------}
{bold.#FFFFFF ⚡ Storm Software Git-Tools - Commit}
{#CCCCCC Please provide the requested details below...}
`);
state.answers = await askQuestions(state);
const message = formatCommitMessage(state, workspaceConfig);
const commitMsgFile = joinPaths(getGitDir(), "COMMIT_EDITMSG");
console.log(chalkTemplate`
{bold.#999999 ----------------------------------------}
{bold.#FFFFFF Commit message} - {#DDDDDD ${message}}
{bold.#FFFFFF Git-Commit File} - {#DDDDDD ${commitMsgFile}}
`);
await runCommitLint(workspaceConfig, { message });
const commandItems = ["git", "commit", "-S"];
commandItems.push(...["--file", commitMsgFile]);
const command = shellescape(commandItems);
if (dryRun) {
writeDebug(
`Skipping execution [dry-run]: ${command.replace(commitMsgFile, ".git/COMMIT_EDITMSG")}`,
workspaceConfig
);
writeDebug(`Message [dry-run]: ${message}`, workspaceConfig);
} else {
await fs.writeFile(commitMsgFile, message);
run(workspaceConfig, command);
}
}
async function askQuestions(state) {
let index = 0;
for (const key of Object.keys(state.config.questions)) {
if (state.config.questions[key] && !state.config.questions[key].hidden && (!state.config.questions[key].when || state.config.questions[key].when(state.answers))) {
state.answers[key] = await askQuestion(
index,
state.config.questions[key]
);
index++;
}
}
return state.answers;
}
async function askQuestion(index, question) {
const message = chalkTemplate`{bold ${index + 1}. ${question.title}} - ${question.description}
`;
if (question.type === "select" && question.enum && Object.keys(question.enum).length > 1) {
return default11({
message,
choices: Object.keys(question.enum).filter((key) => !question.enum?.[key]?.hidden).map((key) => ({
name: question.enum?.[key]?.title || key,
value: key,
description: question.enum?.[key]?.description || ""
})),
default: String(question.defaultValue || "")
});
} else if (question.type === "confirm") {
return default4({
message,
default: Boolean(question.defaultValue)
});
} else {
let validate = void 0;
if (question.minLength !== void 0 || question.maxLength !== void 0) {
validate = (value) => {
if (question.minLength !== void 0 && value.length < question.minLength) {
return `Minimum length is ${question.minLength} characters.`;
}
if (question.maxLength !== void 0 && value.length > question.maxLength) {
return `Maximum length is ${question.maxLength} characters.`;
}
return true;
};
}
return default5({
message,
required: !!(question.minLength !== void 0 && question.minLength > 0),
default: String(question.defaultValue || ""),
validate
});
}
}
function findFileName(filePath) {
return filePath?.split(
filePath?.includes(Path.sep) ? Path.sep : filePath?.includes("/") ? "/" : "\\"
)?.pop() ?? "";
}
function findFilePath(filePath) {
return filePath.replace(findFileName(filePath), "");
}
function createFileToProjectMap(projectFileMap) {
const fileToProjectMap = {};
for (const [projectName, projectFiles] of Object.entries(projectFileMap)) {
for (const file of projectFiles) {
fileToProjectMap[file.file] = projectName;
}
}
return fileToProjectMap;
}
var start = "<!-- START doctoc -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->";
var end = "<!-- END doctoc -->";
var skipTag = "<!-- DOCTOC SKIP -->";
function matchesStart(line) {
return /<!-- START doctoc /.test(line);
}
function matchesEnd(line) {
return /<!-- END doctoc /.test(line);
}
function notNull(x) {
return x !== null;
}
function addAnchor(mode, header) {
header.anchor = anchor(header.name, mode, header.instance);
return header;
}
function getMarkdownHeaders(lines, maxHeaderLevel) {
function extractText(header) {
return header.children.map(function(x) {
if (x.type === Syntax.Link) {
return extractText(x);
} else if (x.type === Syntax.Image) {
return "*";
} else {
return x.raw;
}
}).join("");
}
return parse$1(lines.join("\n")).children.filter(function(x) {
return x.type === Syntax.Header;
}).map(function(x) {
return !maxHeaderLevel || x.depth <= maxHeaderLevel ? { rank: x.depth, name: extractText(x), line: x.loc.start.line } : null;
}).filter(notNull);
}
function countHeaders(headers) {
const instances = {};
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
const name = header.name;
if (Object.prototype.hasOwnProperty.call(instances, name)) {
instances[name]++;
} else {
instances[name] = 0;
}
header.instance = instances[name];
}
return headers;
}
function getLinesToToc(lines, currentToc, info, processAll) {
if (processAll || !currentToc) return lines;
let tocableStart = 0;
if (info.hasEnd) tocableStart = info.endIdx + 1;
return lines.slice(tocableStart);
}
function determineTitle(title, notitle, lines, info) {
const defaultTitle = "**Table of Contents** ";
if (notitle) return "";
if (title) return title;
return info.hasStart ? lines[info.startIdx + 2] : defaultTitle;
}
function transform(content, mode, maxHeaderLevel, title, notitle, entryPrefix, processAll, updateOnly) {
if (content.indexOf(skipTag) !== -1) return { transformed: false };
mode = mode || "github.com";
entryPrefix = entryPrefix || "-";
const maxHeaderLevelHtml = maxHeaderLevel || 4;
const lines = content.split("\n"), info = updateSection.parse(lines, matchesStart, matchesEnd);
if (!info.hasStart && updateOnly) {
return { transformed: false };
}
const inferredTitle = determineTitle(title, notitle, lines, info);
const titleSeparator = inferredTitle ? "\n\n" : "\n";
const currentToc = info.hasStart && lines.slice(info.startIdx, info.endIdx + 1).join("\n"), linesToToc = getLinesToToc(lines, currentToc, info, processAll);
const headers = getMarkdownHeaders(linesToToc, maxHeaderLevel).concat(
getHtmlHeaders(linesToToc, maxHeaderLevelHtml)
);
headers.sort((a, b) => {
if (!a && b) {
return -1;
}
if (a && !b) {
return 1;
}
if (!a && !b) {
return 0;
}
return a.line - b.line;
});
const allHeaders = countHeaders(headers), lowestRank = _(allHeaders).chain().pluck("rank").min().value(), linkedHeaders = _(allHeaders).map(addAnchor.bind(null, mode));
if (linkedHeaders.length === 0) return { transformed: false };
const indentation = mode === "bitbucket.org" || mode === "gitlab.com" ? " " : " ";
const toc = inferredTitle + titleSeparator + linkedHeaders.map(function(x) {
const indent = _(_.range(x.rank - lowestRank)).reduce(function(acc, x2) {
return acc + indentation;
}, "");
return indent + en