Merge branch 'dev'
This commit is contained in:
+2
-1
@@ -11,4 +11,5 @@ logfile.log
|
|||||||
uncaughtExceptions.log
|
uncaughtExceptions.log
|
||||||
.vscode
|
.vscode
|
||||||
src/*.json
|
src/*.json
|
||||||
.idea
|
.idea
|
||||||
|
test.ts
|
||||||
Generated
+6
@@ -10,6 +10,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.6.1",
|
"@clack/prompts": "^0.6.1",
|
||||||
|
"@dqbd/tiktoken": "^1.0.2",
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.3.4",
|
||||||
"chalk": "^5.2.0",
|
"chalk": "^5.2.0",
|
||||||
"cleye": "^1.3.2",
|
"cleye": "^1.3.2",
|
||||||
@@ -83,6 +84,11 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.15.18",
|
"version": "0.15.18",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.6.1",
|
"@clack/prompts": "^0.6.1",
|
||||||
|
"@dqbd/tiktoken": "^1.0.2",
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.3.4",
|
||||||
"chalk": "^5.2.0",
|
"chalk": "^5.2.0",
|
||||||
"cleye": "^1.3.2",
|
"cleye": "^1.3.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import chalk from 'chalk';
|
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 { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
|
||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
import { generateCommitMessageWithChatCompletion } from '../generateCommitMessageFromGitDiff';
|
import { generateCommitMessageWithChatCompletion } from '../generateCommitMessageFromGitDiff';
|
||||||
@@ -17,11 +17,13 @@ export const prepareCommitMessageHook = async () => {
|
|||||||
|
|
||||||
if (commitSource) return;
|
if (commitSource) return;
|
||||||
|
|
||||||
|
const stagedFiles = await getStagedFiles();
|
||||||
const changedFiles = await getChangedFiles();
|
const changedFiles = await getChangedFiles();
|
||||||
if (changedFiles) await gitAdd({ files: changedFiles });
|
|
||||||
|
if (!stagedFiles && changedFiles) await gitAdd({ files: changedFiles });
|
||||||
else {
|
else {
|
||||||
outro("No changes detected, write some code and run `oc` again");
|
outro('No changes detected, write some code and run `oc` again');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const staged = await getStagedFiles();
|
const staged = await getStagedFiles();
|
||||||
@@ -38,11 +40,15 @@ export const prepareCommitMessageHook = async () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const spin = spinner();
|
||||||
|
spin.start('Generating commit message');
|
||||||
const commitMessage = await generateCommitMessageWithChatCompletion(
|
const commitMessage = await generateCommitMessageWithChatCompletion(
|
||||||
await getDiff({ files: staged })
|
await getDiff({ files: staged })
|
||||||
);
|
);
|
||||||
|
if (typeof commitMessage !== 'string') {
|
||||||
if (typeof commitMessage !== 'string') throw new Error(commitMessage.error);
|
spin.stop('Error');
|
||||||
|
throw new Error(commitMessage.error);
|
||||||
|
} else spin.stop('Done');
|
||||||
|
|
||||||
const fileContent = await fs.readFile(messageFilePath);
|
const fileContent = await fs.readFile(messageFilePath);
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import {
|
|||||||
} from 'openai';
|
} from 'openai';
|
||||||
import { api } from './api';
|
import { api } from './api';
|
||||||
import { getConfig } from './commands/config';
|
import { getConfig } from './commands/config';
|
||||||
import { mergeStrings } from './utils/mergeStrings';
|
import { mergeDiffs } from './utils/mergeDiffs';
|
||||||
import { i18n, I18nLocals } from './i18n';
|
import { i18n, I18nLocals } from './i18n';
|
||||||
|
import { tokenCount } from './utils/tokenCount';
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const translation = i18n[(config?.language as I18nLocals) || 'en'];
|
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> = [
|
const INIT_MESSAGES_PROMPT: Array<ChatCompletionRequestMessage> = [
|
||||||
{
|
{
|
||||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
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. ${
|
// prettier-ignore
|
||||||
config?.emoji
|
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.
|
||||||
? 'Use Gitmoji convention to preface the commit'
|
${config?.emoji? 'Use Gitmoji convention to preface the commit.': 'Do not preface the commit with anything.'}
|
||||||
: '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. ${
|
Use the present tense. Lines must not be longer than 74 characters. Use ${translation.localLanguage} to answer.`
|
||||||
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.`
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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
|
||||||
@@ -10,7 +10,7 @@ import {
|
@@ -10,7 +10,7 @@
|
||||||
initWinstonLogger();
|
import {
|
||||||
|
initWinstonLogger();
|
||||||
const app = express();
|
|
||||||
-const port = 7799;
|
const app = express();
|
||||||
+const PORT = 7799;
|
-const port = 7799;
|
||||||
|
+const PORT = 7799;
|
||||||
app.use(express.json());
|
|
||||||
|
app.use(express.json());
|
||||||
@@ -34,6 +34,6 @@ app.use((_, res, next) => {
|
|
||||||
// ROUTES
|
@@ -34,6 +34,6 @@
|
||||||
app.use(PROTECTED_ROUTER_URL, protectedRouter);
|
app.use((_, res, next) => {
|
||||||
|
// ROUTES
|
||||||
-app.listen(port, () => {
|
app.use(PROTECTED_ROUTER_URL, protectedRouter);
|
||||||
- console.log(\`Server listening on port \${port}\`);
|
|
||||||
+app.listen(process.env.PORT || PORT, () => {
|
-app.listen(port, () => {
|
||||||
+ console.log(\`Server listening on port \${PORT}\`);
|
- console.log(\`Server listening on port \${port}\`);
|
||||||
});`
|
+app.listen(process.env.PORT || PORT, () => {
|
||||||
|
+ console.log(\`Server listening on port \${PORT}\`);
|
||||||
|
});`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||||
@@ -80,8 +79,8 @@ interface GenerateCommitMessageError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
|
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
|
||||||
(msg) => msg.content
|
(msg) => tokenCount(msg.content) + 4
|
||||||
).join('').length;
|
).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
const MAX_REQ_TOKENS = 3900 - INIT_MESSAGES_PROMPT_LENGTH;
|
const MAX_REQ_TOKENS = 3900 - INIT_MESSAGES_PROMPT_LENGTH;
|
||||||
|
|
||||||
@@ -89,8 +88,11 @@ export const generateCommitMessageWithChatCompletion = async (
|
|||||||
diff: string
|
diff: string
|
||||||
): Promise<string | GenerateCommitMessageError> => {
|
): Promise<string | GenerateCommitMessageError> => {
|
||||||
try {
|
try {
|
||||||
if (diff.length >= MAX_REQ_TOKENS) {
|
if (tokenCount(diff) >= MAX_REQ_TOKENS) {
|
||||||
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(diff);
|
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(
|
||||||
|
diff,
|
||||||
|
MAX_REQ_TOKENS
|
||||||
|
);
|
||||||
|
|
||||||
const commitMessages = await Promise.all(commitMessagePromises);
|
const commitMessages = await Promise.all(commitMessagePromises);
|
||||||
|
|
||||||
@@ -110,22 +112,28 @@ export const generateCommitMessageWithChatCompletion = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getMessagesPromisesByLines(fileDiff: string, separator: string) {
|
function getMessagesPromisesByChangesInFile(
|
||||||
const lineSeparator = '\n@@';
|
fileDiff: string,
|
||||||
const [fileHeader, ...fileDiffByLines] = fileDiff.split(lineSeparator);
|
separator: string,
|
||||||
|
maxChangeLength: number
|
||||||
|
) {
|
||||||
|
const 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 mergedLines = mergeStrings(
|
const mergedChanges = mergeDiffs(
|
||||||
fileDiffByLines.map((line) => lineSeparator + line),
|
fileDiffByLines.map((line) => hunkHeaderSeparator + line),
|
||||||
MAX_REQ_TOKENS
|
maxChangeLength
|
||||||
);
|
);
|
||||||
|
|
||||||
const lineDiffsWithHeader = mergedLines.map(
|
const lineDiffsWithHeader = mergedChanges.map(
|
||||||
(d) => fileHeader + lineSeparator + d
|
(change) => fileHeader + change
|
||||||
);
|
);
|
||||||
|
|
||||||
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((d) => {
|
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((lineDiff) => {
|
||||||
const messages = generateCommitMessageChatCompletionPrompt(separator + d);
|
const messages = generateCommitMessageChatCompletionPrompt(
|
||||||
|
separator + lineDiff
|
||||||
|
);
|
||||||
|
|
||||||
return api.generateCommitMessage(messages);
|
return api.generateCommitMessage(messages);
|
||||||
});
|
});
|
||||||
@@ -133,20 +141,27 @@ function getMessagesPromisesByLines(fileDiff: string, separator: string) {
|
|||||||
return commitMsgsFromFileLineDiffs;
|
return commitMsgsFromFileLineDiffs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCommitMsgsPromisesFromFileDiffs(diff: string) {
|
export function getCommitMsgsPromisesFromFileDiffs(
|
||||||
|
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
|
// merge multiple files-diffs into 1 prompt to save tokens
|
||||||
const mergedFilesDiffs = mergeStrings(diffByFiles, MAX_REQ_TOKENS);
|
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
|
||||||
|
|
||||||
const commitMessagePromises = [];
|
const commitMessagePromises = [];
|
||||||
|
|
||||||
for (const fileDiff of mergedFilesDiffs) {
|
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
|
// 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);
|
commitMessagePromises.push(...messagesPromises);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+6
-2
@@ -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 pt_br from '../i18n/pt_br.json' assert { type: 'json' };
|
||||||
import vi_VN from '../i18n/vi_VN.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 es_ES from '../i18n/es_ES.json' assert { type: 'json' };
|
||||||
|
import sv from '../i18n/sv.json' assert { type: 'json' };
|
||||||
|
|
||||||
export enum I18nLocals {
|
export enum I18nLocals {
|
||||||
'en' = 'en',
|
'en' = 'en',
|
||||||
@@ -20,7 +21,8 @@ export enum I18nLocals {
|
|||||||
'it' = 'it',
|
'it' = 'it',
|
||||||
'ko' = 'ko',
|
'ko' = 'ko',
|
||||||
'pt_br' = 'pt_br',
|
'pt_br' = 'pt_br',
|
||||||
'es_ES' = 'es_ES'
|
'es_ES' = 'es_ES',
|
||||||
|
'sv' = 'sv',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const i18n = {
|
export const i18n = {
|
||||||
@@ -34,7 +36,8 @@ export const i18n = {
|
|||||||
ko,
|
ko,
|
||||||
pt_br,
|
pt_br,
|
||||||
vi_VN,
|
vi_VN,
|
||||||
es_ES
|
es_ES,
|
||||||
|
sv
|
||||||
};
|
};
|
||||||
|
|
||||||
export const I18N_CONFIG_ALIAS: { [key: string]: string[] } = {
|
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'],
|
vi_VN: ['vi_VN', 'Vietnamese', 'tiếng Việt'],
|
||||||
en: ['en', 'English', 'english'],
|
en: ['en', 'English', 'english'],
|
||||||
es_ES: ['es_ES', 'Spanish', 'español'],
|
es_ES: ['es_ES', 'Spanish', 'español'],
|
||||||
|
sv: ['sv', 'Swedish', 'Svenska'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getI18nLocal(value: string): string | boolean {
|
export function getI18nLocal(value: string): string | boolean {
|
||||||
|
|||||||
@@ -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."
|
||||||
|
}
|
||||||
@@ -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[] = [];
|
const mergedArr: string[] = [];
|
||||||
let currentItem: string = arr[0];
|
let currentItem: string = arr[0];
|
||||||
for (const item of arr.slice(1)) {
|
for (const item of arr.slice(1)) {
|
||||||
if (currentItem.length + item.length <= maxStringLength) {
|
if (tokenCount(currentItem + item) <= maxStringLength) {
|
||||||
currentItem += item;
|
currentItem += item;
|
||||||
} else {
|
} else {
|
||||||
mergedArr.push(currentItem);
|
mergedArr.push(currentItem);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user