UNPKG

@storm-software/git-tools

Version:

Tools for managing Git repositories within a Nx workspace.

1,489 lines (1,461 loc) 133 kB
#!/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