mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-01-12 23:28:16 -05:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91987030f0 | ||
|
|
2a55b08b51 | ||
|
|
ed4d6e0109 | ||
|
|
6712e798c5 | ||
|
|
1347962a93 | ||
|
|
3ca9b9752f | ||
|
|
50e2f67a9b | ||
|
|
c1a4c3daf2 | ||
|
|
e4de6d8186 | ||
|
|
5abe5d715d | ||
|
|
32c34abd22 | ||
|
|
1050cad95d | ||
|
|
7bbc97980e | ||
|
|
2a9a3d5818 | ||
|
|
22935f38ba | ||
|
|
4ae6361ad8 | ||
|
|
5a0a384cbe | ||
|
|
a8839353f7 | ||
|
|
88006b8693 | ||
|
|
5d9d1c972a | ||
|
|
3892bd0e69 | ||
|
|
2b6cc5c360 | ||
|
|
c4c2600cf6 | ||
|
|
c361aaa914 | ||
|
|
f3d673185e | ||
|
|
5615bdce86 | ||
|
|
68b327bee7 | ||
|
|
b747d70e69 | ||
|
|
caa64fbcf9 | ||
|
|
1a49c08409 | ||
|
|
3399d65a0c | ||
|
|
d561170519 | ||
|
|
bf29c260ca | ||
|
|
9e2a3d8988 | ||
|
|
1ea5fbc430 | ||
|
|
55e9adf73d | ||
|
|
60325f53b9 | ||
|
|
3966c4c53a | ||
|
|
2527c80f2f | ||
|
|
e89fc96732 | ||
|
|
b5d1057fd6 | ||
|
|
5f310970cc |
BIN
.github/opencommit-example.png
vendored
Normal file
BIN
.github/opencommit-example.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 318 KiB |
31
README.md
31
README.md
@@ -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.
|
||||
|
||||
2
TODO.md
2
TODO.md
@@ -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
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "open-commit",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-commit",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.12",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.6.1",
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"name": "opencommit",
|
||||
"version": "1.0.1",
|
||||
"description": "AI generates conventional commits with mind-blowing accuracy.",
|
||||
"version": "1.0.12",
|
||||
"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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
16
src/cli.ts
16
src/cli.ts
@@ -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
|
||||
|
||||
@@ -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?'
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
14
src/utils/mergeStrings.ts
Normal 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;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["ES5"],
|
||||
"lib": ["ES5", "ES6"],
|
||||
|
||||
"module": "CommonJS",
|
||||
"module": "ESNext",
|
||||
// "rootDir": "./src",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
|
||||
Reference in New Issue
Block a user