Compare commits
	
		
			14 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1bce93a24e | |||
| f767b846d8 | |||
| 0cac22a7e0 | |||
| 6df1021561 | |||
| 87edf8cfad | |||
| 47a8b05856 | |||
| b6d749beae | |||
| 87a7c4dccb | |||
| 6ce6e88d80 | |||
| 7b499d7638 | |||
| 4b0ce0fd3a | |||
| 778175a2f3 | |||
| 6e63867ca6 | |||
| 539168195e | 
| @@ -3,20 +3,20 @@ const { execSync } = require("child_process"); | ||||
|  | ||||
| const packageFile = fs.readFileSync("./package.json").toString(); | ||||
| const newPackageFile = packageFile.replace( | ||||
|   /"version": "[0-9]+.[0-9]+.[0-9]+"/, | ||||
|   `"version": "${__NEW_VERSION__}"` | ||||
|   /"version": "[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc|dev)\.[0-9]+)?"/, | ||||
|   `"version": "${__NEW_VERSION__}"`, | ||||
| ); | ||||
|  | ||||
| fs.writeFileSync("./package.json", newPackageFile); | ||||
|  | ||||
| const indexFile = fs.readFileSync("./src/index.ts").toString(); | ||||
| const newIndexFile = indexFile.replace( | ||||
|   /version\("[0-9]+\.[0-9]+\.[0-9]+"\)/, | ||||
|   `version("${__NEW_VERSION__}")` | ||||
|   /version\("[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc|dev)\.[0-9]+)?"\)/, | ||||
|   `version("${__NEW_VERSION__}")`, | ||||
| ); | ||||
|  | ||||
| fs.writeFileSync("./src/index.ts", newIndexFile); | ||||
|  | ||||
| execSync( | ||||
|   `rcz changelog --show-hashes --unreleased-as v${__NEW_VERSION__} > CHANGELOG.md` | ||||
|   `rcz changelog --show-hashes --unreleased-as v${__NEW_VERSION__} > CHANGELOG.md`, | ||||
| ); | ||||
|   | ||||
							
								
								
									
										28
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,7 +1,35 @@ | ||||
| # Changelog | ||||
| Generation of this changelog is based on commits | ||||
| ## v1.14.0 | ||||
| ### Features | ||||
| - [f767b846d] - **commands**: add ability to choose files to stage (#22) | ||||
| ### Miscellaneous | ||||
| - [0cac22a7e] - **changelog**: improve generation time by ~2800 times (#23) | ||||
| ## v1.13.1 | ||||
| ### Fixes | ||||
| - [87edf8cfa] - **changelog**: wrong semver sequence generation (#21) | ||||
| ### Miscellaneous | ||||
| - [6df102156] - **release**: v1.13.1 | ||||
| - [47a8b0585] - **commands**: change head error message | ||||
| - [b6d749bea] - fix version regex to include pre-releases | ||||
| ## v1.13.0 | ||||
| ### Fixes | ||||
| - [7b499d763] - **commands**: commit crash upon missing HEAD (#19) | ||||
| ### Miscellaneous | ||||
| - [87a7c4dcc] - **release**: v1.13.0 | ||||
| - [6ce6e88d8] - Merge pull request 'Refactor into multiple command files' (#20) from refactor/#18 into main | ||||
| ## v1.13.0-rc.0 | ||||
| ### Miscellaneous | ||||
| - [4b0ce0fd3] - **release**: v1.13.0-rc.0 | ||||
| - [778175a2f] - split commands into files (#18) | ||||
| ## v1.12.4 | ||||
| ### Fixes | ||||
| - [539168195] - incorrect commander dependency type | ||||
| ### Miscellaneous | ||||
| - [6e63867ca] - **release**: v1.12.4 | ||||
| ## v1.12.3 | ||||
| ### Miscellaneous | ||||
| - [673b02052] - **release**: v1.12.3 | ||||
| - [5e6900e70] - downgrade commander to v10 | ||||
| ## v1.12.2 | ||||
| ### Fixes | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "@resultium/rcz", | ||||
|   "version": "1.12.3", | ||||
|   "version": "1.14.0", | ||||
|   "license": "GPL-3.0-or-later", | ||||
|   "description": "Resultium commit standardization library, inspired by conventional commits", | ||||
|   "main": "./dist/index.js", | ||||
| @@ -31,13 +31,13 @@ | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@clack/prompts": "^0.7.0", | ||||
|     "commander": "10.0.1", | ||||
|     "semver": "^7.5.4", | ||||
|     "simple-git": "^3.21.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@types/node": "^20.10.4", | ||||
|     "@types/semver": "^7.5.6", | ||||
|     "commander": "^10.0.1", | ||||
|     "typescript": "^5.3.3" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										8
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -8,6 +8,9 @@ dependencies: | ||||
|   '@clack/prompts': | ||||
|     specifier: ^0.7.0 | ||||
|     version: 0.7.0 | ||||
|   commander: | ||||
|     specifier: 10.0.1 | ||||
|     version: 10.0.1 | ||||
|   semver: | ||||
|     specifier: ^7.5.4 | ||||
|     version: 7.5.4 | ||||
| @@ -22,9 +25,6 @@ devDependencies: | ||||
|   '@types/semver': | ||||
|     specifier: ^7.5.6 | ||||
|     version: 7.5.6 | ||||
|   commander: | ||||
|     specifier: ^10.0.1 | ||||
|     version: 10.0.1 | ||||
|   typescript: | ||||
|     specifier: ^5.3.3 | ||||
|     version: 5.3.3 | ||||
| @@ -73,7 +73,7 @@ packages: | ||||
|   /commander@10.0.1: | ||||
|     resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} | ||||
|     engines: {node: '>=14'} | ||||
|     dev: true | ||||
|     dev: false | ||||
|  | ||||
|   /debug@4.3.4: | ||||
|     resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} | ||||
|   | ||||
							
								
								
									
										190
									
								
								src/commands/changelog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/commands/changelog.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | ||||
| /* | ||||
| Copyright 2024 Resultium LLC | ||||
|  | ||||
| This file is part of RCZ. | ||||
|  | ||||
| RCZ is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU General Public License as published by | ||||
| the Free Software Foundation, either version 3 of the License, or | ||||
| any later version. | ||||
|  | ||||
| RCZ is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| GNU General Public License for more details. | ||||
|  | ||||
| You should have received a copy of the GNU General Public License | ||||
| along with RCZ. If not, see <https://www.gnu.org/licenses/>. | ||||
| */ | ||||
|  | ||||
| import { Command } from "commander"; | ||||
| import simpleGit from "simple-git"; | ||||
| import { CommitStack } from "../types"; | ||||
| import { gt, sort } from "semver"; | ||||
|  | ||||
| const command = new Command("changelog") | ||||
|   .alias("ch") | ||||
|   .description("Outputs a markdown formatted changelog") | ||||
|   .option("--show-hashes", "show first 9 characters of commit hashes") | ||||
|   .option("--last-only", "display only latest release changes") | ||||
|   .option( | ||||
|     "--unreleased-as <version>", | ||||
|     "show unreleased changes as different version", | ||||
|   ) | ||||
|   .action(async (options) => { | ||||
|     const showHashes = options.showHashes ? true : false; | ||||
|     const lastOnly = options.lastOnly ? true : false; | ||||
|     const unreleased = options.unreleasedAs || "Unreleased"; | ||||
|  | ||||
|     if ((await simpleGit().tags()).all.length === 0) { | ||||
|       return console.log( | ||||
|         "[rcz]: not even one release has yet been made, cannot make a changelog", | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const commits = (await simpleGit().log()).all; | ||||
|     let lastTag = ""; | ||||
|     let parsedCommitStacks: Array<CommitStack> = []; | ||||
|  | ||||
|     console.log("# Changelog"); | ||||
|     console.log("Generation of this changelog is based on commits"); | ||||
|  | ||||
|     for (const commit of commits) { | ||||
|       const tag = | ||||
|         (commit.refs.match(/tag: (\S+)(?:,|$)/)?.[1] ?? lastTag) || unreleased; | ||||
|  | ||||
|       let currentCommitStackIndex = parsedCommitStacks.findIndex( | ||||
|         (commitStack) => commitStack.version === tag, | ||||
|       ); | ||||
|  | ||||
|       if (currentCommitStackIndex === -1) { | ||||
|         parsedCommitStacks.push({ | ||||
|           version: tag || unreleased, | ||||
|           breaking: [], | ||||
|           features: [], | ||||
|           fixes: [], | ||||
|           miscellaneous: [], | ||||
|         }); | ||||
|  | ||||
|         currentCommitStackIndex = parsedCommitStacks.findIndex( | ||||
|           (commitStack) => commitStack.version === tag, | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       if (commit.message.includes("!:")) { | ||||
|         parsedCommitStacks[currentCommitStackIndex].breaking.push(commit); | ||||
|       } else if (commit.message.startsWith("feat")) { | ||||
|         parsedCommitStacks[currentCommitStackIndex].features.push(commit); | ||||
|       } else if (commit.message.startsWith("fix")) { | ||||
|         parsedCommitStacks[currentCommitStackIndex].fixes.push(commit); | ||||
|       } else { | ||||
|         parsedCommitStacks[currentCommitStackIndex].miscellaneous.push(commit); | ||||
|       } | ||||
|  | ||||
|       lastTag = tag; | ||||
|     } | ||||
|  | ||||
|     // Might be confusing so I will leave mdn docs here | ||||
|     // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort | ||||
|     parsedCommitStacks = parsedCommitStacks.sort((a, b) => { | ||||
|       if (a.version === unreleased) { | ||||
|         return -1; | ||||
|       } else if (b.version === unreleased) { | ||||
|         return 1; | ||||
|       } else { | ||||
|         return gt(a.version, b.version) ? -1 : 1; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     if (lastOnly) { | ||||
|       parsedCommitStacks = [parsedCommitStacks[0]]; | ||||
|     } | ||||
|  | ||||
|     for (const commitStack of parsedCommitStacks) { | ||||
|       console.log(`## ${commitStack.version}`); | ||||
|  | ||||
|       if (commitStack.breaking.length > 0) { | ||||
|         console.log(`### Breaking`); | ||||
|         for (const commit of commitStack.breaking) { | ||||
|           const shortHash = commit.hash.slice(0, 9); | ||||
|  | ||||
|           // Selects contents between parenthesis and a semicolon, via https://stackoverflow.com/a/17779833/14544732 | ||||
|           const type = /\(([^)]+)\):/.exec(commit.message) | ||||
|             ? /\(([^)]+)\):/.exec(commit.message)![1] | ||||
|             : null; | ||||
|           const firstMessageLine = commit.message.split("\n"); | ||||
|           const briefMessage = firstMessageLine[0].includes(":") | ||||
|             ? firstMessageLine[0].split(":")[1].trim() | ||||
|             : firstMessageLine[0]; | ||||
|  | ||||
|           console.log( | ||||
|             `${showHashes ? `- [${shortHash}]` : ``} - ${ | ||||
|               type ? `**${type}**: ${briefMessage}` : briefMessage | ||||
|             }`, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (commitStack.features.length > 0) { | ||||
|         console.log(`### Features`); | ||||
|         for (const commit of commitStack.features) { | ||||
|           const shortHash = commit.hash.slice(0, 9); | ||||
|           const type = /\(([^)]+)\):/.exec(commit.message) | ||||
|             ? /\(([^)]+)\):/.exec(commit.message)![1] | ||||
|             : null; | ||||
|           const firstMessageLine = commit.message.split("\n"); | ||||
|           const briefMessage = firstMessageLine[0].includes(":") | ||||
|             ? firstMessageLine[0].split(":")[1].trim() | ||||
|             : firstMessageLine[0]; | ||||
|  | ||||
|           console.log( | ||||
|             `${showHashes ? `- [${shortHash}]` : ``} - ${ | ||||
|               type ? `**${type}**: ${briefMessage}` : briefMessage | ||||
|             }`, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (commitStack.fixes.length > 0) { | ||||
|         console.log(`### Fixes`); | ||||
|         for (const commit of commitStack.fixes) { | ||||
|           const shortHash = commit.hash.slice(0, 9); | ||||
|           const type = /\(([^)]+)\):/.exec(commit.message) | ||||
|             ? /\(([^)]+)\):/.exec(commit.message)![1] | ||||
|             : null; | ||||
|           const firstMessageLine = commit.message.split("\n"); | ||||
|           const briefMessage = firstMessageLine[0].includes(":") | ||||
|             ? firstMessageLine[0].split(":")[1].trim() | ||||
|             : firstMessageLine[0]; | ||||
|  | ||||
|           console.log( | ||||
|             `${showHashes ? `- [${shortHash}]` : ``} - ${ | ||||
|               type ? `**${type}**: ${briefMessage}` : briefMessage | ||||
|             }`, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (commitStack.miscellaneous.length > 0) { | ||||
|         console.log(`### Miscellaneous`); | ||||
|         for (const commit of commitStack.miscellaneous) { | ||||
|           const shortHash = commit.hash.slice(0, 9); | ||||
|           const type = /\(([^)]+)\):/.exec(commit.message) | ||||
|             ? /\(([^)]+)\):/.exec(commit.message)![1] | ||||
|             : null; | ||||
|           const firstMessageLine = commit.message.split("\n"); | ||||
|           const briefMessage = firstMessageLine[0].includes(":") | ||||
|             ? firstMessageLine[0].split(":")[1].trim() | ||||
|             : firstMessageLine[0]; | ||||
|  | ||||
|           console.log( | ||||
|             `${showHashes ? `- [${shortHash}]` : ``} - ${ | ||||
|               type ? `**${type}**: ${briefMessage}` : briefMessage | ||||
|             }`, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|  | ||||
| export default command; | ||||
							
								
								
									
										312
									
								
								src/commands/commit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								src/commands/commit.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,312 @@ | ||||
| /* | ||||
| Copyright 2024 Resultium LLC | ||||
|  | ||||
| This file is part of RCZ. | ||||
|  | ||||
| RCZ is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU General Public License as published by | ||||
| the Free Software Foundation, either version 3 of the License, or | ||||
| any later version. | ||||
|  | ||||
| RCZ is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| GNU General Public License for more details. | ||||
|  | ||||
| You should have received a copy of the GNU General Public License | ||||
| along with RCZ. If not, see <https://www.gnu.org/licenses/>. | ||||
| */ | ||||
|  | ||||
| import { Command } from "commander"; | ||||
| import { checkForUpdates, getConfig } from "../utils/functions"; | ||||
| import { | ||||
|   cancel, | ||||
|   confirm, | ||||
|   intro, | ||||
|   isCancel, | ||||
|   multiselect, | ||||
|   note, | ||||
|   outro, | ||||
|   select, | ||||
|   text, | ||||
| } from "@clack/prompts"; | ||||
| import { existsSync } from "fs"; | ||||
| import { join } from "path"; | ||||
| import simpleGit, { ResetMode } from "simple-git"; | ||||
|  | ||||
| const command = new Command("commit") | ||||
|   .alias("c") | ||||
|   .description("Create a conventional commit") | ||||
|   .option("-S, --sign", "sign the commit") | ||||
|   .option("--amend", "amend commit message to the last commit") | ||||
|   .option("--sudo", "remove any validation") | ||||
|   .action(async (options) => { | ||||
|     await checkForUpdates(); | ||||
|  | ||||
|     const config = await getConfig(); | ||||
|  | ||||
|     const sign = config?.autoSignCommits || options.sign ? true : false; | ||||
|     const amend = options.amend ? true : false; | ||||
|     const sudo = options.sudo ? true : false; | ||||
|  | ||||
|     intro("Creating a conventional commit"); | ||||
|  | ||||
|     if (!existsSync(join(process.cwd(), ".git"))) { | ||||
|       cancel("Git repository has not been initialized"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const stageAll = amend | ||||
|       ? null | ||||
|       : await confirm({ | ||||
|           message: "Stage all changes?", | ||||
|           initialValue: (await simpleGit().diff(["--cached"])).toString() | ||||
|             ? false | ||||
|             : true, | ||||
|         }); | ||||
|  | ||||
|     if (isCancel(stageAll)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const stageableFiles = (await simpleGit().status()).files; | ||||
|     if (stageAll === false) { | ||||
|       const stagedFiles = (await simpleGit().diff(["--name-only", "--cached"])) | ||||
|         .split("\n") | ||||
|         .filter((file) => file); | ||||
|  | ||||
|       const filesToStage = await multiselect({ | ||||
|         message: "Which files would you like to stash?", | ||||
|         options: stageableFiles.map((file) => ({ | ||||
|           value: file.path, | ||||
|           label: file.path, | ||||
|           hint: file.from, | ||||
|         })), | ||||
|         initialValues: stagedFiles, | ||||
|         required: false, | ||||
|       }); | ||||
|  | ||||
|       if (isCancel(filesToStage)) { | ||||
|         cancel("Commit creation cancelled"); | ||||
|         process.exit(0); | ||||
|       } | ||||
|  | ||||
|       await simpleGit().reset(ResetMode.MIXED); | ||||
|       await simpleGit().add(filesToStage); | ||||
|     } else if (stageAll === true) { | ||||
|       note( | ||||
|         stageableFiles.map((file) => file.path).join("\n"), | ||||
|         "Committing following files", | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       const changedLines = ( | ||||
|         ( | ||||
|           await simpleGit().diff(["--numstat", stageAll ? "HEAD" : "--cached"]) | ||||
|         ).match(/\d+/gm) || [] | ||||
|       ).reduce((partialSum, num) => partialSum + Number(num), 0); | ||||
|  | ||||
|       if (changedLines > 250 && !sudo) { | ||||
|         const proceedCommitting = await confirm({ | ||||
|           message: | ||||
|             "You are about to commit changes to more than 250 lines, are you sure you want to proceed?", | ||||
|           initialValue: false, | ||||
|         }); | ||||
|  | ||||
|         if (isCancel(proceedCommitting)) { | ||||
|           cancel("Commit creation cancelled"); | ||||
|           process.exit(0); | ||||
|         } | ||||
|  | ||||
|         if (!proceedCommitting) { | ||||
|           cancel("Cancelled, please split this commit into smaller ones"); | ||||
|           process.exit(0); | ||||
|         } | ||||
|       } | ||||
|     } catch { | ||||
|       note("HEAD hasn't been found, skipping commit line sum check"); | ||||
|     } | ||||
|  | ||||
|     const type: string | symbol = await select({ | ||||
|       message: "Choose a commit type", | ||||
|       options: config?.types || [ | ||||
|         { | ||||
|           label: "feat", | ||||
|           value: "feat", | ||||
|           hint: "new feature", | ||||
|         }, | ||||
|         { | ||||
|           label: "fix", | ||||
|           value: "fix", | ||||
|           hint: "functionality bug fix", | ||||
|         }, | ||||
|         { | ||||
|           label: "build", | ||||
|           value: "build", | ||||
|           hint: "changes that affect the build system, configs or external dependencies", | ||||
|         }, | ||||
|         { | ||||
|           label: "ci", | ||||
|           value: "ci", | ||||
|           hint: "change to CI/CD configurations and scripts e.g. CircleCI, GitHub workflows", | ||||
|         }, | ||||
|         { | ||||
|           label: "docs", | ||||
|           value: "docs", | ||||
|           hint: "documentation changes e.g. README, CHANGELOG", | ||||
|         }, | ||||
|         { | ||||
|           label: "perf", | ||||
|           value: "perf", | ||||
|           hint: "code change, that improves performance", | ||||
|         }, | ||||
|         { | ||||
|           label: "refactor", | ||||
|           value: "refactor", | ||||
|           hint: "code change that neither fixes a bug nor adds a feature", | ||||
|         }, | ||||
|         { | ||||
|           label: "chore", | ||||
|           value: "chore", | ||||
|           hint: "changes that are routinely, e.g. dependency update or a release commit", | ||||
|         }, | ||||
|       ], | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(type)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const scope: string | symbol = await text({ | ||||
|       message: | ||||
|         "Input a scope (subsystem which change is relevant to e.g. router, forms, core) or leave empty", | ||||
|       placeholder: "router", | ||||
|       validate: (value) => { | ||||
|         if (sudo) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         if (config?.scopes && value) { | ||||
|           if (!config?.scopes.includes(value)) | ||||
|             return "This scope is not allowed by local configuration"; | ||||
|         } | ||||
|  | ||||
|         if (value.includes(" ")) { | ||||
|           return "Must not include a space"; | ||||
|         } | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(scope)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const message = await text({ | ||||
|       message: `Briefly describe made changes in imperative tense, maximum length 50`, | ||||
|       placeholder: "warn upon suspicious router requests", | ||||
|       validate: (value) => { | ||||
|         if (sudo) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         if (value.length > 50) { | ||||
|           return "Your message is too long"; | ||||
|         } | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(message)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const body = await text({ | ||||
|       message: `Insert a commit body (motivation or elaboration for the change), can be left empty`, | ||||
|       placeholder: | ||||
|         "improves regex for suspicious character check in router requests", | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(body)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const footer = await text({ | ||||
|       message: `Insert commit footer, can be left empty, e.g. Acked-by: @johndoe`, | ||||
|       placeholder: "Acked-by: @security", | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(footer)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const isBreaking = await confirm({ | ||||
|       message: "Does this commit have breaking changes?", | ||||
|       initialValue: false, | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(isBreaking)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const resolvesIssue = await confirm({ | ||||
|       message: "Does this commit resolve an existing issue?", | ||||
|       initialValue: false, | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(resolvesIssue)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const issue = resolvesIssue | ||||
|       ? await text({ message: "Enter an issue identifier, e.g. #274" }) | ||||
|       : null; | ||||
|  | ||||
|     if (isCancel(issue)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const commitMessage = `${type.toString()}${ | ||||
|       scope ? `(${scope.toString()})` : `` | ||||
|     }${isBreaking ? "!" : ""}: ${message.toString()}${ | ||||
|       resolvesIssue ? ` (${issue?.toString()})` : `` | ||||
|     }${body ? `\n\n${body}` : ``}${footer ? `\n\n${footer}` : ``}`; | ||||
|  | ||||
|     if (stageAll) { | ||||
|       await simpleGit() | ||||
|         .add(".") | ||||
|         .commit( | ||||
|           commitMessage, | ||||
|           sign | ||||
|             ? amend | ||||
|               ? ["-S", "--amend"] | ||||
|               : ["-S"] | ||||
|             : amend | ||||
|               ? ["--amend"] | ||||
|               : [], | ||||
|         ); | ||||
|     } else { | ||||
|       await simpleGit().commit( | ||||
|         commitMessage, | ||||
|         sign ? (amend ? ["-S", "--amend"] : ["-S"]) : amend ? ["--amend"] : [], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     note(commitMessage); | ||||
|  | ||||
|     outro( | ||||
|       `Finished ${ | ||||
|         amend ? "amending" : "creating" | ||||
|       } a conventional commit, feel free to push`, | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
| export default command; | ||||
							
								
								
									
										92
									
								
								src/commands/release.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/commands/release.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| /* | ||||
| Copyright 2024 Resultium LLC | ||||
|  | ||||
| This file is part of RCZ. | ||||
|  | ||||
| RCZ is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU General Public License as published by | ||||
| the Free Software Foundation, either version 3 of the License, or | ||||
| any later version. | ||||
|  | ||||
| RCZ is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| GNU General Public License for more details. | ||||
|  | ||||
| You should have received a copy of the GNU General Public License | ||||
| along with RCZ. If not, see <https://www.gnu.org/licenses/>. | ||||
| */ | ||||
|  | ||||
| import { Command } from "commander"; | ||||
| import { | ||||
|   getConfig, | ||||
|   getOnReleaseScript, | ||||
|   getPostReleaseScript, | ||||
| } from "../utils/functions"; | ||||
| import { readFile, writeFile } from "fs/promises"; | ||||
| import { join } from "path"; | ||||
| import { cwd } from "process"; | ||||
| import simpleGit from "simple-git"; | ||||
|  | ||||
| const command = new Command("release") | ||||
|   .alias("rel") | ||||
|   .description( | ||||
|     "Changes package.json version and creates a new commit with a tag", | ||||
|   ) | ||||
|   .argument("<version>", "new version formatted in SemVer") | ||||
|   .option("-S, --sign", "sign the release commit and tag") | ||||
|   .action(async (string: string, options) => { | ||||
|     const config = await getConfig(); | ||||
|  | ||||
|     const sign = config?.autoSignReleases || options.sign ? true : false; | ||||
|     const version = string.replace("v", ""); | ||||
|  | ||||
|     const onReleaseFile = await getOnReleaseScript(); | ||||
|  | ||||
|     if (onReleaseFile) { | ||||
|       const releaseScript = ` | ||||
|         const __NEW_VERSION__ = "${version}"; | ||||
|         const __IS_SIGNED__ = ${sign}; | ||||
|  | ||||
|         ${onReleaseFile}`; | ||||
|  | ||||
|       eval(releaseScript); | ||||
|     } else { | ||||
|       const packageFile = JSON.parse( | ||||
|         (await readFile(join(cwd(), "package.json"))).toString(), | ||||
|       ); | ||||
|  | ||||
|       if (!packageFile) { | ||||
|         console.log("[rcz]: this directory does not have a package.json file"); | ||||
|       } | ||||
|  | ||||
|       packageFile.version = version; | ||||
|       await writeFile( | ||||
|         join(cwd(), "package.json"), | ||||
|         JSON.stringify(packageFile, null, 4), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     await simpleGit() | ||||
|       .add(".") | ||||
|       .commit(`chore(release): v${version}`, sign ? ["-S"] : []) | ||||
|       .tag( | ||||
|         sign | ||||
|           ? [`-s`, `v${version}`, `-m`, `"v${version}"`] | ||||
|           : [`-a`, `v${version}`, `-m`, `"v${version}"`], | ||||
|       ); | ||||
|  | ||||
|     const postReleaseFile = await getPostReleaseScript(); | ||||
|  | ||||
|     if (postReleaseFile) { | ||||
|       const postReleaseScript = ` | ||||
|         const __NEW_VERSION__ = "${version}"; | ||||
|         const __IS_SIGNED__ = ${sign}; | ||||
|  | ||||
|         ${postReleaseFile}`; | ||||
|  | ||||
|       eval(postReleaseScript); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
| export default command; | ||||
							
								
								
									
										62
									
								
								src/commands/validate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/commands/validate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| /* | ||||
| Copyright 2024 Resultium LLC | ||||
|  | ||||
| This file is part of RCZ. | ||||
|  | ||||
| RCZ is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU General Public License as published by | ||||
| the Free Software Foundation, either version 3 of the License, or | ||||
| any later version. | ||||
|  | ||||
| RCZ is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| GNU General Public License for more details. | ||||
|  | ||||
| You should have received a copy of the GNU General Public License | ||||
| along with RCZ. If not, see <https://www.gnu.org/licenses/>. | ||||
| */ | ||||
|  | ||||
| import { Command } from "commander"; | ||||
| import { getConfig } from "../utils/functions"; | ||||
| import { readFileSync } from "fs"; | ||||
|  | ||||
| const command = new Command("validate") | ||||
|   .description("Validate whether a string fits given commit conventions") | ||||
|   .argument("[message]", "string for validation") | ||||
|   .option("-C, --code-only", "return code only") | ||||
|   .action(async (string: string, options) => { | ||||
|     try { | ||||
|       const message = string || readFileSync(0, "utf-8"); | ||||
|       const codeOnly = options.codeOnly ? true : false; | ||||
|  | ||||
|       const config = await getConfig(); | ||||
|  | ||||
|       // Regex for testing: | ||||
|       // /(build|feat|docs)(\((commands|changelog)\))?!?: .* ?(\(..*\))?((\n\n..*)?(\n\n..*)?)?/gm | ||||
|  | ||||
|       const testRegex = new RegExp( | ||||
|         `(${ | ||||
|           config?.types?.map((type) => type.value).join("|") || | ||||
|           "feat|fix|build|ci|docs|perf|refactor|chore" | ||||
|         })(\\((${ | ||||
|           config?.scopes ? [...config?.scopes, "release"].join("|") : "..*" | ||||
|         })\\))?!?: .* ?(\\(..*\\))?((\n\n..*)?(\n\n..*)?)?`, | ||||
|         "gm", | ||||
|       ); | ||||
|  | ||||
|       if (codeOnly) { | ||||
|         console.log(testRegex.test(message) ? 0 : 1); | ||||
|       } else { | ||||
|         console.log( | ||||
|           testRegex.test(message) | ||||
|             ? "[rcz]: valid message" | ||||
|             : "[rcz]: invalid message", | ||||
|         ); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       console.log("[rcz]: no stdin found"); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
| export default command; | ||||
							
								
								
									
										687
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										687
									
								
								src/index.ts
									
									
									
									
									
								
							| @@ -19,674 +19,27 @@ You should have received a copy of the GNU General Public License | ||||
| along with RCZ. If not, see <https://www.gnu.org/licenses/>. | ||||
| */ | ||||
|  | ||||
| import { | ||||
|   cancel, | ||||
|   confirm, | ||||
|   intro, | ||||
|   isCancel, | ||||
|   note, | ||||
|   outro, | ||||
|   select, | ||||
|   text, | ||||
| } from "@clack/prompts"; | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
| import { CommitStack, Config } from "./types"; | ||||
| import simpleGit from "simple-git"; | ||||
| import { Command } from "commander"; | ||||
| import semver from "semver"; | ||||
| import { execSync } from "child_process"; | ||||
| import { tmpdir } from "os"; | ||||
| import { request } from "http"; | ||||
| import { readdir } from "fs/promises"; | ||||
| import { join } from "path"; | ||||
|  | ||||
| const GetConfig = async () => { | ||||
|   if (fs.existsSync(path.join(process.cwd(), ".rczrc"))) { | ||||
|     return JSON.parse( | ||||
|       ( | ||||
|         await fs.promises.readFile(path.join(process.cwd(), ".rczrc")) | ||||
|       ).toString(), | ||||
|     ) as Config; | ||||
|   } else if (fs.existsSync(path.join(process.cwd(), ".rczrc.json"))) { | ||||
|     return JSON.parse( | ||||
|       ( | ||||
|         await fs.promises.readFile(path.join(process.cwd(), ".rczrc.json")) | ||||
|       ).toString(), | ||||
|     ) as Config; | ||||
|   } else if (fs.existsSync(path.join(process.cwd(), "rcz.config.json"))) { | ||||
|     return JSON.parse( | ||||
|       ( | ||||
|         await fs.promises.readFile(path.join(process.cwd(), "rcz.config.json")) | ||||
|       ).toString(), | ||||
|     ) as Config; | ||||
|   } else if (fs.existsSync(path.join(process.cwd(), ".rcz", "config.json"))) { | ||||
|     return JSON.parse( | ||||
|       ( | ||||
|         await fs.promises.readFile( | ||||
|           path.join(process.cwd(), ".rcz", "config.json"), | ||||
|         ) | ||||
|       ).toString(), | ||||
|     ) as Config; | ||||
|   } else { | ||||
|     return null; | ||||
| (async () => { | ||||
|   const program = new Command(); | ||||
|  | ||||
|   program | ||||
|     .name("rcz") | ||||
|     .description("Resultium commit standardization command-line interface") | ||||
|     .version("1.14.0"); | ||||
|  | ||||
|   const commandFiles = await readdir(join(__dirname, "commands")); | ||||
|  | ||||
|   for (const commandFile of commandFiles) { | ||||
|     const command = ( | ||||
|       await import(join(__dirname, "commands", commandFile.split(".")[0])) | ||||
|     ).default; | ||||
|  | ||||
|     program.addCommand(command); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const GetOnReleaseScript = async () => { | ||||
|   if (fs.existsSync(path.join(process.cwd(), ".rcz.onrelease.js"))) { | ||||
|     return ( | ||||
|       await fs.promises.readFile(path.join(process.cwd(), ".rcz.onrelease.js")) | ||||
|     ).toString(); | ||||
|   } else if (fs.existsSync(path.join(process.cwd(), ".rcz", "onrelease.js"))) { | ||||
|     return ( | ||||
|       await fs.promises.readFile( | ||||
|         path.join(process.cwd(), ".rcz", "onrelease.js"), | ||||
|       ) | ||||
|     ).toString(); | ||||
|   } else { | ||||
|     return null; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const GetPostReleaseScript = async () => { | ||||
|   if (fs.existsSync(path.join(process.cwd(), ".rcz.postrelease.js"))) { | ||||
|     return ( | ||||
|       await fs.promises.readFile( | ||||
|         path.join(process.cwd(), ".rcz.postrelease.js"), | ||||
|       ) | ||||
|     ).toString(); | ||||
|   } else if ( | ||||
|     fs.existsSync(path.join(process.cwd(), ".rcz", "postrelease.js")) | ||||
|   ) { | ||||
|     return ( | ||||
|       await fs.promises.readFile( | ||||
|         path.join(process.cwd(), ".rcz", "postrelease.js"), | ||||
|       ) | ||||
|     ).toString(); | ||||
|   } else { | ||||
|     return null; | ||||
|   } | ||||
| }; | ||||
| const isOnline = () => { | ||||
|   return new Promise<boolean>((resolve) => { | ||||
|     request({ method: "GET", hostname: "icanhazip.com" }, (res) => { | ||||
|       res.on("data", () => {}); | ||||
|  | ||||
|       res.on("end", () => { | ||||
|         resolve(res.statusCode === 200); | ||||
|       }); | ||||
|     }) | ||||
|       .on("error", () => { | ||||
|         resolve(false); | ||||
|       }) | ||||
|       .end(); | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| const CheckForUpdates = async () => { | ||||
|   const updateText = | ||||
|     "You are running on an outdated version of Resultium Commitizen, in order to update run\n\nnpm install -g @resultium/rcz@latest"; | ||||
|  | ||||
|   const cachedVersion = fs.existsSync(path.join(tmpdir(), "rcz-server-version")) | ||||
|     ? (await fs.promises.readFile(path.join(tmpdir(), "rcz-server-version"))) | ||||
|         .toString() | ||||
|         .trim() | ||||
|     : null; | ||||
|   const localVersion = execSync("rcz --version").toString().trim(); | ||||
|  | ||||
|   // even if cached once in a while it should get newest data | ||||
|   if ((cachedVersion && Math.random() < 0.1) || cachedVersion === null) { | ||||
|     if (!(await isOnline())) return; | ||||
|  | ||||
|     const serverVersion = execSync("npm show @resultium/rcz version") | ||||
|       .toString() | ||||
|       .trim(); | ||||
|  | ||||
|     fs.promises.writeFile( | ||||
|       path.join(tmpdir(), "rcz-server-version"), | ||||
|       serverVersion, | ||||
|     ); | ||||
|  | ||||
|     if (semver.gt(serverVersion, localVersion)) note(updateText); | ||||
|   } else if (cachedVersion) { | ||||
|     if (semver.gt(cachedVersion, localVersion)) note(updateText); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const program = new Command(); | ||||
|  | ||||
| program | ||||
|   .name("rcz") | ||||
|   .description("Resultium commit standardization command-line interface") | ||||
|   .version("1.12.3"); | ||||
|  | ||||
| program | ||||
|   .command("commit") | ||||
|   .alias("c") | ||||
|   .description("Create a conventional commit") | ||||
|   .option("-S, --sign", "sign the commit") | ||||
|   .option("--amend", "amend commit message to the last commit") | ||||
|   .option("--sudo", "remove any validation") | ||||
|   .action(async (options) => { | ||||
|     await CheckForUpdates(); | ||||
|  | ||||
|     const config = await GetConfig(); | ||||
|  | ||||
|     const sign = config?.autoSignCommits || options.sign ? true : false; | ||||
|     const amend = options.amend ? true : false; | ||||
|     const sudo = options.sudo ? true : false; | ||||
|  | ||||
|     intro("Creating a conventional commit"); | ||||
|  | ||||
|     if (!fs.existsSync(path.join(process.cwd(), ".git"))) { | ||||
|       cancel("Git repository has not been initialized"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const stageAll = amend | ||||
|       ? null | ||||
|       : await confirm({ | ||||
|           message: "Stage all changes?", | ||||
|           initialValue: (await simpleGit().diff(["--cached"])).toString() | ||||
|             ? false | ||||
|             : true, | ||||
|         }); | ||||
|  | ||||
|     if (isCancel(stageAll)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const changedLines = ( | ||||
|       ( | ||||
|         await simpleGit().diff(["--numstat", stageAll ? "HEAD" : "--cached"]) | ||||
|       ).match(/\d+/gm) || [] | ||||
|     ).reduce((partialSum, num) => partialSum + Number(num), 0); | ||||
|  | ||||
|     if (changedLines > 250 && !sudo) { | ||||
|       const proceedCommitting = await confirm({ | ||||
|         message: | ||||
|           "You are about to commit changes to more than 250 lines, are you sure you want to proceed?", | ||||
|         initialValue: false, | ||||
|       }); | ||||
|  | ||||
|       if (isCancel(proceedCommitting)) { | ||||
|         cancel("Commit creation cancelled"); | ||||
|         process.exit(0); | ||||
|       } | ||||
|  | ||||
|       if (!proceedCommitting) { | ||||
|         cancel("Cancelled, please split this commit into smaller ones"); | ||||
|         process.exit(0); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const type: string | symbol = await select({ | ||||
|       message: "Choose a commit type", | ||||
|       options: config?.types || [ | ||||
|         { | ||||
|           label: "feat", | ||||
|           value: "feat", | ||||
|           hint: "new feature", | ||||
|         }, | ||||
|         { | ||||
|           label: "fix", | ||||
|           value: "fix", | ||||
|           hint: "functionality bug fix", | ||||
|         }, | ||||
|         { | ||||
|           label: "build", | ||||
|           value: "build", | ||||
|           hint: "changes that affect the build system, configs or external dependencies", | ||||
|         }, | ||||
|         { | ||||
|           label: "ci", | ||||
|           value: "ci", | ||||
|           hint: "change to CI/CD configurations and scripts e.g. CircleCI, GitHub workflows", | ||||
|         }, | ||||
|         { | ||||
|           label: "docs", | ||||
|           value: "docs", | ||||
|           hint: "documentation changes e.g. README, CHANGELOG", | ||||
|         }, | ||||
|         { | ||||
|           label: "perf", | ||||
|           value: "perf", | ||||
|           hint: "code change, that improves performance", | ||||
|         }, | ||||
|         { | ||||
|           label: "refactor", | ||||
|           value: "refactor", | ||||
|           hint: "code change that neither fixes a bug nor adds a feature", | ||||
|         }, | ||||
|         { | ||||
|           label: "chore", | ||||
|           value: "chore", | ||||
|           hint: "changes that are routinely, e.g. dependency update or a release commit", | ||||
|         }, | ||||
|       ], | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(type)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const scope: string | symbol = await text({ | ||||
|       message: | ||||
|         "Input a scope (subsystem which change is relevant to e.g. router, forms, core) or leave empty", | ||||
|       placeholder: "router", | ||||
|       validate: (value) => { | ||||
|         if (sudo) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         if (config?.scopes && value) { | ||||
|           if (!config?.scopes.includes(value)) | ||||
|             return "This scope is not allowed by local configuration"; | ||||
|         } | ||||
|  | ||||
|         if (value.includes(" ")) { | ||||
|           return "Must not include a space"; | ||||
|         } | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(scope)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const message = await text({ | ||||
|       message: `Briefly describe made changes in imperative tense, maximum length 50`, | ||||
|       placeholder: "warn upon suspicious router requests", | ||||
|       validate: (value) => { | ||||
|         if (sudo) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         if (value.length > 50) { | ||||
|           return "Your message is too long"; | ||||
|         } | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(message)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const body = await text({ | ||||
|       message: `Insert a commit body (motivation or elaboration for the change), can be left empty`, | ||||
|       placeholder: | ||||
|         "improves regex for suspicious character check in router requests", | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(body)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const footer = await text({ | ||||
|       message: `Insert commit footer, can be left empty, e.g. Acked-by: @johndoe`, | ||||
|       placeholder: "Acked-by: @security", | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(footer)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const isBreaking = await confirm({ | ||||
|       message: "Does this commit have breaking changes?", | ||||
|       initialValue: false, | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(isBreaking)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const resolvesIssue = await confirm({ | ||||
|       message: "Does this commit resolve an existing issue?", | ||||
|       initialValue: false, | ||||
|     }); | ||||
|  | ||||
|     if (isCancel(resolvesIssue)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const issue = resolvesIssue | ||||
|       ? await text({ message: "Enter an issue identifier, e.g. #274" }) | ||||
|       : null; | ||||
|  | ||||
|     if (isCancel(issue)) { | ||||
|       cancel("Commit creation cancelled"); | ||||
|       process.exit(0); | ||||
|     } | ||||
|  | ||||
|     const commitMessage = `${type.toString()}${ | ||||
|       scope ? `(${scope.toString()})` : `` | ||||
|     }${isBreaking ? "!" : ""}: ${message.toString()}${ | ||||
|       resolvesIssue ? ` (${issue?.toString()})` : `` | ||||
|     }${body ? `\n\n${body}` : ``}${footer ? `\n\n${footer}` : ``}`; | ||||
|  | ||||
|     if (stageAll) { | ||||
|       await simpleGit() | ||||
|         .add(".") | ||||
|         .commit( | ||||
|           commitMessage, | ||||
|           sign | ||||
|             ? amend | ||||
|               ? ["-S", "--amend"] | ||||
|               : ["-S"] | ||||
|             : amend | ||||
|               ? ["--amend"] | ||||
|               : [], | ||||
|         ); | ||||
|     } else { | ||||
|       await simpleGit().commit( | ||||
|         commitMessage, | ||||
|         sign ? (amend ? ["-S", "--amend"] : ["-S"]) : amend ? ["--amend"] : [], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     note(commitMessage); | ||||
|  | ||||
|     outro( | ||||
|       `Finished ${ | ||||
|         amend ? "amending" : "creating" | ||||
|       } a conventional commit, feel free to push`, | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
| program | ||||
|   .command("changelog") | ||||
|   .alias("ch") | ||||
|   .description("Outputs a markdown formatted changelog") | ||||
|   .option("--show-hashes", "show first 9 characters of commit hashes") | ||||
|   .option("--last-only", "display only latest release changes") | ||||
|   .option( | ||||
|     "--unreleased-as <version>", | ||||
|     "show unreleased changes as different version", | ||||
|   ) | ||||
|   .action(async (options) => { | ||||
|     const showHashes = options.showHashes ? true : false; | ||||
|     const lastOnly = options.lastOnly ? true : false; | ||||
|     const unreleased = options.unreleasedAs || "Unreleased"; | ||||
|  | ||||
|     if ((await simpleGit().tags()).all.length === 0) { | ||||
|       return console.log( | ||||
|         "[rcz]: not even one release has yet been made, cannot make a changelog", | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const commits = (await simpleGit().log()).all; | ||||
|     let lastTag = ""; | ||||
|     let parsedCommitStacks: Array<CommitStack> = []; | ||||
|  | ||||
|     console.log("# Changelog"); | ||||
|     console.log("Generation of this changelog is based on commits"); | ||||
|  | ||||
|     for (const commit of commits) { | ||||
|       const tag = | ||||
|         semver.sort( | ||||
|           (await simpleGit().tags([`--contains=${commit.hash}`])).all, | ||||
|         )[0]! || unreleased; | ||||
|  | ||||
|       const currentCommitStack = parsedCommitStacks.find( | ||||
|         (commitStack) => commitStack.version === tag, | ||||
|       ) || { | ||||
|         version: tag || unreleased, | ||||
|         breaking: [], | ||||
|         features: [], | ||||
|         fixes: [], | ||||
|         miscellaneous: [], | ||||
|       }; | ||||
|  | ||||
|       if (lastTag !== tag) { | ||||
|         parsedCommitStacks = [currentCommitStack, ...parsedCommitStacks]; | ||||
|       } | ||||
|  | ||||
|       if (commit.message.includes("!:")) { | ||||
|         parsedCommitStacks = [ | ||||
|           { | ||||
|             ...currentCommitStack, | ||||
|             breaking: [...currentCommitStack.breaking, commit], | ||||
|           }, | ||||
|           ...parsedCommitStacks.filter( | ||||
|             (commitStack) => commitStack.version !== tag, | ||||
|           ), | ||||
|         ]; | ||||
|       } else if (commit.message.startsWith("feat")) { | ||||
|         parsedCommitStacks = [ | ||||
|           { | ||||
|             ...currentCommitStack, | ||||
|             features: [...currentCommitStack.features, commit], | ||||
|           }, | ||||
|           ...parsedCommitStacks.filter( | ||||
|             (commitStack) => commitStack.version !== tag, | ||||
|           ), | ||||
|         ]; | ||||
|       } else if (commit.message.startsWith("fix")) { | ||||
|         parsedCommitStacks = [ | ||||
|           { | ||||
|             ...currentCommitStack, | ||||
|             fixes: [...currentCommitStack.fixes, commit], | ||||
|           }, | ||||
|           ...parsedCommitStacks.filter( | ||||
|             (commitStack) => commitStack.version !== tag, | ||||
|           ), | ||||
|         ]; | ||||
|       } else { | ||||
|         parsedCommitStacks = [ | ||||
|           { | ||||
|             ...currentCommitStack, | ||||
|             miscellaneous: [...currentCommitStack.miscellaneous, commit], | ||||
|           }, | ||||
|           ...parsedCommitStacks.filter( | ||||
|             (commitStack) => commitStack.version !== tag, | ||||
|           ), | ||||
|         ]; | ||||
|       } | ||||
|  | ||||
|       lastTag = tag; | ||||
|     } | ||||
|  | ||||
|     parsedCommitStacks = parsedCommitStacks.reverse(); | ||||
|  | ||||
|     if (lastOnly) { | ||||
|       parsedCommitStacks = [parsedCommitStacks[0]]; | ||||
|     } | ||||
|  | ||||
|     for (const commitStack of parsedCommitStacks) { | ||||
|       console.log(`## ${commitStack.version}`); | ||||
|  | ||||
|       if (commitStack.breaking.length > 0) { | ||||
|         console.log(`### Breaking`); | ||||
|         for (const commit of commitStack.breaking) { | ||||
|           const shortHash = commit.hash.slice(0, 9); | ||||
|  | ||||
|           // Selects contents between parenthesis and a semicolon, via https://stackoverflow.com/a/17779833/14544732 | ||||
|           const type = /\(([^)]+)\):/.exec(commit.message) | ||||
|             ? /\(([^)]+)\):/.exec(commit.message)![1] | ||||
|             : null; | ||||
|           const firstMessageLine = commit.message.split("\n"); | ||||
|           const briefMessage = firstMessageLine[0].includes(":") | ||||
|             ? firstMessageLine[0].split(":")[1].trim() | ||||
|             : firstMessageLine[0]; | ||||
|  | ||||
|           console.log( | ||||
|             `${showHashes ? `- [${shortHash}]` : ``} - ${ | ||||
|               type ? `**${type}**: ${briefMessage}` : briefMessage | ||||
|             }`, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (commitStack.features.length > 0) { | ||||
|         console.log(`### Features`); | ||||
|         for (const commit of commitStack.features) { | ||||
|           const shortHash = commit.hash.slice(0, 9); | ||||
|           const type = /\(([^)]+)\):/.exec(commit.message) | ||||
|             ? /\(([^)]+)\):/.exec(commit.message)![1] | ||||
|             : null; | ||||
|           const firstMessageLine = commit.message.split("\n"); | ||||
|           const briefMessage = firstMessageLine[0].includes(":") | ||||
|             ? firstMessageLine[0].split(":")[1].trim() | ||||
|             : firstMessageLine[0]; | ||||
|  | ||||
|           console.log( | ||||
|             `${showHashes ? `- [${shortHash}]` : ``} - ${ | ||||
|               type ? `**${type}**: ${briefMessage}` : briefMessage | ||||
|             }`, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (commitStack.fixes.length > 0) { | ||||
|         console.log(`### Fixes`); | ||||
|         for (const commit of commitStack.fixes) { | ||||
|           const shortHash = commit.hash.slice(0, 9); | ||||
|           const type = /\(([^)]+)\):/.exec(commit.message) | ||||
|             ? /\(([^)]+)\):/.exec(commit.message)![1] | ||||
|             : null; | ||||
|           const firstMessageLine = commit.message.split("\n"); | ||||
|           const briefMessage = firstMessageLine[0].includes(":") | ||||
|             ? firstMessageLine[0].split(":")[1].trim() | ||||
|             : firstMessageLine[0]; | ||||
|  | ||||
|           console.log( | ||||
|             `${showHashes ? `- [${shortHash}]` : ``} - ${ | ||||
|               type ? `**${type}**: ${briefMessage}` : briefMessage | ||||
|             }`, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (commitStack.miscellaneous.length > 0) { | ||||
|         console.log(`### Miscellaneous`); | ||||
|         for (const commit of commitStack.miscellaneous) { | ||||
|           const shortHash = commit.hash.slice(0, 9); | ||||
|           const type = /\(([^)]+)\):/.exec(commit.message) | ||||
|             ? /\(([^)]+)\):/.exec(commit.message)![1] | ||||
|             : null; | ||||
|           const firstMessageLine = commit.message.split("\n"); | ||||
|           const briefMessage = firstMessageLine[0].includes(":") | ||||
|             ? firstMessageLine[0].split(":")[1].trim() | ||||
|             : firstMessageLine[0]; | ||||
|  | ||||
|           console.log( | ||||
|             `${showHashes ? `- [${shortHash}]` : ``} - ${ | ||||
|               type ? `**${type}**: ${briefMessage}` : briefMessage | ||||
|             }`, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|  | ||||
| program | ||||
|   .command("release") | ||||
|   .alias("rel") | ||||
|   .description( | ||||
|     "Changes package.json version and creates a new commit with a tag", | ||||
|   ) | ||||
|   .argument("<version>", "new version formatted in SemVer") | ||||
|   .option("-S, --sign", "sign the release commit and tag") | ||||
|   .action(async (string: string, options) => { | ||||
|     const config = await GetConfig(); | ||||
|  | ||||
|     const sign = config?.autoSignReleases || options.sign ? true : false; | ||||
|     const version = string.replace("v", ""); | ||||
|  | ||||
|     const onReleaseFile = await GetOnReleaseScript(); | ||||
|  | ||||
|     if (onReleaseFile) { | ||||
|       const releaseScript = ` | ||||
|         const __NEW_VERSION__ = "${version}"; | ||||
|         const __IS_SIGNED__ = ${sign}; | ||||
|  | ||||
|         ${onReleaseFile}`; | ||||
|  | ||||
|       eval(releaseScript); | ||||
|     } else { | ||||
|       const packageFile = JSON.parse( | ||||
|         ( | ||||
|           await fs.promises.readFile(path.join(process.cwd(), "package.json")) | ||||
|         ).toString(), | ||||
|       ); | ||||
|  | ||||
|       if (!packageFile) { | ||||
|         console.log("[rcz]: this directory does not have a package.json file"); | ||||
|       } | ||||
|  | ||||
|       packageFile.version = version; | ||||
|       await fs.promises.writeFile( | ||||
|         path.join(process.cwd(), "package.json"), | ||||
|         JSON.stringify(packageFile, null, 4), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     await simpleGit() | ||||
|       .add(".") | ||||
|       .commit(`chore(release): v${version}`, sign ? ["-S"] : []) | ||||
|       .tag( | ||||
|         sign | ||||
|           ? [`-s`, `v${version}`, `-m`, `"v${version}"`] | ||||
|           : [`-a`, `v${version}`, `-m`, `"v${version}"`], | ||||
|       ); | ||||
|  | ||||
|     const postReleaseFile = await GetPostReleaseScript(); | ||||
|  | ||||
|     if (postReleaseFile) { | ||||
|       const postReleaseScript = ` | ||||
|         const __NEW_VERSION__ = "${version}"; | ||||
|         const __IS_SIGNED__ = ${sign}; | ||||
|  | ||||
|         ${postReleaseFile}`; | ||||
|  | ||||
|       eval(postReleaseScript); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
| program | ||||
|   .command("validate") | ||||
|   .description("Validate whether a string fits given commit conventions") | ||||
|   .argument("[message]", "string for validation") | ||||
|   .option("-C, --code-only", "return code only") | ||||
|   .action(async (string: string, options) => { | ||||
|     try { | ||||
|       const message = string || fs.readFileSync(0, "utf-8"); | ||||
|       const codeOnly = options.codeOnly ? true : false; | ||||
|  | ||||
|       const config = await GetConfig(); | ||||
|  | ||||
|       // Regex for testing: | ||||
|       // /(build|feat|docs)(\((commands|changelog)\))?!?: .* ?(\(..*\))?((\n\n..*)?(\n\n..*)?)?/gm | ||||
|  | ||||
|       const testRegex = new RegExp( | ||||
|         `(${ | ||||
|           config?.types?.map((type) => type.value).join("|") || | ||||
|           "feat|fix|build|ci|docs|perf|refactor|chore" | ||||
|         })(\\((${ | ||||
|           config?.scopes ? [...config?.scopes, "release"].join("|") : "..*" | ||||
|         })\\))?!?: .* ?(\\(..*\\))?((\n\n..*)?(\n\n..*)?)?`, | ||||
|         "gm", | ||||
|       ); | ||||
|  | ||||
|       if (codeOnly) { | ||||
|         console.log(testRegex.test(message) ? 0 : 1); | ||||
|       } else { | ||||
|         console.log( | ||||
|           testRegex.test(message) | ||||
|             ? "[rcz]: valid message" | ||||
|             : "[rcz]: invalid message", | ||||
|         ); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       console.log("[rcz]: no stdin found"); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
| program.parse(); | ||||
|   program.parse(); | ||||
| })(); | ||||
|   | ||||
							
								
								
									
										112
									
								
								src/utils/functions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/utils/functions.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| /* | ||||
| Copyright 2024 Resultium LLC | ||||
|  | ||||
| This file is part of RCZ. | ||||
|  | ||||
| RCZ is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU General Public License as published by | ||||
| the Free Software Foundation, either version 3 of the License, or | ||||
| any later version. | ||||
|  | ||||
| RCZ is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| GNU General Public License for more details. | ||||
|  | ||||
| You should have received a copy of the GNU General Public License | ||||
| along with RCZ. If not, see <https://www.gnu.org/licenses/>. | ||||
| */ | ||||
|  | ||||
| import { existsSync } from "fs"; | ||||
| import { readFile, writeFile } from "fs/promises"; | ||||
| import { join } from "path"; | ||||
| import { cwd } from "process"; | ||||
| import { Config } from "../types"; | ||||
| import { request } from "http"; | ||||
| import { tmpdir } from "os"; | ||||
| import { execSync } from "child_process"; | ||||
| import { gt } from "semver"; | ||||
| import { note } from "@clack/prompts"; | ||||
|  | ||||
| export const getConfig = async () => { | ||||
|   if (existsSync(join(cwd(), ".rczrc"))) { | ||||
|     return JSON.parse( | ||||
|       (await readFile(join(cwd(), ".rczrc"))).toString(), | ||||
|     ) as Config; | ||||
|   } else if (existsSync(join(cwd(), ".rczrc.json"))) { | ||||
|     return JSON.parse( | ||||
|       (await readFile(join(cwd(), ".rczrc.json"))).toString(), | ||||
|     ) as Config; | ||||
|   } else if (existsSync(join(cwd(), "rcz.config.json"))) { | ||||
|     return JSON.parse( | ||||
|       (await readFile(join(cwd(), "rcz.config.json"))).toString(), | ||||
|     ) as Config; | ||||
|   } else if (existsSync(join(cwd(), ".rcz", "config.json"))) { | ||||
|     return JSON.parse( | ||||
|       (await readFile(join(cwd(), ".rcz", "config.json"))).toString(), | ||||
|     ) as Config; | ||||
|   } else { | ||||
|     return null; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const getOnReleaseScript = async () => { | ||||
|   if (existsSync(join(cwd(), ".rcz.onrelease.js"))) { | ||||
|     return (await readFile(join(cwd(), ".rcz.onrelease.js"))).toString(); | ||||
|   } else if (existsSync(join(cwd(), ".rcz", "onrelease.js"))) { | ||||
|     return (await readFile(join(cwd(), ".rcz", "onrelease.js"))).toString(); | ||||
|   } else { | ||||
|     return null; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const getPostReleaseScript = async () => { | ||||
|   if (existsSync(join(cwd(), ".rcz.postrelease.js"))) { | ||||
|     return (await readFile(join(cwd(), ".rcz.postrelease.js"))).toString(); | ||||
|   } else if (existsSync(join(cwd(), ".rcz", "postrelease.js"))) { | ||||
|     return (await readFile(join(cwd(), ".rcz", "postrelease.js"))).toString(); | ||||
|   } else { | ||||
|     return null; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const isOnline = () => { | ||||
|   return new Promise<boolean>((resolve) => { | ||||
|     request({ method: "GET", hostname: "icanhazip.com" }, (res) => { | ||||
|       res.on("data", () => {}); | ||||
|  | ||||
|       res.on("end", () => { | ||||
|         resolve(res.statusCode === 200); | ||||
|       }); | ||||
|     }) | ||||
|       .on("error", () => { | ||||
|         resolve(false); | ||||
|       }) | ||||
|       .end(); | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| export const checkForUpdates = async () => { | ||||
|   const updateText = | ||||
|     "You are running on an outdated version of Resultium Commitizen, in order to update run\n\nnpm install -g @resultium/rcz@latest"; | ||||
|  | ||||
|   const cachedVersion = existsSync(join(tmpdir(), "rcz-server-version")) | ||||
|     ? (await readFile(join(tmpdir(), "rcz-server-version"))).toString().trim() | ||||
|     : null; | ||||
|   const localVersion = execSync("rcz --version").toString().trim(); | ||||
|  | ||||
|   // even if cached once in a while it should get newest data | ||||
|   if ((cachedVersion && Math.random() < 0.1) || cachedVersion === null) { | ||||
|     if (!(await isOnline())) return; | ||||
|  | ||||
|     const serverVersion = execSync("npm show @resultium/rcz version") | ||||
|       .toString() | ||||
|       .trim(); | ||||
|  | ||||
|     writeFile(join(tmpdir(), "rcz-server-version"), serverVersion); | ||||
|  | ||||
|     if (gt(serverVersion, localVersion)) note(updateText); | ||||
|   } else if (cachedVersion) { | ||||
|     if (gt(cachedVersion, localVersion)) note(updateText); | ||||
|   } | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user