Compare commits

..

42 Commits

Author SHA1 Message Date
di-sukharev
ed4d6e0109 1.0.11 2023-03-10 22:49:43 +08:00
di-sukharev
6712e798c5 * feat(cli.ts): add check for new opencommit version after commit is made
* chore(cli.ts): import execa and outro from their respective packages
2023-03-10 22:49:27 +08:00
di-sukharev
1347962a93 1.0.10 2023-03-10 22:42:33 +08:00
di-sukharev
3ca9b9752f * fix(api.ts): add process.exit(1) to terminate the process if apiKey is not provided 2023-03-10 22:41:53 +08:00
di-sukharev
50e2f67a9b 1.0.9 2023-03-10 17:56:21 +08:00
di-sukharev
c1a4c3daf2 * chore: update package description and keywords
* chore: update README with new package description and title
2023-03-10 17:55:51 +08:00
di-sukharev
e4de6d8186 * feat(README.md): add OpenCommit example image to README
* chore(README.md): update OPENAI_API_KEY config key to use 'your_api_key' instead of 'your token'
* chore(README.md): update description of where the api key is stored
2023-03-10 15:59:57 +08:00
di-sukharev
5abe5d715d * refactor(commit.ts): remove unnecessary blank lines
* feat(commit.ts): add spinner to show progress of `git push` command
* feat(commit.ts): show success message after successful `git push` command execution
2023-03-10 15:24:49 +08:00
di-sukharev
32c34abd22 1.0.8 2023-03-09 21:32:24 +08:00
di-sukharev
1050cad95d * refactor(commit.ts): extract stdout from git push command into a variable 2023-03-09 21:32:07 +08:00
di-sukharev
7bbc97980e * refactor(commit.ts): remove unused stdout variable and simplify pushSpinner.stop() call 2023-03-09 21:31:31 +08:00
di-sukharev
2a9a3d5818 * chore(README.md): add emoji to the project description 2023-03-09 21:30:44 +08:00
di-sukharev
22935f38ba 1.0.7 2023-03-09 21:29:52 +08:00
di-sukharev
4ae6361ad8 * chore(package.json): update description with emojis. 2023-03-09 21:29:28 +08:00
di-sukharev
5a0a384cbe 1.0.6 2023-03-09 21:25:49 +08:00
di-sukharev
a8839353f7 * chore(commit.ts): add spinner to indicate git push process
* refactor(commit.ts): change success message to include spinner in `git push` process
2023-03-09 21:25:38 +08:00
di-sukharev
88006b8693 * fix(commit.ts): add check for cancelation of git push confirmation prompt 2023-03-09 21:23:58 +08:00
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
di-sukharev
7d483f0d7b 1.0.1 2023-03-06 23:00:41 +08:00
di-sukharev
d1ed0a9fea major 2023-03-06 23:00:38 +08:00
13 changed files with 144 additions and 66 deletions

BIN
.github/opencommit-example.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

View File

@@ -5,15 +5,18 @@
<h4 align="center">Author <a href="https://github.com/di-sukharev">@di-sukharev</a> <a href="https://twitter.com/io_Y_oi"><img src="https://img.shields.io/twitter/follow/io_Y_oi?style=flat&label=io_Y_oi&logo=twitter&color=0bf&logoColor=fff" align="center"></a>
</h4>
</div>
<p>AI generates conventional commits with mind-blowing accuracy</p>
<h2>GPT CLI to auto-generate impressive commits in 1 second</h2>
<p>Killing lame commits with AI 🤯🔫</p>
<a href="https://www.npmjs.com/package/opencommit"><img src="https://img.shields.io/npm/v/opencommit" alt="Current version"></a>
</div>
---
## Examples
<div align="center">
<img src=".github/opencommit-example.png" alt="OpenCommit example"/>
</div>
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
@@ -30,10 +33,10 @@ Look into [the commits](https://github.com/di-sukharev/opencommit/commit) to see
3. Set the key to opencommit config:
```sh
opencommit config set OPENAI_API_KEY=<your token>
opencommit config set OPENAI_API_KEY=<your_api_key>
```
Your token isn't sent to anyone, it's saved in `~/.opencommit` config file.
Your api key is stored locally in `~/.opencommit` config file.
## Usage
@@ -60,27 +63,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 +93,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": "0.0.1",
"version": "1.0.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-commit",
"version": "0.0.1",
"version": "1.0.11",
"license": "ISC",
"dependencies": {
"@clack/prompts": "^0.6.1",

View File

@@ -1,13 +1,17 @@
{
"name": "opencommit",
"version": "0.0.1",
"description": "AI generates conventional commits with mind-blowing accuracy.",
"version": "1.0.11",
"description": "GPT CLI to auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
"keywords": [
"git",
"chatgpt",
"gpt",
"ai",
"openai",
"opencommit",
"aicommit",
"aicommits",
"gptcommit",
"commit"
],
"main": "cli.js",

View File

@@ -1,7 +1,6 @@
import { intro, outro } from '@clack/prompts';
import {
ChatCompletionRequestMessage,
ChatCompletionResponseMessage,
Configuration as OpenAiApiConfiguration,
OpenAIApi
} from 'openai';
@@ -21,6 +20,7 @@ if (!apiKey) {
outro(
'For help Look into README https://github.com/di-sukharev/opencommit#setup'
);
process.exit(1);
}
// if (!apiKey) {
@@ -43,7 +43,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 +55,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,12 +1,14 @@
#!/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';
import { prepareCommitMessageHook } from './commands/prepare-commit-msg-hook';
import { commit } from './commands/commit';
import { execa } from 'execa';
import { outro } from '@clack/prompts';
const rawArgv = process.argv.slice(2);
@@ -19,11 +21,17 @@ cli(
ignoreArgv: (type) => type === 'unknown-flag' || type === 'argument',
help: { description: packageJSON.description }
},
() => {
async () => {
if (isHookCalled) {
prepareCommitMessageHook();
await prepareCommitMessageHook();
} else {
commit();
await commit();
const { stdout } = await execa('npm', ['view', 'opencommit', 'version']);
if (stdout !== packageJSON.version)
outro(
'new opencommit version is available, update with `npm i -g opencommit`'
);
}
},
rawArgv

View File

@@ -44,8 +44,22 @@ ${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 && !isCancel(isPushConfirmedByUser)) {
const pushSpinner = spinner();
pushSpinner.start('Running `git push`');
const { stdout } = await execa('git', ['push']);
pushSpinner.stop(`${chalk.green('✔')} successfully pushed all commits`);
if (stdout) outro(stdout);
}
} else outro(`${chalk.gray('✖')} process cancelled`);
};
@@ -79,7 +93,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,