From 8e51587260aff2a4fc107b888c6477319bc89bb2 Mon Sep 17 00:00:00 2001 From: Olivers Vitins Date: Sun, 20 Aug 2023 03:15:10 +0300 Subject: [PATCH] feat(commands): add release and changelog commands --- src/index.ts | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/types.ts | 10 +++ 2 files changed, 217 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 53c413f..6d0244d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,8 +12,8 @@ import { } from "@clack/prompts"; import fs from "fs"; import path from "path"; -import { Config } from "./types"; -import simpleGit from "simple-git"; +import { CommitStack, Config } from "./types"; +import simpleGit, { DefaultLogFields, ListLogLine } from "simple-git"; import { Command } from "commander"; const GetConfig = async () => { @@ -187,4 +187,209 @@ program outro("Finished creating a conventional commit, feel free to push"); }); +program + .command("changelog") + .description("Outputs a markdown formatted changelog") + .option("--show-hashes", "show first 9 characters of commit hashes") + .option("--last-only", "display only latest release changes") + .action(async (options) => { + const showHashes = options.showHashes ? true : false; + const lastOnly = options.lastOnly ? true : false; + + 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 = []; + + console.log("# Changelog"); + + for (const commit of commits) { + const tag = (await simpleGit().tags([`--contains=${commit.hash}`])) + .latest!; + const currentCommitStack = parsedCommitStacks.find( + (commitStack) => commitStack.version === tag + ) || { + version: tag || "", + 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") + .description( + "Changes package.json version and creates a new commit with a tag" + ) + .argument("", "new version formatted in SemVer") + .action(async (string: string) => { + const version = string.replace("v", ""); + 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() + .commit(`chore(release): v${version}`) + .addTag(`v${version}`); + }); + program.parse(); diff --git a/src/types.ts b/src/types.ts index e8961bb..611218f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import { DefaultLogFields, ListLogLine } from "simple-git"; + export interface Config { types?: Array; scopes?: Array; @@ -8,3 +10,11 @@ export interface Type { value: string; hint: string; } + +export interface CommitStack { + version: string; + breaking: Array; + features: Array; + fixes: Array; + miscellaneous: Array; +}