/* 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 . */ import { Command } from "commander"; import { checkForUpdates, getConfig } from "../utils/functions"; import { cancel, confirm, intro, isCancel, note, outro, select, text, } from "@clack/prompts"; import { existsSync } from "fs"; import { join } from "path"; import simpleGit 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); } 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;