Compare commits

...

23 Commits

Author SHA1 Message Date
di-sukharev
5d9d1c972a 1.0.5 2023-03-09 21:22:46 +08:00
di-sukharev
3892bd0e69 * feat(commit.ts): add option to push commits after successful commit 2023-03-09 21:22:35 +08:00
di-sukharev
2b6cc5c360 * chore(README.md): update example section to clarify that all commits in the repo are done with OpenCommit 2023-03-09 21:20:17 +08:00
di-sukharev
c4c2600cf6 1.0.4 2023-03-09 15:29:34 +08:00
di-sukharev
c361aaa914 * refactor(generateCommitMessageFromGitDiff.ts): merge multiple line-diffs and multiple files-diffs to save tokens
* refactor(generateCommitMessageFromGitDiff.ts): split file-diff into line-diffs only if file-diff is bigger than gpt context
2023-03-09 15:28:18 +08:00
di-sukharev
f3d673185e deleted log txt file 2023-03-09 15:04:51 +08:00
di-sukharev
5615bdce86 * refactor(commit.ts): add stdout to commit function output
* fix(commit.ts): fix commit confirmation message to show correct status
2023-03-09 14:02:04 +08:00
di-sukharev
68b327bee7 * refactor(prepare-commit-msg-hook.ts): remove outro function call and its message from try block 2023-03-09 14:00:50 +08:00
di-sukharev
b747d70e69 1.0.3 2023-03-09 13:59:55 +08:00
di-sukharev
caa64fbcf9 * refactor(prepare-commit-msg-hook.ts): change appendFile to writeFile to write commit message to file
* chore(prepare-commit-msg-hook.ts): add newline character before appending file content to commit message
2023-03-09 13:58:27 +08:00
di-sukharev
1a49c08409 * chore(commit.ts): change stagedFilesSpinner message when no files are staged 2023-03-09 13:56:03 +08:00
di-sukharev
3399d65a0c 1.0.2 2023-03-07 23:02:22 +08:00
di-sukharev
d561170519 * refactor(api.ts): remove console.log statement
* fix(generateCommitMessageFromGitDiff.ts): increase MAX_REQ_TOKENS to 3900 to avoid exceeding OpenAI's limit
2023-03-07 22:37:13 +08:00
di-sukharev
bf29c260ca * 🚀 feat(generateCommitMessageFromGitDiff.ts): add mergeStrings utility function
* 🐛 fix(generateCommitMessageFromGitDiff.ts): remove unnecessary separator variable

* 🐛 fix: split file diffs by files
*  feat: add support for generating commit messages by file diffs

*  feat(generateCommitMessageFromGitDiff.ts): add mergeStrings utility function to merge string arrays
* 🐛 fix(generateCommitMessageFromGitDiff.ts): reduce MAX_REQ_TOKENS to 1000
*  feat(generateCommitMessageFromGitDiff.ts): add support for generating commit messages for large diffs by splitting them into smaller chunks and generating commit messages for each chunk using OpenAI's GPT-3 model.

* ♻️ refactor: extract getCommitMsgsPromisesFromFileDiffs function from generateCommitMessage function
* ♻️ refactor: extract getMessagesPromisesByLines function from getCommitMsgsPromisesFromFileDiffs function
*  feat: add support for merging file diffs into one commit message if it exceeds MAX_REQ_TOKENS limit

*  feat(mergeStrings.ts): add mergeStrings function to merge strings in an array based on maxStringLength
2023-03-07 16:19:00 +08:00
di-sukharev
9e2a3d8988 getMessagesPromisesByLines 2023-03-07 16:16:12 +08:00
di-sukharev
1ea5fbc430 * 🐛 fix(api.ts): return message content instead of whole response object
* 🚀 chore(generateCommitMessageFromGitDiff.ts): filter out null promises and update TODO comment
* 🚀 chore(tsconfig.json): update target to ES2020 and remove ES5 from lib
2023-03-07 15:51:10 +08:00
di-sukharev
55e9adf73d * 🐛 fix(cli.ts): add assert statement to packageJSON import
The assert statement was added to ensure that the imported packageJSON is of type JSON. This prevents any potential runtime errors that may occur if the imported file is not of the expected type.

