Compare commits

..

16 Commits

Author SHA1 Message Date
di-sukharev
cad179953a 2.2.10 2023-05-26 13:14:42 +08:00
di-sukharev
8979841010 refactor(api.ts): reformat imports to be grouped and sorted alphabetically
refactor(api.ts): replace axios with execa to get the latest version of opencommit from npm registry
2023-05-26 13:14:33 +08:00
di-sukharev
1e974086d3 build 2023-05-26 13:12:07 +08:00
di-sukharev
792ab67ef1 2.2.9 2023-05-26 13:12:00 +08:00
di-sukharev
a7af55df37 feat(checkIsLatestVersion.ts): add outro message from @clack/prompts to warn user about not using the latest stable version of OpenCommit 2023-05-26 13:11:51 +08:00
di-sukharev
5c540abae9 build 2023-05-26 13:10:41 +08:00
di-sukharev
f69e716dcc 2.2.8 2023-05-26 13:10:29 +08:00
di-sukharev
1d8d8e57c2 style(.prettierrc): reorder properties to follow alphabetical order 2023-05-26 13:09:53 +08:00
di-sukharev
fdc638cd86 build 2023-05-26 13:08:19 +08:00
di-sukharev
56e02f2604 2.2.7 2023-05-26 13:08:08 +08:00
di-sukharev
b926a627a8 Merge remote-tracking branch 'origin/dev' 2023-05-26 13:07:42 +08:00
Gabriel Moreli
32f3e176f0 feat(api.ts): solving bad request issue (#187)
* 2.0.18

* patch

* 2.0.19

* style(.prettierrc): reorder properties to follow alphabetical order and improve readability

* feat(generateCommitMessageFromGitDiff.ts): changing logic of MAX_REQ_TOKENS

fix(api.ts): add missing import for GenerateCommitMessageErrorEnum
The token count validation is added to prevent the request from exceeding the default model token limit. The validation is done by counting the tokens in each message and adding 4 to each count to account for the additional tokens added by the API. If the total token count exceeds the limit, an error is thrown. The missing import for GenerateCommitMessageErrorEnum is also added.

feat: add support for splitting long line-diffs into smaller pieces
This change adds support for splitting long line-diffs into smaller pieces to avoid exceeding the maximum commit message length. The `splitDiff` function splits a single line into multiple lines if it exceeds the maximum length. It also splits the diff into smaller pieces if adding the next line would exceed the maximum length. This change improves the readability of commit messages and makes them more consistent.

refactor: improve code readability by adding whitespace and reformatting code
This commit improves the readability of the code by adding whitespace and reformatting the code. The changes do not affect the functionality of the code. Additionally, a new function `delay` has been added to the file.

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
2023-05-26 13:07:09 +08:00
di-sukharev
cf4212016f build 2023-05-26 12:57:44 +08:00
di-sukharev
c491fa4bad 2.2.6 2023-05-26 12:57:38 +08:00
di-sukharev
f10fc37fe7 refactor(CommandsEnum.ts): reorder COMMANDS enum values to improve readability and maintainability 2023-05-26 12:57:20 +08:00
di-sukharev
f7b1a6358f build 2023-05-26 12:55:27 +08:00
10 changed files with 325 additions and 165 deletions

View File

@@ -1,4 +1,4 @@
{
"singleQuote": true,
"trailingComma": "none"
"trailingComma": "none",
"singleQuote": true
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "opencommit",
"version": "2.2.5",
"version": "2.2.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "opencommit",
"version": "2.2.5",
"version": "2.2.10",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",

View File

@@ -1,6 +1,6 @@
{
"name": "opencommit",
"version": "2.2.5",
"version": "2.2.10",
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
"keywords": [
"git",
@@ -41,7 +41,7 @@
"start": "node ./out/cli.cjs",
"dev": "ts-node ./src/cli.ts",
"build": "rimraf out && node esbuild.config.js",
"deploy": "npm version patch && npm run build:push && git push --tags && npm publish --tag latest",
"deploy": "npm run build:push && git push --tags && npm publish --tag latest",
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
"lint": "eslint src --ext ts && tsc --noEmit",
"format": "prettier --write src"

View File

@@ -1,4 +1,4 @@
export enum COMMANDS {
hook = 'hook',
config = 'config'
config = 'config',
hook = 'hook'
}

View File

@@ -7,7 +7,14 @@ import {
OpenAIApi
} from 'openai';
import { CONFIG_MODES, getConfig } from './commands/config';
import {
CONFIG_MODES,
DEFAULT_MODEL_TOKEN_LIMIT,
getConfig
} from './commands/config';
import { tokenCount } from './utils/tokenCount';
import { GenerateCommitMessageErrorEnum } from './generateCommitMessageFromGitDiff';
import { execa } from 'execa';
const config = getConfig();
@@ -56,6 +63,14 @@ class OpenAi {
max_tokens: maxTokens || 500
};
try {
const REQUEST_TOKENS = messages
.map((msg) => tokenCount(msg.content) + 4)
.reduce((a, b) => a + b, 0);
if (REQUEST_TOKENS > DEFAULT_MODEL_TOKEN_LIMIT - maxTokens) {
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
}
const { data } = await this.openAI.createChatCompletion(params);
const message = data.choices[0].message;
@@ -88,10 +103,8 @@ export const getOpenCommitLatestVersion = async (): Promise<
string | undefined
> => {
try {
const { data } = await axios.get(
'https://unpkg.com/opencommit/package.json'
);
return data.version;
const { stdout } = await execa('npm', ['view', 'opencommit', 'version']);
return stdout;
} catch (_) {
outro('Error while getting the latest version of opencommit');
return undefined;

View File

@@ -22,6 +22,8 @@ export enum CONFIG_KEYS {
OCO_LANGUAGE = 'OCO_LANGUAGE'
}
export const DEFAULT_MODEL_TOKEN_LIMIT = 4096;
export enum CONFIG_MODES {
get = 'get',
set = 'set'

View File

@@ -1,28 +1,28 @@
import {
ChatCompletionRequestMessage,
ChatCompletionRequestMessageRoleEnum
ChatCompletionRequestMessage,
ChatCompletionRequestMessageRoleEnum
} from 'openai';
import { api } from './api';
import { getConfig } from './commands/config';
import { mergeDiffs } from './utils/mergeDiffs';
import { i18n, I18nLocals } from './i18n';
import { tokenCount } from './utils/tokenCount';
import {api} from './api';
import {DEFAULT_MODEL_TOKEN_LIMIT, getConfig} from './commands/config';
import {mergeDiffs} from './utils/mergeDiffs';
import {i18n, I18nLocals} from './i18n';
import {tokenCount} from './utils/tokenCount';
const config = getConfig();
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
const INIT_MESSAGES_PROMPT: Array<ChatCompletionRequestMessage> = [
{
role: ChatCompletionRequestMessageRoleEnum.System,
// prettier-ignore
content: `You are to act as the author of a commit message in git. Your mission is to create clean and comprehensive commit messages in the conventional commit convention and explain WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.': 'Do not preface the commit with anything.'}
${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.': "Don't add any descriptions to the commit, only commit message."}
{
role: ChatCompletionRequestMessageRoleEnum.System,
// prettier-ignore
content: `You are to act as the author of a commit message in git. Your mission is to create clean and comprehensive commit messages in the conventional commit convention and explain WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.' : 'Do not preface the commit with anything.'}
${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.' : "Don't add any descriptions to the commit, only commit message."}
Use the present tense. Lines must not be longer than 74 characters. Use ${translation.localLanguage} to answer.`
},
{
role: ChatCompletionRequestMessageRoleEnum.User,
content: `diff --git a/src/server.ts b/src/server.ts
},
{
role: ChatCompletionRequestMessageRoleEnum.User,
content: `diff --git a/src/server.ts b/src/server.ts
index ad4db42..f3b18a9 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -46,128 +46,183 @@ app.use((_, res, next) => {
+app.listen(process.env.PORT || PORT, () => {
+ console.log(\`Server listening on port \${PORT}\`);
});`
},
{
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
},
{
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
${config?.OCO_EMOJI ? '✨ ' : ''}${translation.commitFeat}
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
}
}
];
const generateCommitMessageChatCompletionPrompt = (
diff: string
diff: string
): Array<ChatCompletionRequestMessage> => {
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
chatContextAsCompletionRequest.push({
role: ChatCompletionRequestMessageRoleEnum.User,
content: diff
});
chatContextAsCompletionRequest.push({
role: ChatCompletionRequestMessageRoleEnum.User,
content: diff
});
return chatContextAsCompletionRequest;
return chatContextAsCompletionRequest;
};
export enum GenerateCommitMessageErrorEnum {
tooMuchTokens = 'TOO_MUCH_TOKENS',
internalError = 'INTERNAL_ERROR',
emptyMessage = 'EMPTY_MESSAGE'
tooMuchTokens = 'TOO_MUCH_TOKENS',
internalError = 'INTERNAL_ERROR',
emptyMessage = 'EMPTY_MESSAGE'
}
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
(msg) => tokenCount(msg.content) + 4
(msg) => tokenCount(msg.content) + 4
).reduce((a, b) => a + b, 0);
const MAX_REQ_TOKENS = 3000 - INIT_MESSAGES_PROMPT_LENGTH;
const ADJUSTMENT_FACTOR = 20;
export const generateCommitMessageByDiff = async (
diff: string
diff: string
): Promise<string> => {
try {
if (tokenCount(diff) >= MAX_REQ_TOKENS) {
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(
diff,
MAX_REQ_TOKENS
);
try {
const MAX_REQUEST_TOKENS = DEFAULT_MODEL_TOKEN_LIMIT
- ADJUSTMENT_FACTOR
- INIT_MESSAGES_PROMPT_LENGTH
- config?.OCO_OPENAI_MAX_TOKENS;
const commitMessages = await Promise.all(commitMessagePromises);
if (tokenCount(diff) >= MAX_REQUEST_TOKENS) {
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(
diff,
MAX_REQUEST_TOKENS
);
return commitMessages.join('\n\n');
} else {
const messages = generateCommitMessageChatCompletionPrompt(diff);
const commitMessages = [];
for (const promise of commitMessagePromises) {
commitMessages.push(await promise);
await delay(2000);
}
const commitMessage = await api.generateCommitMessage(messages);
return commitMessages.join('\n\n');
} else {
const messages = generateCommitMessageChatCompletionPrompt(diff);
if (!commitMessage)
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
const commitMessage = await api.generateCommitMessage(messages);
return commitMessage;
if (!commitMessage)
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
return commitMessage;
}
} catch (error) {
throw error;
}
} catch (error) {
throw error;
}
};
function getMessagesPromisesByChangesInFile(
fileDiff: string,
separator: string,
maxChangeLength: number
fileDiff: string,
separator: string,
maxChangeLength: number
) {
const hunkHeaderSeparator = '@@ ';
const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator);
const hunkHeaderSeparator = '@@ ';
const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator);
// merge multiple line-diffs into 1 to save tokens
const mergedChanges = mergeDiffs(
fileDiffByLines.map((line) => hunkHeaderSeparator + line),
maxChangeLength
);
const lineDiffsWithHeader = mergedChanges.map(
(change) => fileHeader + change
);
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((lineDiff) => {
const messages = generateCommitMessageChatCompletionPrompt(
separator + lineDiff
// merge multiple line-diffs into 1 to save tokens
const mergedChanges = mergeDiffs(
fileDiffByLines.map((line) => hunkHeaderSeparator + line),
maxChangeLength
);
return api.generateCommitMessage(messages);
});
const lineDiffsWithHeader = [];
for (const change of mergedChanges) {
const totalChange = fileHeader + change;
if (tokenCount(totalChange) > maxChangeLength) {
// If the totalChange is too large, split it into smaller pieces
const splitChanges = splitDiff(totalChange, maxChangeLength);
lineDiffsWithHeader.push(...splitChanges);
} else {
lineDiffsWithHeader.push(totalChange);
}
}
return commitMsgsFromFileLineDiffs;
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((lineDiff) => {
const messages = generateCommitMessageChatCompletionPrompt(
separator + lineDiff
);
return api.generateCommitMessage(messages);
});
return commitMsgsFromFileLineDiffs;
}
function splitDiff(diff: string, maxChangeLength: number) {
const lines = diff.split('\n');
const splitDiffs = [];
let currentDiff = '';
for (let line of lines) {
// If a single line exceeds maxChangeLength, split it into multiple lines
while (tokenCount(line) > maxChangeLength) {
const subLine = line.substring(0, maxChangeLength);
line = line.substring(maxChangeLength);
splitDiffs.push(subLine);
}
// Check the tokenCount of the currentDiff and the line separately
if (tokenCount(currentDiff) + tokenCount('\n' + line) > maxChangeLength) {
// If adding the next line would exceed the maxChangeLength, start a new diff
splitDiffs.push(currentDiff);
currentDiff = line;
} else {
// Otherwise, add the line to the current diff
currentDiff += '\n' + line;
}
}
// Add the last diff
if (currentDiff) {
splitDiffs.push(currentDiff);
}
return splitDiffs;
}
export function getCommitMsgsPromisesFromFileDiffs(
diff: string,
maxDiffLength: number
diff: string,
maxDiffLength: number
) {
const separator = 'diff --git ';
const separator = 'diff --git ';
const diffByFiles = diff.split(separator).slice(1);
const diffByFiles = diff.split(separator).slice(1);
// merge multiple files-diffs into 1 prompt to save tokens
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
// merge multiple files-diffs into 1 prompt to save tokens
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
const commitMessagePromises = [];
const commitMessagePromises = [];
for (const fileDiff of mergedFilesDiffs) {
if (tokenCount(fileDiff) >= maxDiffLength) {
// if file-diff is bigger than gpt context — split fileDiff into lineDiff
const messagesPromises = getMessagesPromisesByChangesInFile(
fileDiff,
separator,
maxDiffLength
);
for (const fileDiff of mergedFilesDiffs) {
if (tokenCount(fileDiff) >= maxDiffLength) {
// if file-diff is bigger than gpt context — split fileDiff into lineDiff
const messagesPromises = getMessagesPromisesByChangesInFile(
fileDiff,
separator,
maxDiffLength
);
commitMessagePromises.push(...messagesPromises);
} else {
const messages = generateCommitMessageChatCompletionPrompt(
separator + fileDiff
);
commitMessagePromises.push(...messagesPromises);
} else {
const messages = generateCommitMessageChatCompletionPrompt(
separator + fileDiff
);
commitMessagePromises.push(api.generateCommitMessage(messages));
commitMessagePromises.push(api.generateCommitMessage(messages));
}
}
}
return commitMessagePromises;
return commitMessagePromises;
}
function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -1,6 +1,7 @@
import { getOpenCommitLatestVersion } from '../api';
import currentPackage from '../../package.json' assert { type: 'json' };
import chalk from 'chalk';
import { outro } from '@clack/prompts';
export const checkIsLatestVersion = async () => {
const latestVersion = await getOpenCommitLatestVersion();
@@ -9,7 +10,7 @@ export const checkIsLatestVersion = async () => {
const currentVersion = currentPackage.version;
if (currentVersion !== latestVersion) {
console.warn(
outro(
chalk.yellow(
`
You are not using the latest stable version of OpenCommit with new features and bug fixes.