mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-01-14 16:18:02 -05:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
792ab67ef1 | ||
|
|
a7af55df37 | ||
|
|
5c540abae9 | ||
|
|
f69e716dcc | ||
|
|
1d8d8e57c2 | ||
|
|
fdc638cd86 | ||
|
|
56e02f2604 | ||
|
|
b926a627a8 | ||
|
|
32f3e176f0 | ||
|
|
cf4212016f |
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"trailingComma": "none",
|
||||||
"trailingComma": "none"
|
"singleQuote": true
|
||||||
}
|
}
|
||||||
|
|||||||
98
out/cli.cjs
98
out/cli.cjs
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
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "2.2.6",
|
"version": "2.2.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "2.2.6",
|
"version": "2.2.9",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "2.2.6",
|
"version": "2.2.9",
|
||||||
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
|
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"git",
|
"git",
|
||||||
|
|||||||
12
src/api.ts
12
src/api.ts
@@ -7,7 +7,9 @@ import {
|
|||||||
OpenAIApi
|
OpenAIApi
|
||||||
} from 'openai';
|
} 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';
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
@@ -56,6 +58,14 @@ class OpenAi {
|
|||||||
max_tokens: maxTokens || 500
|
max_tokens: maxTokens || 500
|
||||||
};
|
};
|
||||||
try {
|
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 { data } = await this.openAI.createChatCompletion(params);
|
||||||
|
|
||||||
const message = data.choices[0].message;
|
const message = data.choices[0].message;
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export enum CONFIG_KEYS {
|
|||||||
OCO_LANGUAGE = 'OCO_LANGUAGE'
|
OCO_LANGUAGE = 'OCO_LANGUAGE'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_MODEL_TOKEN_LIMIT = 4096;
|
||||||
|
|
||||||
export enum CONFIG_MODES {
|
export enum CONFIG_MODES {
|
||||||
get = 'get',
|
get = 'get',
|
||||||
set = 'set'
|
set = 'set'
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import {
|
import {
|
||||||
ChatCompletionRequestMessage,
|
ChatCompletionRequestMessage,
|
||||||
ChatCompletionRequestMessageRoleEnum
|
ChatCompletionRequestMessageRoleEnum
|
||||||
} from 'openai';
|
} from 'openai';
|
||||||
import { api } from './api';
|
import {api} from './api';
|
||||||
import { getConfig } from './commands/config';
|
import {DEFAULT_MODEL_TOKEN_LIMIT, getConfig} from './commands/config';
|
||||||
import { mergeDiffs } from './utils/mergeDiffs';
|
import {mergeDiffs} from './utils/mergeDiffs';
|
||||||
import { i18n, I18nLocals } from './i18n';
|
import {i18n, I18nLocals} from './i18n';
|
||||||
import { tokenCount } from './utils/tokenCount';
|
import {tokenCount} from './utils/tokenCount';
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
|
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
|
||||||
|
|
||||||
const INIT_MESSAGES_PROMPT: Array<ChatCompletionRequestMessage> = [
|
const INIT_MESSAGES_PROMPT: Array<ChatCompletionRequestMessage> = [
|
||||||
{
|
{
|
||||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||||
// prettier-ignore
|
// 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.
|
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_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."}
|
${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.`
|
Use the present tense. Lines must not be longer than 74 characters. Use ${translation.localLanguage} to answer.`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: ChatCompletionRequestMessageRoleEnum.User,
|
role: ChatCompletionRequestMessageRoleEnum.User,
|
||||||
content: `diff --git a/src/server.ts b/src/server.ts
|
content: `diff --git a/src/server.ts b/src/server.ts
|
||||||
index ad4db42..f3b18a9 100644
|
index ad4db42..f3b18a9 100644
|
||||||
--- a/src/server.ts
|
--- a/src/server.ts
|
||||||
+++ b/src/server.ts
|
+++ b/src/server.ts
|
||||||
@@ -46,128 +46,183 @@ app.use((_, res, next) => {
|
|||||||
+app.listen(process.env.PORT || PORT, () => {
|
+app.listen(process.env.PORT || PORT, () => {
|
||||||
+ console.log(\`Server listening on port \${PORT}\`);
|
+ console.log(\`Server listening on port \${PORT}\`);
|
||||||
});`
|
});`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||||
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
|
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
|
||||||
${config?.OCO_EMOJI ? '✨ ' : ''}${translation.commitFeat}
|
${config?.OCO_EMOJI ? '✨ ' : ''}${translation.commitFeat}
|
||||||
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
|
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const generateCommitMessageChatCompletionPrompt = (
|
const generateCommitMessageChatCompletionPrompt = (
|
||||||
diff: string
|
diff: string
|
||||||
): Array<ChatCompletionRequestMessage> => {
|
): Array<ChatCompletionRequestMessage> => {
|
||||||
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
|
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
|
||||||
|
|
||||||
chatContextAsCompletionRequest.push({
|
chatContextAsCompletionRequest.push({
|
||||||
role: ChatCompletionRequestMessageRoleEnum.User,
|
role: ChatCompletionRequestMessageRoleEnum.User,
|
||||||
content: diff
|
content: diff
|
||||||
});
|
});
|
||||||
|
|
||||||
return chatContextAsCompletionRequest;
|
return chatContextAsCompletionRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum GenerateCommitMessageErrorEnum {
|
export enum GenerateCommitMessageErrorEnum {
|
||||||
tooMuchTokens = 'TOO_MUCH_TOKENS',
|
tooMuchTokens = 'TOO_MUCH_TOKENS',
|
||||||
internalError = 'INTERNAL_ERROR',
|
internalError = 'INTERNAL_ERROR',
|
||||||
emptyMessage = 'EMPTY_MESSAGE'
|
emptyMessage = 'EMPTY_MESSAGE'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
|
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);
|
).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
const MAX_REQ_TOKENS = 3000 - INIT_MESSAGES_PROMPT_LENGTH;
|
const ADJUSTMENT_FACTOR = 20;
|
||||||
|
|
||||||
export const generateCommitMessageByDiff = async (
|
export const generateCommitMessageByDiff = async (
|
||||||
diff: string
|
diff: string
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
if (tokenCount(diff) >= MAX_REQ_TOKENS) {
|
const MAX_REQUEST_TOKENS = DEFAULT_MODEL_TOKEN_LIMIT
|
||||||
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(
|
- ADJUSTMENT_FACTOR
|
||||||
diff,
|
- INIT_MESSAGES_PROMPT_LENGTH
|
||||||
MAX_REQ_TOKENS
|
- 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');
|
const commitMessages = [];
|
||||||
} else {
|
for (const promise of commitMessagePromises) {
|
||||||
const messages = generateCommitMessageChatCompletionPrompt(diff);
|
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)
|
const commitMessage = await api.generateCommitMessage(messages);
|
||||||
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
|
|
||||||
|
|
||||||
return commitMessage;
|
if (!commitMessage)
|
||||||
|
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
|
||||||
|
|
||||||
|
return commitMessage;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getMessagesPromisesByChangesInFile(
|
function getMessagesPromisesByChangesInFile(
|
||||||
fileDiff: string,
|
fileDiff: string,
|
||||||
separator: string,
|
separator: string,
|
||||||
maxChangeLength: number
|
maxChangeLength: number
|
||||||
) {
|
) {
|
||||||
const hunkHeaderSeparator = '@@ ';
|
const hunkHeaderSeparator = '@@ ';
|
||||||
const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator);
|
const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator);
|
||||||
|
|
||||||
// merge multiple line-diffs into 1 to save tokens
|
// merge multiple line-diffs into 1 to save tokens
|
||||||
const mergedChanges = mergeDiffs(
|
const mergedChanges = mergeDiffs(
|
||||||
fileDiffByLines.map((line) => hunkHeaderSeparator + line),
|
fileDiffByLines.map((line) => hunkHeaderSeparator + line),
|
||||||
maxChangeLength
|
maxChangeLength
|
||||||
);
|
|
||||||
|
|
||||||
const lineDiffsWithHeader = mergedChanges.map(
|
|
||||||
(change) => fileHeader + change
|
|
||||||
);
|
|
||||||
|
|
||||||
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((lineDiff) => {
|
|
||||||
const messages = generateCommitMessageChatCompletionPrompt(
|
|
||||||
separator + lineDiff
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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(
|
export function getCommitMsgsPromisesFromFileDiffs(
|
||||||
diff: string,
|
diff: string,
|
||||||
maxDiffLength: number
|
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
|
// merge multiple files-diffs into 1 prompt to save tokens
|
||||||
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
|
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
|
||||||
|
|
||||||
const commitMessagePromises = [];
|
const commitMessagePromises = [];
|
||||||
|
|
||||||
for (const fileDiff of mergedFilesDiffs) {
|
for (const fileDiff of mergedFilesDiffs) {
|
||||||
if (tokenCount(fileDiff) >= maxDiffLength) {
|
if (tokenCount(fileDiff) >= maxDiffLength) {
|
||||||
// if file-diff is bigger than gpt context — split fileDiff into lineDiff
|
// if file-diff is bigger than gpt context — split fileDiff into lineDiff
|
||||||
const messagesPromises = getMessagesPromisesByChangesInFile(
|
const messagesPromises = getMessagesPromisesByChangesInFile(
|
||||||
fileDiff,
|
fileDiff,
|
||||||
separator,
|
separator,
|
||||||
maxDiffLength
|
maxDiffLength
|
||||||
);
|
);
|
||||||
|
|
||||||
commitMessagePromises.push(...messagesPromises);
|
commitMessagePromises.push(...messagesPromises);
|
||||||
} else {
|
} else {
|
||||||
const messages = generateCommitMessageChatCompletionPrompt(
|
const messages = generateCommitMessageChatCompletionPrompt(
|
||||||
separator + fileDiff
|
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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getOpenCommitLatestVersion } from '../api';
|
import { getOpenCommitLatestVersion } from '../api';
|
||||||
import currentPackage from '../../package.json' assert { type: 'json' };
|
import currentPackage from '../../package.json' assert { type: 'json' };
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { outro } from '@clack/prompts';
|
||||||
|
|
||||||
export const checkIsLatestVersion = async () => {
|
export const checkIsLatestVersion = async () => {
|
||||||
const latestVersion = await getOpenCommitLatestVersion();
|
const latestVersion = await getOpenCommitLatestVersion();
|
||||||
@@ -9,7 +10,7 @@ export const checkIsLatestVersion = async () => {
|
|||||||
const currentVersion = currentPackage.version;
|
const currentVersion = currentPackage.version;
|
||||||
|
|
||||||
if (currentVersion !== latestVersion) {
|
if (currentVersion !== latestVersion) {
|
||||||
console.warn(
|
outro(
|
||||||
chalk.yellow(
|
chalk.yellow(
|
||||||
`
|
`
|
||||||
You are not using the latest stable version of OpenCommit with new features and bug fixes.
|
You are not using the latest stable version of OpenCommit with new features and bug fixes.
|
||||||
|
|||||||
Reference in New Issue
Block a user