* 🐛 fix(generateCommitMessageFromGitDiff.ts): adjust MAX_REQ_TOKENS to account for INIT_MESSAGES_PROMPT_LENGTH
* 💄 style(generateCommitMessageFromGitDiff.ts): remove unnecessary separator variable
The MAX_REQ_TOKENS constant was not accounting for the length of the INIT_MESSAGES_PROMPT, which caused the function to fail when the diff length was close to the limit. This has been fixed by adjusting the constant to include the prompt length. The separator variable was not being used and has been removed for clarity.

* 🚀 chore(tsconfig.json): update compiler options
The target compiler option has been updated to ESNext to allow for the use of the latest ECMAScript features. The lib compiler option has been updated to include ES6 in addition to ES5. The module compiler option has been updated to ESNext to allow for the use of the latest module syntax.
2023-03-07 15:21:16 +08:00
di-sukharev
60325f53b9 * 🚧 chore(prepare-commit-msg-hook.ts): remove console.log and add TODO comment
The console.log statement is removed as it is not needed. A TODO comment is added to remind the developer to change the code to read file and write file with commitMessage.
2023-03-06 23:56:38 +08:00
di-sukharev
3966c4c53a * 🐛 fix(prepare-commit-msg-hook.ts): remove console.log statement
The console.log statement was left in the code and is not needed. This commit removes it.
2023-03-06 23:52:34 +08:00
di-sukharev
2527c80f2f * 📝 docs(README.md): replace opencommit with oc in commands
The commands in the README.md file have been updated to use the new 'oc' command instead of the old 'opencommit' command. This improves consistency with the naming conventions and makes it easier to use the tool.
2023-03-06 23:41:47 +08:00
di-sukharev
e89fc96732 * 🎨 style(githook.ts): remove unused import
The import of fileURLToPath from the url module is not used in the file, so it has been removed to improve code readability and maintainability.
2023-03-06 23:35:39 +08:00
di-sukharev
b5d1057fd6 * 📝 docs(README.md): update link to commits
The link to the commits was updated to point to a specific commit (eae7618d57) instead of the general commits page. This provides a more direct and specific reference to the commits.
2023-03-06 23:22:00 +08:00
di-sukharev
5f310970cc * 📝 docs(README.md): remove emoji from header
Removed the cowboy emoji from the header of the README.md file to improve readability.

* 📝 chore(TODO.md): reorder TODO list
Reordered the TODO list in the TODO.md file to prioritize the task of making the bundle smaller by properly configuring esbuild.
2023-03-06 23:19:17 +08:00
12 changed files with 116 additions and 58 deletions

View File

