diff --git a/README.md b/README.md index b92f3a7..2e2e389 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ git add oco ``` +Link to the GitMoji specification: https://gitmoji.dev/ + You can also run it with local model through ollama: - install and start ollama @@ -69,6 +71,17 @@ git add AI_PROVIDER='ollama' opencommit ``` +### Flags +There are multiple optional flags that can be used with the `oco` command: + +#### Use Full GitMoji Specification +This flag can only be used if the `OCO_EMOJI` configuration item is set to `true`. This flag allows users to use all emojis in the GitMoji specification, By default, the GitMoji full specification is set to `false`, which only includes 10 emojis (๐Ÿ›โœจ๐Ÿ“๐Ÿš€โœ…โ™ป๏ธโฌ†๏ธ๐Ÿ”ง๐ŸŒ๐Ÿ’ก). +This is due to limit the number of tokens sent in each request. However, if you would like to use the full GitMoji specification, you can use the `--fgm` flag. + +``` +oco --fgm +``` + ## Configuration ### Local per repo configuration diff --git a/src/cli.ts b/src/cli.ts index 5d77666..7baf6e3 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,17 +17,19 @@ cli( version: packageJSON.version, name: 'opencommit', commands: [configCommand, hookCommand, commitlintConfigCommand], - flags: {}, + flags: { + fgm: Boolean + }, ignoreArgv: (type) => type === 'unknown-flag' || type === 'argument', help: { description: packageJSON.description } }, - async () => { + async ({ flags }) => { await checkIsLatestVersion(); if (await isHookCalled()) { prepareCommitMessageHook(); } else { - commit(extraArgs); + commit(extraArgs, flags.fgm); } }, extraArgs diff --git a/src/commands/commit.ts b/src/commands/commit.ts index 10ce123..9b696ed 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -40,14 +40,15 @@ const checkMessageTemplate = (extraArgs: string[]): string | false => { const generateCommitMessageFromGitDiff = async ( diff: string, - extraArgs: string[] + extraArgs: string[], + fullGitMojiSpec: boolean ): Promise => { await assertGitRepo(); const commitSpinner = spinner(); commitSpinner.start('Generating the commit message'); try { - let commitMessage = await generateCommitMessageByDiff(diff); + let commitMessage = await generateCommitMessageByDiff(diff, fullGitMojiSpec); const messageTemplate = checkMessageTemplate(extraArgs); if ( @@ -154,7 +155,8 @@ ${chalk.grey('โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”')}` export async function commit( extraArgs: string[] = [], - isStageAllFlag: Boolean = false + fullGitMojiSpec: boolean = false, + isStageAllFlag: Boolean = false, ) { if (isStageAllFlag) { const changedFiles = await getChangedFiles(); @@ -194,7 +196,7 @@ export async function commit( isStageAllAndCommitConfirmedByUser && !isCancel(isStageAllAndCommitConfirmedByUser) ) { - await commit(extraArgs, true); + await commit(extraArgs, true, fullGitMojiSpec); process.exit(1); } @@ -212,7 +214,7 @@ export async function commit( await gitAdd({ files }); } - await commit(extraArgs, false); + await commit(extraArgs, false, fullGitMojiSpec); process.exit(1); } @@ -225,7 +227,8 @@ export async function commit( const [, generateCommitError] = await trytm( generateCommitMessageFromGitDiff( await getDiff({ files: stagedFiles }), - extraArgs + extraArgs, + fullGitMojiSpec ) ); diff --git a/src/generateCommitMessageFromGitDiff.ts b/src/generateCommitMessageFromGitDiff.ts index fe7bbfb..d953df8 100644 --- a/src/generateCommitMessageFromGitDiff.ts +++ b/src/generateCommitMessageFromGitDiff.ts @@ -14,9 +14,10 @@ const MAX_TOKENS_INPUT = config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DE const MAX_TOKENS_OUTPUT = config?.OCO_TOKENS_MAX_OUTPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT; const generateCommitMessageChatCompletionPrompt = async ( - diff: string + diff: string, + fullGitMojiSpec: boolean ): Promise> => { - const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(); + const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec); const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT]; @@ -38,10 +39,11 @@ export enum GenerateCommitMessageErrorEnum { const ADJUSTMENT_FACTOR = 20; export const generateCommitMessageByDiff = async ( - diff: string + diff: string, + fullGitMojiSpec: boolean ): Promise => { try { - const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(); + const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec); const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map( (msg) => tokenCount(msg.content) + 4 @@ -56,7 +58,8 @@ export const generateCommitMessageByDiff = async ( if (tokenCount(diff) >= MAX_REQUEST_TOKENS) { const commitMessagePromises = await getCommitMsgsPromisesFromFileDiffs( diff, - MAX_REQUEST_TOKENS + MAX_REQUEST_TOKENS, + fullGitMojiSpec ); const commitMessages = []; @@ -68,7 +71,7 @@ export const generateCommitMessageByDiff = async ( return commitMessages.join('\n\n'); } - const messages = await generateCommitMessageChatCompletionPrompt(diff); + const messages = await generateCommitMessageChatCompletionPrompt(diff, fullGitMojiSpec); const engine = getEngine() const commitMessage = await engine.generateCommitMessage(messages); @@ -85,7 +88,8 @@ export const generateCommitMessageByDiff = async ( function getMessagesPromisesByChangesInFile( fileDiff: string, separator: string, - maxChangeLength: number + maxChangeLength: number, + fullGitMojiSpec: boolean ) { const hunkHeaderSeparator = '@@ '; const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator); @@ -112,7 +116,8 @@ function getMessagesPromisesByChangesInFile( const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map( async (lineDiff) => { const messages = await generateCommitMessageChatCompletionPrompt( - separator + lineDiff + separator + lineDiff, + fullGitMojiSpec ); return engine.generateCommitMessage(messages); @@ -160,7 +165,8 @@ function splitDiff(diff: string, maxChangeLength: number) { export const getCommitMsgsPromisesFromFileDiffs = async ( diff: string, - maxDiffLength: number + maxDiffLength: number, + fullGitMojiSpec: boolean ) => { const separator = 'diff --git '; @@ -177,13 +183,15 @@ export const getCommitMsgsPromisesFromFileDiffs = async ( const messagesPromises = getMessagesPromisesByChangesInFile( fileDiff, separator, - maxDiffLength + maxDiffLength, + fullGitMojiSpec ); commitMessagePromises.push(...messagesPromises); } else { const messages = await generateCommitMessageChatCompletionPrompt( - separator + fileDiff + separator + fileDiff, + fullGitMojiSpec ); const engine = getEngine() diff --git a/src/prompts.ts b/src/prompts.ts index 243ee86..f2d35fc 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -11,6 +11,7 @@ import { configureCommitlintIntegration } from './modules/commitlint/config'; import { commitlintPrompts } from './modules/commitlint/prompts'; import { ConsistencyPrompt } from './modules/commitlint/types'; import * as utils from './modules/commitlint/utils'; +import { removeConventionalCommitWord } from './utils/removeConventionalCommitWord'; const config = getConfig(); const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en']; @@ -18,14 +19,97 @@ const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en']; export const IDENTITY = 'You are to act as the author of a commit message in git.'; -const INIT_MAIN_PROMPT = (language: string): ChatCompletionRequestMessage => ({ +const INIT_MAIN_PROMPT = ( + language: string, + fullGitMojiSpec: boolean +): ChatCompletionRequestMessage => ({ role: ChatCompletionRequestMessageRoleEnum.System, - content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the conventional commit convention and explain WHAT were the changes and mainly WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message. - ${ - config?.OCO_EMOJI - ? 'Use GitMoji convention to preface the commit.' - : 'Do not preface the commit with anything.' - } + content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the ${ + fullGitMojiSpec ? 'GitMoji specification' : 'conventional commit convention' + } and explain WHAT were the changes and mainly WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message. + ${ + config?.OCO_EMOJI + ? 'Use GitMoji convention to preface the commit. Here are some help to choose the right emoji (emoji, description): ' + + '๐Ÿ›, Fix a bug; ' + + 'โœจ, Introduce new features; ' + + '๐Ÿ“, Add or update documentation; ' + + '๐Ÿš€, Deploy stuff; ' + + 'โœ…, Add, update, or pass tests; ' + + 'โ™ป๏ธ, Refactor code; ' + + 'โฌ†๏ธ, Upgrade dependencies; ' + + '๐Ÿ”ง, Add or update configuration files; ' + + '๐ŸŒ, Internationalization and localization; ' + + '๐Ÿ’ก, Add or update comments in source code; ' + + `${ + fullGitMojiSpec + ? '๐ŸŽจ, Improve structure / format of the code; ' + + 'โšก๏ธ, Improve performance; ' + + '๐Ÿ”ฅ, Remove code or files; ' + + '๐Ÿš‘๏ธ, Critical hotfix; ' + + '๐Ÿ’„, Add or update the UI and style files; ' + + '๐ŸŽ‰, Begin a project; ' + + '๐Ÿ”’๏ธ, Fix security issues; ' + + '๐Ÿ”, Add or update secrets; ' + + '๐Ÿ”–, Release / Version tags; ' + + '๐Ÿšจ, Fix compiler / linter warnings; ' + + '๐Ÿšง, Work in progress; ' + + '๐Ÿ’š, Fix CI Build; ' + + 'โฌ‡๏ธ, Downgrade dependencies; ' + + '๐Ÿ“Œ, Pin dependencies to specific versions; ' + + '๐Ÿ‘ท, Add or update CI build system; ' + + '๐Ÿ“ˆ, Add or update analytics or track code; ' + + 'โž•, Add a dependency; ' + + 'โž–, Remove a dependency; ' + + '๐Ÿ”จ, Add or update development scripts; ' + + 'โœ๏ธ, Fix typos; ' + + '๐Ÿ’ฉ, Write bad code that needs to be improved; ' + + 'โช๏ธ, Revert changes; ' + + '๐Ÿ”€, Merge branches; ' + + '๐Ÿ“ฆ๏ธ, Add or update compiled files or packages; ' + + '๐Ÿ‘ฝ๏ธ, Update code due to external API changes; ' + + '๐Ÿšš, Move or rename resources (e.g.: files, paths, routes); ' + + '๐Ÿ“„, Add or update license; ' + + '๐Ÿ’ฅ, Introduce breaking changes; ' + + '๐Ÿฑ, Add or update assets; ' + + 'โ™ฟ๏ธ, Improve accessibility; ' + + '๐Ÿป, Write code drunkenly; ' + + '๐Ÿ’ฌ, Add or update text and literals; ' + + '๐Ÿ—ƒ๏ธ, Perform database related changes; ' + + '๐Ÿ”Š, Add or update logs; ' + + '๐Ÿ”‡, Remove logs; ' + + '๐Ÿ‘ฅ, Add or update contributor(s); ' + + '๐Ÿšธ, Improve user experience / usability; ' + + '๐Ÿ—๏ธ, Make architectural changes; ' + + '๐Ÿ“ฑ, Work on responsive design; ' + + '๐Ÿคก, Mock things; ' + + '๐Ÿฅš, Add or update an easter egg; ' + + '๐Ÿ™ˆ, Add or update a .gitignore file; ' + + '๐Ÿ“ธ, Add or update snapshots; ' + + 'โš—๏ธ, Perform experiments; ' + + '๐Ÿ”๏ธ, Improve SEO; ' + + '๐Ÿท๏ธ, Add or update types; ' + + '๐ŸŒฑ, Add or update seed files; ' + + '๐Ÿšฉ, Add, update, or remove feature flags; ' + + '๐Ÿฅ…, Catch errors; ' + + '๐Ÿ’ซ, Add or update animations and transitions; ' + + '๐Ÿ—‘๏ธ, Deprecate code that needs to be cleaned up; ' + + '๐Ÿ›‚, Work on code related to authorization, roles and permissions; ' + + '๐Ÿฉน, Simple fix for a non-critical issue; ' + + '๐Ÿง, Data exploration/inspection; ' + + 'โšฐ๏ธ, Remove dead code; ' + + '๐Ÿงช, Add a failing test; ' + + '๐Ÿ‘”, Add or update business logic; ' + + '๐Ÿฉบ, Add or update healthcheck; ' + + '๐Ÿงฑ, Infrastructure related changes; ' + + '๐Ÿง‘โ€๐Ÿ’ป, Improve developer experience; ' + + '๐Ÿ’ธ, Add sponsorships or money related infrastructure; ' + + '๐Ÿงต, Add or update code related to multithreading or concurrency; ' + + '๐Ÿฆบ, Add or update code related to validation.' + : '' + }` + : 'Do not preface the commit with anything. Conventional commit keywords:' + + 'fix, feat, build, chore, ci, docs, style, refactor, perf, test.' + } ${ config?.OCO_DESCRIPTION ? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.' @@ -66,14 +150,22 @@ const INIT_CONSISTENCY_PROMPT = ( translation: ConsistencyPrompt ): ChatCompletionRequestMessage => ({ role: ChatCompletionRequestMessageRoleEnum.Assistant, - content: `${config?.OCO_EMOJI ? '๐Ÿ› ' : ''}${translation.commitFix} -${config?.OCO_EMOJI ? 'โœจ ' : ''}${translation.commitFeat} + content: `${ + config?.OCO_EMOJI + ? `๐Ÿ› ${removeConventionalCommitWord(translation.commitFix)}` + : translation.commitFix + } +${ + config?.OCO_EMOJI + ? `โœจ ${removeConventionalCommitWord(translation.commitFeat)}` + : translation.commitFeat +} ${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}` }); -export const getMainCommitPrompt = async (): Promise< - ChatCompletionRequestMessage[] -> => { +export const getMainCommitPrompt = async ( + fullGitMojiSpec: boolean +): Promise => { switch (config?.OCO_PROMPT_MODULE) { case '@commitlint': if (!(await utils.commitlintLLMConfigExists())) { @@ -102,7 +194,7 @@ export const getMainCommitPrompt = async (): Promise< default: // conventional-commit return [ - INIT_MAIN_PROMPT(translation.localLanguage), + INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec), INIT_DIFF_PROMPT, INIT_CONSISTENCY_PROMPT(translation) ]; diff --git a/src/utils/removeConventionalCommitWord.ts b/src/utils/removeConventionalCommitWord.ts new file mode 100644 index 0000000..3010e1c --- /dev/null +++ b/src/utils/removeConventionalCommitWord.ts @@ -0,0 +1,3 @@ +export function removeConventionalCommitWord(message: string): string { + return message.replace(/^(fix|feat)\((.+?)\):/, '($2):'); +}