Compare commits

..

10 Commits

Author SHA1 Message Date
di-sukharev
0e1ad33179 1.1.26 2023-03-29 10:45:35 +08:00
di-sukharev
e7eaa5425e 1.1.25 2023-03-29 10:45:30 +08:00
di-sukharev
4b96670374 Merge branch 'dev' 2023-03-29 10:45:17 +08:00
di-sukharev
e128cdece1 refactor(generateCommitMessageFromGitDiff.ts): rename mergeStrings to mergeDiffs and add maxDiffLength parameter
feat(generateCommitMessageFromGitDiff.ts): split file diffs into changes in files if they exceed the maximum token count
The mergeStrings function was renamed to mergeDiffs to better reflect its purpose. The function now takes an additional parameter, maxDiffLength, which is used to split file diffs into changes in files if they exceed the maximum token count. This change improves the efficiency of the function and reduces the number of tokens used.
2023-03-29 10:44:45 +08:00
di-sukharev
4cc73208cd chore(.gitignore): add test.ts to the list of ignored files
fix(prepare-commit-msg-hook.ts): add missing await keyword to getStagedFiles() function call
feat(prepare-commit-msg-hook.ts): add spinner to indicate commit message generation progress
feat(utils/mergeDiffs.ts): add mergeDiffs function to merge array of strings into an array of strings with a maximum length
The test.ts file is now ignored by git. The missing await keyword has been added to the getStagedFiles() function call. A spinner has been added to indicate the progress of commit message generation. The mergeDiffs function has been added to merge an array of strings into an array of strings with a maximum length.
2023-03-29 10:43:27 +08:00
di-sukharev
ea864d18f4 refactor(generateCommitMessageFromGitDiff.ts): update INIT_MESSAGES_PROMPT to improve readability and remove unnecessary line breaks 2023-03-29 09:35:35 +08:00
di-sukharev
5d131e66fa Merge branch 'master' into dev 2023-03-29 09:34:38 +08:00
di-sukharev
bf24be92a1 chore(generateCommitMessageFromGitDiff.ts): update INIT_MESSAGES_PROMPT to clarify commit message conventions and instructions 2023-03-29 09:32:15 +08:00
Raymond
3103ae18b8 Count file diff by token, not by length of string (#63)
* 1.1.23

* 1.1.24

* feat(package.json): add @dqbd/tiktoken dependency

refactor(generateCommitMessageFromGitDiff.ts): add tokenCount function to count the number of tokens in a string
refactor(generateCommitMessageFromGitDiff.ts): change the way the length of INIT_MESSAGES_PROMPT is calculated to use tokenCount function
refactor(generateCommitMessageFromGitDiff.ts): change the way the length of diff is calculated to use tokenCount function

refactor(generateCommitMessageFromGitDiff.ts): rename function parameter from diff to fileDiff and update function calls accordingly
feat(generateCommitMessageFromGitDiff.ts): add tokenCount function to count tokens in fileDiff and use it to check if fileDiff is bigger than MAX_REQ_TOKENS

feat(utils): add tokenCount function to count the number of tokens in a string
refactor(utils/mergeStrings.ts): use tokenCount function to count the number of tokens in a string instead of checking the length of the concatenated string

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
2023-03-28 18:43:02 +08:00
R4V3N
7c9feba3ba Added Swedish Translation (#68)
* 1.1.23

* 1.1.24

* Added Swedish langauge

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
2023-03-28 18:38:49 +08:00
9 changed files with 117 additions and 63 deletions

3
.gitignore vendored
View File

@@ -11,4 +11,5 @@ logfile.log
uncaughtExceptions.log
.vscode
src/*.json
.idea
.idea
test.ts

10
package-lock.json generated
View File

@@ -1,15 +1,16 @@
{
"name": "opencommit",
"version": "1.1.24",
"version": "1.1.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "opencommit",
"version": "1.1.24",
"version": "1.1.26",
"license": "MIT",
"dependencies": {
"@clack/prompts": "^0.6.1",
"@dqbd/tiktoken": "^1.0.2",
"axios": "^1.3.4",
"chalk": "^5.2.0",
"cleye": "^1.3.2",
@@ -83,6 +84,11 @@
"node": ">=12"
}
},
"node_modules/@dqbd/tiktoken": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@dqbd/tiktoken/-/tiktoken-1.0.2.tgz",
"integrity": "sha512-AjGTBRWsMoVmVeN55NLyupyM8TNamOUBl6tj5t/leLDVup3CFGO9tVagNL1jf3GyZLkWZSTmYVbPQ/M2LEcNzw=="
},
"node_modules/@esbuild/android-arm": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "opencommit",
"version": "1.1.24",
"version": "1.1.26",
"description": "GPT CLI to auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
"keywords": [
"git",
@@ -60,6 +60,7 @@
},
"dependencies": {
"@clack/prompts": "^0.6.1",
"@dqbd/tiktoken": "^1.0.2",
"axios": "^1.3.4",
"chalk": "^5.2.0",
"cleye": "^1.3.2",

View File

@@ -1,6 +1,6 @@
import fs from 'fs/promises';
import chalk from 'chalk';
import { intro, outro } from '@clack/prompts';
import { intro, outro, spinner } from '@clack/prompts';
import { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
import { getConfig } from './config';
import { generateCommitMessageWithChatCompletion } from '../generateCommitMessageFromGitDiff';
@@ -17,11 +17,13 @@ export const prepareCommitMessageHook = async () => {
if (commitSource) return;
const stagedFiles = await getStagedFiles();
const changedFiles = await getChangedFiles();
if (changedFiles) await gitAdd({ files: changedFiles });
if (!stagedFiles && changedFiles) await gitAdd({ files: changedFiles });
else {
outro("No changes detected, write some code and run `oc` again");
process.exit(1);
outro('No changes detected, write some code and run `oc` again');
process.exit(1);
}
const staged = await getStagedFiles();
@@ -38,11 +40,15 @@ export const prepareCommitMessageHook = async () => {
);
}
const spin = spinner();
spin.start('Generating commit message');
const commitMessage = await generateCommitMessageWithChatCompletion(
await getDiff({ files: staged })
);
if (typeof commitMessage !== 'string') throw new Error(commitMessage.error);
if (typeof commitMessage !== 'string') {
spin.stop('Error');
throw new Error(commitMessage.error);
} else spin.stop('Done');
const fileContent = await fs.readFile(messageFilePath);

View File

@@ -4,8 +4,9 @@ import {
} from 'openai';
import { api } from './api';
import { getConfig } from './commands/config';
import { mergeStrings } from './utils/mergeStrings';
import { mergeDiffs } from './utils/mergeDiffs';
import { i18n, I18nLocals } from './i18n';
import { tokenCount } from './utils/tokenCount';
const config = getConfig();
const translation = i18n[(config?.language as I18nLocals) || 'en'];
@@ -13,40 +14,38 @@ const translation = i18n[(config?.language as I18nLocals) || 'en'];
const INIT_MESSAGES_PROMPT: Array<ChatCompletionRequestMessage> = [
{
role: ChatCompletionRequestMessageRoleEnum.System,
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. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message. ${
config?.emoji
? 'Use Gitmoji convention to preface the commit'
: 'Do not preface the commit with anything'
}, use the present tense. ${
config?.description
? 'Add a short description of what commit is about 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 ${translation.localLanguage} to answer.`
// 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 why a change was done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
${config?.emoji? 'Use Gitmoji convention to preface the commit.': 'Do not preface the commit with anything.'}
${config?.description ? 'Add a short description of why the commit is 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
index ad4db42..f3b18a9 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -10,7 +10,7 @@ import {
initWinstonLogger();
const app = express();
-const port = 7799;
+const PORT = 7799;
app.use(express.json());
@@ -34,6 +34,6 @@ app.use((_, res, next) => {
// ROUTES
app.use(PROTECTED_ROUTER_URL, protectedRouter);
-app.listen(port, () => {
- console.log(\`Server listening on port \${port}\`);
+app.listen(process.env.PORT || PORT, () => {
+ console.log(\`Server listening on port \${PORT}\`);
});`
index ad4db42..f3b18a9 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -10,7 +10,7 @@
import {
initWinstonLogger();
const app = express();
-const port = 7799;
+const PORT = 7799;
app.use(express.json());
@@ -34,6 +34,6 @@
app.use((_, res, next) => {
// ROUTES
app.use(PROTECTED_ROUTER_URL, protectedRouter);
-app.listen(port, () => {
- console.log(\`Server listening on port \${port}\`);
+app.listen(process.env.PORT || PORT, () => {
+ console.log(\`Server listening on port \${PORT}\`);
});`
},
{
role: ChatCompletionRequestMessageRoleEnum.Assistant,
@@ -80,8 +79,8 @@ interface GenerateCommitMessageError {
}
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
(msg) => msg.content
).join('').length;
(msg) => tokenCount(msg.content) + 4
).reduce((a, b) => a + b, 0);
const MAX_REQ_TOKENS = 3900 - INIT_MESSAGES_PROMPT_LENGTH;
@@ -89,8 +88,11 @@ export const generateCommitMessageWithChatCompletion = async (
diff: string
): Promise<string | GenerateCommitMessageError> => {
try {
if (diff.length >= MAX_REQ_TOKENS) {
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(diff);
if (tokenCount(diff) >= MAX_REQ_TOKENS) {
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(
diff,
MAX_REQ_TOKENS
);
const commitMessages = await Promise.all(commitMessagePromises);
@@ -110,22 +112,28 @@ export const generateCommitMessageWithChatCompletion = async (
}
};
function getMessagesPromisesByLines(fileDiff: string, separator: string) {
const lineSeparator = '\n@@';
const [fileHeader, ...fileDiffByLines] = fileDiff.split(lineSeparator);
function getMessagesPromisesByChangesInFile(
fileDiff: string,
separator: string,
maxChangeLength: number
) {
const hunkHeaderSeparator = '@@ ';
const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator);
// merge multiple line-diffs into 1 to save tokens
const mergedLines = mergeStrings(
fileDiffByLines.map((line) => lineSeparator + line),
MAX_REQ_TOKENS
const mergedChanges = mergeDiffs(
fileDiffByLines.map((line) => hunkHeaderSeparator + line),
maxChangeLength
);
const lineDiffsWithHeader = mergedLines.map(
(d) => fileHeader + lineSeparator + d
const lineDiffsWithHeader = mergedChanges.map(
(change) => fileHeader + change
);
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((d) => {
const messages = generateCommitMessageChatCompletionPrompt(separator + d);
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((lineDiff) => {
const messages = generateCommitMessageChatCompletionPrompt(
separator + lineDiff
);
return api.generateCommitMessage(messages);
});
@@ -133,20 +141,27 @@ function getMessagesPromisesByLines(fileDiff: string, separator: string) {
return commitMsgsFromFileLineDiffs;
}
function getCommitMsgsPromisesFromFileDiffs(diff: string) {
export function getCommitMsgsPromisesFromFileDiffs(
diff: string,
maxDiffLength: number
) {
const separator = 'diff --git ';
const diffByFiles = diff.split(separator).slice(1);
// merge multiple files-diffs into 1 prompt to save tokens
const mergedFilesDiffs = mergeStrings(diffByFiles, MAX_REQ_TOKENS);
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
const commitMessagePromises = [];
for (const fileDiff of mergedFilesDiffs) {
if (fileDiff.length >= MAX_REQ_TOKENS) {
if (tokenCount(fileDiff) >= maxDiffLength) {
// if file-diff is bigger than gpt context — split fileDiff into lineDiff
const messagesPromises = getMessagesPromisesByLines(fileDiff, separator);
const messagesPromises = getMessagesPromisesByChangesInFile(
fileDiff,
separator,
maxDiffLength
);
commitMessagePromises.push(...messagesPromises);
} else {

View File

@@ -9,6 +9,7 @@ import ja from '../i18n/ja.json' assert { type: 'json' };
import pt_br from '../i18n/pt_br.json' assert { type: 'json' };
import vi_VN from '../i18n/vi_VN.json' assert { type: 'json' };
import es_ES from '../i18n/es_ES.json' assert { type: 'json' };
import sv from '../i18n/sv.json' assert { type: 'json' };
export enum I18nLocals {
'en' = 'en',
@@ -20,7 +21,8 @@ export enum I18nLocals {
'it' = 'it',
'ko' = 'ko',
'pt_br' = 'pt_br',
'es_ES' = 'es_ES'
'es_ES' = 'es_ES',
'sv' = 'sv',
};
export const i18n = {
@@ -34,7 +36,8 @@ export const i18n = {
ko,
pt_br,
vi_VN,
es_ES
es_ES,
sv
};
export const I18N_CONFIG_ALIAS: { [key: string]: string[] } = {
@@ -49,6 +52,7 @@ export const I18N_CONFIG_ALIAS: { [key: string]: string[] } = {
vi_VN: ['vi_VN', 'Vietnamese', 'tiếng Việt'],
en: ['en', 'English', 'english'],
es_ES: ['es_ES', 'Spanish', 'español'],
sv: ['sv', 'Swedish', 'Svenska'],
};
export function getI18nLocal(value: string): string | boolean {

6
src/i18n/sv.json Normal file
View File

@@ -0,0 +1,6 @@
{
"localLanguage": "svenska",
"commitFix": "fixa(server.ts): ändra variabelnamnet för port från små bokstäver till stora bokstäver PORT",
"commitFeat": "nyhet(server.ts): lägg till stöd för process.env.PORT miljövariabel",
"commitDescription": "Variabeln som innehåller portnumret heter nu PORT vilket förbättrar konsekvensen med namngivningskonventionerna eftersom PORT är en konstant. Stöd för en miljövariabel gör att applikationen kan vara mer flexibel då den nu kan köras på vilken port som helst som specificeras via miljövariabeln process.env.PORT."
}

View File

@@ -1,8 +1,9 @@
export function mergeStrings(arr: string[], maxStringLength: number): string[] {
import { tokenCount } from './tokenCount';
export function mergeDiffs(arr: string[], maxStringLength: number): string[] {
const mergedArr: string[] = [];
let currentItem: string = arr[0];
for (const item of arr.slice(1)) {
if (currentItem.length + item.length <= maxStringLength) {
if (tokenCount(currentItem + item) <= maxStringLength) {
currentItem += item;
} else {
mergedArr.push(currentItem);

14
src/utils/tokenCount.ts Normal file
View File

@@ -0,0 +1,14 @@
import { Tiktoken } from "@dqbd/tiktoken/lite"
import cl100k_base from "@dqbd/tiktoken/encoders/cl100k_base.json" assert{type: "json"}
export function tokenCount(content: string): number {
const encoding = new Tiktoken(
cl100k_base.bpe_ranks,
cl100k_base.special_tokens,
cl100k_base.pat_str
);
const tokens = encoding.encode(content);
encoding.free();
return tokens.length;
}