@@ -13,7 +13,7 @@
## Examples
Look into [the commits](https://github.com/di-sukharev/opencommit/commit) to see how OpenCommit works. Emoji and long commit description text is configurable.
All the commits in this repo are done with OpenCommit — look into [the commits](https://github.com/di-sukharev/opencommit/commit/eae7618d575ee8d2e9fff5de56da79d40c4bc5fc) to see how OpenCommit works. Emoji and long commit description text is configurable.
## Setup
@@ -60,27 +60,27 @@ oc
To add emoji:
```sh
opencommit config set emoji=true
oc config set emoji=true
```
To remove emoji:
```sh
opencommit config set emoji=false
oc config set emoji=false
```
### Postface commits with descriptions of changes 🤠
### Postface commits with descriptions of changes
To add descriptions:
```sh
opencommit config set description=true
oc config set description=true
```
To remove description:
```sh
opencommit config set description=false
oc config set description=false
```
## Git hook
@@ -90,20 +90,20 @@ You can set opencommit as Git [`prepare-commit-msg`](https://git-scm.com/docs/gi
To set the hook:
```sh
opencommit hook set
oc hook set
```
To unset the hook:
```sh
opencommit hook unset
oc hook unset
```
To use the hook:
```sh
git add <files...>
git commit
git add <files...>
git commit
```
Or follow the process of your IDE Source Control feature, when it calls `git commit` command — OpenCommit will integrate into the flow.

View File

@@ -1,7 +1,7 @@
# TODOs
- [] [build for both mjs and cjs](https://snyk.io/blog/best-practices-create-modern-npm-package/)
- [] make bundle smaller by properly configuring esbuild
- [] [build for both mjs and cjs](https://snyk.io/blog/best-practices-create-modern-npm-package/)
- [] do // TODOs in the code
- [] batch small files in one request
- [] add tests

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "open-commit",
"version": "1.0.1",
"version": "1.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-commit",
"version": "1.0.1",
"version": "1.0.5",
"license": "ISC",
"dependencies": {
"@clack/prompts": "^0.6.1",

View File

@@ -1,6 +1,6 @@
{
"name": "opencommit",
"version": "1.0.1",
"version": "1.0.5",
"description": "AI generates conventional commits with mind-blowing accuracy.",
"keywords": [
"git",

View File

@@ -1,7 +1,6 @@
import { intro, outro } from '@clack/prompts';
import {
ChatCompletionRequestMessage,
ChatCompletionResponseMessage,
Configuration as OpenAiApiConfiguration,
OpenAIApi
} from 'openai';
@@ -43,7 +42,7 @@ class OpenAi {
public generateCommitMessage = async (
messages: Array<ChatCompletionRequestMessage>
): Promise<ChatCompletionResponseMessage | undefined> => {
): Promise<string | undefined> => {
try {
const { data } = await this.openAI.createChatCompletion({
model: 'gpt-3.5-turbo',
@@ -55,9 +54,9 @@ class OpenAi {
const message = data.choices[0].message;
return message;
return message?.content;
} catch (error) {
console.error('openAI api error', { error });
// console.error('openAI api error', { error });
throw error;
}
};

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { cli } from 'cleye';
import packageJSON from '../package.json';
import packageJSON from '../package.json' assert { type: 'json' };
import { configCommand } from './commands/config';
import { hookCommand, isHookCalled } from './commands/githook.js';

View File

@@ -44,8 +44,18 @@ ${chalk.grey('——————————————————')}`
});
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
await execa('git', ['commit', '-m', commitMessage]);
const { stdout } = await execa('git', ['commit', '-m', commitMessage]);
outro(`${chalk.green('✔')} successfully committed`);
outro(stdout);
const isPushConfirmedByUser = await confirm({
message: 'Do you want to run `git push`?'
});
if (isPushConfirmedByUser) {
const { stdout } = await execa('git', ['push']);
outro(`${chalk.green('✔')} successfully pushed all commits`);
outro(stdout);
}
} else outro(`${chalk.gray('✖')} process cancelled`);
};
@@ -79,7 +89,7 @@ export async function commit(isStageAllFlag = false) {
.bold('`oc`')} command.`
);
stagedFilesSpinner.stop('Counting staged files');
stagedFilesSpinner.stop('No files are staged');
const isStageAllAndCommitConfirmedByUser = await confirm({
message: 'Do you want to stage all files and generate commit message?'
});

View File

@@ -1,6 +1,5 @@
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { command } from 'cleye';
import { assertGitRepo } from '../utils/git.js';
import { existsSync } from 'fs';

View File

@@ -37,9 +37,12 @@ export const prepareCommitMessageHook = async () => {
if (typeof commitMessage !== 'string') throw new Error(commitMessage.error);
await fs.appendFile(messageFilePath, commitMessage);
const fileContent = await fs.readFile(messageFilePath);
outro(`${chalk.green('✔')} commit done`);
await fs.writeFile(
messageFilePath,
commitMessage + '\n' + fileContent.toString()
);
} catch (error) {
outro(`${chalk.red('✖')} ${error}`);
process.exit(1);

View File

@@ -4,6 +4,7 @@ import {
} from 'openai';
import { api } from './api';
import { getConfig } from './commands/config';
import { mergeStrings } from './utils/mergeStrings';
const config = getConfig();
@@ -81,47 +82,79 @@ const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
(msg) => msg.content
).join('').length;
const MAX_REQ_TOKENS = 3900 - INIT_MESSAGES_PROMPT_LENGTH;
export const generateCommitMessageWithChatCompletion = async (
diff: string
): Promise<string | GenerateCommitMessageError> => {
try {
const MAX_REQ_TOKENS = 3900;
if (diff.length >= MAX_REQ_TOKENS) {
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(diff);
if (INIT_MESSAGES_PROMPT_LENGTH + diff.length >= MAX_REQ_TOKENS) {
const separator = 'diff --git ';
const diffByFiles = diff.split(separator).slice(1);
const commitMessages = [];
for (const diffFile of diffByFiles) {
if (INIT_MESSAGES_PROMPT_LENGTH + diffFile.length >= MAX_REQ_TOKENS)
continue;
const messages = generateCommitMessageChatCompletionPrompt(
separator + diffFile
);
const commitMessage = await api.generateCommitMessage(messages);
// TODO: handle this edge case
if (!commitMessage?.content) continue;
commitMessages.push(commitMessage?.content);
}
const commitMessages = await Promise.all(commitMessagePromises);
return commitMessages.join('\n\n');
} else {
const messages = generateCommitMessageChatCompletionPrompt(diff);
const commitMessage = await api.generateCommitMessage(messages);
if (!commitMessage)
return { error: GenerateCommitMessageErrorEnum.emptyMessage };
return commitMessage;
}
const messages = generateCommitMessageChatCompletionPrompt(diff);
const commitMessage = await api.generateCommitMessage(messages);
if (!commitMessage)
return { error: GenerateCommitMessageErrorEnum.emptyMessage };
return commitMessage.content;
} catch (error) {
return { error: GenerateCommitMessageErrorEnum.internalError };
}
};
function getMessagesPromisesByLines(fileDiff: string, separator: string) {
const lineSeparator = '\n@@';
const [fileHeader, ...fileDiffByLines] = fileDiff.split(lineSeparator);
// merge multiple line-diffs into 1 to save tokens
const mergedLines = mergeStrings(
fileDiffByLines.map((line) => lineSeparator + line),
MAX_REQ_TOKENS
);
const lineDiffsWithHeader = mergedLines.map(
(d) => fileHeader + lineSeparator + d
);
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((d) => {
const messages = generateCommitMessageChatCompletionPrompt(separator + d);
return api.generateCommitMessage(messages);
});
return commitMsgsFromFileLineDiffs;
}
function getCommitMsgsPromisesFromFileDiffs(diff: string) {
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 commitMessagePromises = [];
for (const fileDiff of mergedFilesDiffs) {
if (fileDiff.length >= MAX_REQ_TOKENS) {
// if file-diff is bigger than gpt context — split fileDiff into lineDiff
const messagesPromises = getMessagesPromisesByLines(fileDiff, separator);
commitMessagePromises.push(...messagesPromises);
} else {
const messages = generateCommitMessageChatCompletionPrompt(
separator + fileDiff
);
commitMessagePromises.push(api.generateCommitMessage(messages));
}
}
return commitMessagePromises;
}

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

@@ -0,0 +1,14 @@
export function mergeStrings(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) {
currentItem += item;
} else {
mergedArr.push(currentItem);
currentItem = item;
}
}
mergedArr.push(currentItem);
return mergedArr;
}

View File

@@ -1,9 +1,9 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES5"],
"lib": ["ES5", "ES6"],
"module": "CommonJS",
"module": "ESNext",
// "rootDir": "./src",
"moduleResolution": "node",
"resolveJsonModule": true,