Compare commits

..

51 Commits

Author SHA1 Message Date
di-sukharev
2d48648f52 chore(package.json): update version to 3.0.3
The version in package.json has been updated from 3.0.2 to 3.0.3. This change is made to reflect the latest version of the project.
2023-09-05 13:51:21 +08:00
di-sukharev
40297e0c6a chore(package.json): update version to 3.0.2
The version in package.json has been updated from 3.0.1 to 3.0.2. This change is made to reflect the latest version of the project.
2023-09-05 13:49:17 +08:00
di-sukharev
75f0cd47b8 chore(package.json): update version to 3.0.1
The version in package.json has been updated from 3.0.0 to 3.0.1. This change is made to reflect the new version of the project.
2023-09-05 13:48:09 +08:00
Sukharev
c76313737d #247: fix (#248) 2023-09-05 13:45:34 +08:00
di-sukharev
ac8c87be9e docs(README.md): add note about MacOS potentially requiring 'sudo' when installing package globally
The README.md file now includes a note that MacOS may prompt the user to run the 'npm install -g opencommit' command with 'sudo' when installing the package globally. This note is added to provide clarity and help users who encounter this situation.
2023-09-03 15:22:47 +08:00
di-sukharev
a9050fda39 build 2023-09-03 15:17:12 +08:00
Sukharev
b98b892ba1 v3.0.0 (#243)
* update major version

---------

Co-authored-by: E Einowski <5124085+EwiththeBowtie@users.noreply.github.com>
Co-authored-by: Max Zavodniuk <lovemaxonly@gmail.com>
Co-authored-by: Malthe Poulsen <30603252+malpou@users.noreply.github.com>
Co-authored-by: Sébastien Fichot <fichot.sebastien@gmail.com>
2023-09-03 15:16:02 +08:00
Sukharev
ba5dd848b8 Update README.md 2023-08-24 12:00:43 +08:00
di-sukharev
916ddf02d1 2.4.2 2023-07-05 15:20:11 +08:00
di-sukharev
fa85ec0351 build 2023-07-05 15:20:03 +08:00
di-sukharev
3a7f2d69e7 2.4.1 2023-07-05 15:19:29 +08:00
di-sukharev
f240d35231 build 2023-07-05 15:19:20 +08:00
di-sukharev
04ee73ca9d 2.4.0 2023-07-05 15:18:19 +08:00
di-sukharev
91885cdd1c 2.3.0 2023-07-05 15:18:02 +08:00
di-sukharev
b7a2cd46b3 build 2023-07-05 15:13:58 +08:00
di-sukharev
028c0bc518 format files 2023-07-05 15:13:30 +08:00
di-sukharev
897eb73cd7 feat(README.md): add GitHub Hackathon winner badge to project description
docs(README.md): restructure README, move CLI setup instructions to top for better visibility
feat(cli.cjs): extend file exclusion list in 'getDiff' to include image files for better performance
refactor(config.ts): add more model options in configValidators for flexibility
refactor(config.ts): set default model to 'gpt-3.5-turbo-16k' for better performance
refactor(git.ts): simplify 'getCoreHooksPath' function for readability
feat(git.ts): extend file exclusion list in 'getDiff' to include image files for better performance
2023-07-05 15:11:17 +08:00
di-sukharev
e7ce40a8d1 build 2023-07-05 14:57:03 +08:00
di-sukharev
18d25672d7 docs(CONTRIBUTING.md): clarify pull request target branch in contribution guidelines
This change emphasizes that pull requests should be made to the `dev` branch, not the `master` branch, to ensure proper workflow.
2023-07-05 14:52:09 +08:00
di-sukharev
79b1008e02 Merge branch 'dev' 2023-07-05 14:50:19 +08:00
seho
3c0a271bf8 feat(config.ts): add OCO_MESSAGE_TEMPLATE_PLACEHOLDER configuration item to allow users to customize the message template placeholder (#208)
feat(commit.ts): add check for message templates in extraArgs and replace OCO_MESSAGE_TEMPLATE_PLACEHOLDER with generated commit message if found
docs(README.md): add documentation for OCO_MESSAGE_TEMPLATE_PLACEHOLDER configuration item and how to use it in the command line (#205)
2023-07-05 14:27:43 +08:00
Sam Rudloff
ccfd24a9e5 Adding gpt-3.5-turbo-16k Modal (#215) 2023-07-05 14:25:21 +08:00
Avighna
0c8bf5562d Fixed lots of changes (#195) 2023-06-19 18:34:58 +08:00
Sukharev
96c7676a13 Revert "Fixing grammatical errors and enhancing overall grammar in README.md (#190)" (#210)
This reverts commit 90f64d5475.
2023-06-19 18:33:39 +08:00
Avighna
90f64d5475 Fixing grammatical errors and enhancing overall grammar in README.md (#190)
* Fixed grammatical errors and enhanced overall grammar in README.md
2023-06-19 18:32:12 +08:00
di-sukharev
a70b831f9d build 2023-05-26 13:14:47 +08:00
di-sukharev
cad179953a 2.2.10 2023-05-26 13:14:42 +08:00
di-sukharev
8979841010 refactor(api.ts): reformat imports to be grouped and sorted alphabetically
refactor(api.ts): replace axios with execa to get the latest version of opencommit from npm registry
2023-05-26 13:14:33 +08:00
di-sukharev
1e974086d3 build 2023-05-26 13:12:07 +08:00
di-sukharev
792ab67ef1 2.2.9 2023-05-26 13:12:00 +08:00
di-sukharev
a7af55df37 feat(checkIsLatestVersion.ts): add outro message from @clack/prompts to warn user about not using the latest stable version of OpenCommit 2023-05-26 13:11:51 +08:00
di-sukharev
5c540abae9 build 2023-05-26 13:10:41 +08:00
di-sukharev
f69e716dcc 2.2.8 2023-05-26 13:10:29 +08:00
di-sukharev
1d8d8e57c2 style(.prettierrc): reorder properties to follow alphabetical order 2023-05-26 13:09:53 +08:00
di-sukharev
fdc638cd86 build 2023-05-26 13:08:19 +08:00
di-sukharev
56e02f2604 2.2.7 2023-05-26 13:08:08 +08:00
di-sukharev
b926a627a8 Merge remote-tracking branch 'origin/dev' 2023-05-26 13:07:42 +08:00
Gabriel Moreli
32f3e176f0 feat(api.ts): solving bad request issue (#187)
* 2.0.18

* patch

* 2.0.19

* style(.prettierrc): reorder properties to follow alphabetical order and improve readability

* feat(generateCommitMessageFromGitDiff.ts): changing logic of MAX_REQ_TOKENS

fix(api.ts): add missing import for GenerateCommitMessageErrorEnum
The token count validation is added to prevent the request from exceeding the default model token limit. The validation is done by counting the tokens in each message and adding 4 to each count to account for the additional tokens added by the API. If the total token count exceeds the limit, an error is thrown. The missing import for GenerateCommitMessageErrorEnum is also added.

feat: add support for splitting long line-diffs into smaller pieces
This change adds support for splitting long line-diffs into smaller pieces to avoid exceeding the maximum commit message length. The `splitDiff` function splits a single line into multiple lines if it exceeds the maximum length. It also splits the diff into smaller pieces if adding the next line would exceed the maximum length. This change improves the readability of commit messages and makes them more consistent.

refactor: improve code readability by adding whitespace and reformatting code
This commit improves the readability of the code by adding whitespace and reformatting the code. The changes do not affect the functionality of the code. Additionally, a new function `delay` has been added to the file.

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
2023-05-26 13:07:09 +08:00
di-sukharev
cf4212016f build 2023-05-26 12:57:44 +08:00
di-sukharev
c491fa4bad 2.2.6 2023-05-26 12:57:38 +08:00
di-sukharev
f10fc37fe7 refactor(CommandsEnum.ts): reorder COMMANDS enum values to improve readability and maintainability 2023-05-26 12:57:20 +08:00
di-sukharev
f7b1a6358f build 2023-05-26 12:55:27 +08:00
di-sukharev
5f9f29c467 2.2.5 2023-05-26 12:55:14 +08:00
di-sukharev
0ec5dab80a 2.2.4 2023-05-26 12:54:52 +08:00
di-sukharev
8d47a1bb0f chore(package.json): revert version to 2.2.3 and remove postversion script as it is no longer needed
feat(package.json): add deploy script to automate versioning, building, pushing and publishing the package to npm registry
2023-05-26 12:54:42 +08:00
di-sukharev
fad05e0757 2.2.4 2023-05-26 12:52:00 +08:00
di-sukharev
12f7e7eaf9 build 2023-05-26 12:51:55 +08:00
di-sukharev
6490532818 2.2.3 2023-05-26 12:43:53 +08:00
di-sukharev
e0e953dab8 build 2023-05-26 12:43:49 +08:00
di-sukharev
75fa04efd4 2.2.2 2023-05-26 12:42:47 +08:00
di-sukharev
93019139fb build 2023-05-26 12:42:43 +08:00
34 changed files with 7562 additions and 4710 deletions

View File

@@ -9,7 +9,7 @@ Thanks for considering contributing to the project.
3. Create a new branch for your changes.
4. Make your changes and commit them with descriptive commit messages.
5. Push your changes to your forked repository.
6. Create a pull request from your branch to the `dev` branch.
6. Create a pull request from your branch to the `dev` branch. Not `master` branch, PR to `dev` branch, please.
## Getting started

BIN
.github/github-mark-white.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

1
.npmignore Normal file
View File

@@ -0,0 +1 @@
out/github-action.cjs

1
.opencommitignore Normal file
View File

@@ -0,0 +1 @@
out

View File

@@ -1,4 +1,4 @@
{
"singleQuote": true,
"trailingComma": "none"
"trailingComma": "none",
"singleQuote": true
}

433
README.md
View File

@@ -3,11 +3,12 @@
<img src=".github/logo-grad.svg" alt="OpenCommit logo"/>
<h1 align="center">OpenCommit</h1>
<h4 align="center">Follow the bird <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>
<h2>Auto-generate meaningful 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>
<h4 align="center">🪩 Winner of GitHub 2023 HACKATHON <a href="https://twitter.com/io_Y_oi/status/1683448136973582336"><img style="width:14px; height:14px; margin-top: -4px" src=".github/github-mark-white.png" align="center"></a>
</h4>
</div>
---
@@ -16,15 +17,265 @@
<img src=".github/opencommit-example.png" alt="OpenCommit example"/>
</div>
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.
All the commits in this repo are authored by OpenCommit — look at [the commits](https://github.com/di-sukharev/opencommit/commit/eae7618d575ee8d2e9fff5de56da79d40c4bc5fc) to see how OpenCommit works. Emojis and long commit descriptions are configurable.
## Setup OpenCommit as a Github Action 🔥
## Setup OpenCommit as a CLI tool
You can use OpenCommit by simply running it via the CLI like this `oco`. 2 seconds and your staged changes are committed with a meaningful message.
1. Install OpenCommit globally to use in any repository:
```sh
npm install -g opencommit
```
MacOS may ask to run the command with `sudo` when installing a package globally.
2. Get your API key from [OpenAI](https://platform.openai.com/account/api-keys). Make sure that you add your payment details, so the API works.
3. Set the key to OpenCommit config:
```sh
oco config set OCO_OPENAI_API_KEY=<your_api_key>
```
Your API key is stored locally in the `~/.opencommit` config file.
## Usage
You can call OpenCommit directly to generate a commit message for your staged changes:
```sh
git add <files...>
opencommit
```
You can also use the `oco` shortcut:
```sh
git add <files...>
oco
```
## Configuration
### Local per repo configuration
Create a `.env` file and add OpenCommit config variables there like this:
```env
OCO_OPENAI_API_KEY=<your OpenAI API token>
OCO_OPENAI_MAX_TOKENS=<max response tokens from OpenAI API>
OCO_OPENAI_BASE_PATH=<may be used to set proxy path to OpenAI api>
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>
OCO_EMOJI=<boolean, add GitMoji>
OCO_MODEL=<either 'gpt-4', 'gpt-3.5-turbo-16k' (default), 'gpt-3.5-turbo-0613' or 'gpt-3.5-turbo'>
OCO_LANGUAGE=<locale, scroll to the bottom to see options>
OCO_MESSAGE_TEMPLATE_PLACEHOLDER=<message template placeholder, default: '$msg'>
OCO_PROMPT_MODULE=<either conventional-commit or @commitlint, default: conventional-commit>
```
### Global config for all repos
Local config still has more priority than Global config, but you may set `OCO_MODEL` and `OCO_LOCALE` globally and set local configs for `OCO_EMOJI` and `OCO_DESCRIPTION` per repo which is more convenient.
Simply set any of the variables above like this:
```sh
oco config set OCO_MODEL=gpt-4
```
Configure [GitMoji](https://gitmoji.dev/) to preface a message.
```sh
oco config set OCO_EMOJI=true
```
To remove preface emojis:
```sh
oco config set OCO_EMOJI=false
```
### Switch to GPT-4 or other models
By default, OpenCommit uses `gpt-3.5-turbo-16k` model.
You may switch to GPT-4 which performs better, but costs ~x15 times more 🤠
```sh
oco config set OCO_MODEL=gpt-4
```
or for as a cheaper option:
```sh
oco config set OCO_MODEL=gpt-3.5-turbo
```
Make sure that you spell it `gpt-4` (lowercase) and that you have API access to the 4th model. Even if you have ChatGPT+, that doesn't necessarily mean that you have API access to GPT-4.
### Locale configuration
To globally specify the language used to generate commit messages:
```sh
# de, German ,Deutsch
oco config set OCO_LANGUAGE=de
oco config set OCO_LANGUAGE=German
oco config set OCO_LANGUAGE=Deutsch
# fr, French, française
oco config set OCO_LANGUAGE=fr
oco config set OCO_LANGUAGE=French
oco config set OCO_LANGUAGE=française
```
The default language setting is **English**
All available languages are currently listed in the [i18n](https://github.com/di-sukharev/opencommit/tree/master/src/i18n) folder
### Switch to `@commitlint`
OpenCommit allows you to choose the prompt module used to generate commit messages. By default, OpenCommit uses its conventional-commit message generator. However, you can switch to using the `@commitlint` prompt module if you prefer. This option lets you generate commit messages in respect with the local config.
You can set this option by running the following command:
```sh
oco config set OCO_PROMPT_MODULE=<module>
```
Replace `<module>` with either `conventional-commit` or `@commitlint`.
#### Example:
To switch to using th` '@commitlint` prompt module, run:
```sh
oco config set OCO_PROMPT_MODULE=@commitlint
```
To switch back to the default conventional-commit message generator, run:
```sh
oco config set OCO_PROMPT_MODULE=conventional-commit
```
#### Integrating with `@commitlint`
The integration between `@commitlint` and OpenCommit is done automatically the first time OpenCommit is run with `OCO_PROMPT_MODULE` set to `@commitlint`. However, if you need to force set or reset the configuration for `@commitlint`, you can run the following command:
```sh
oco commitlint force
```
To view the generated configuration for `@commitlint`, you can use this command:
```sh
oco commitlint get
```
This allows you to ensure that the configuration is set up as desired.
Additionally, the integration creates a file named `.opencommit-commitlint` which contains the prompts used for the local `@commitlint` configuration. You can modify this file to fine-tune the example commit message generated by OpenAI. This gives you the flexibility to make adjustments based on your preferences or project guidelines.
OpenCommit generates a file named `.opencommit-commitlint` in your project directory which contains the prompts used for the local `@commitlint` configuration. You can modify this file to fine-tune the example commit message generated by OpenAI. If the local `@commitlint` configuration changes, this file will be updated the next time OpenCommit is run.
This offers you greater control over the generated commit messages, allowing for customization that aligns with your project's conventions.
## Git flags
The `opencommit` or `oco` commands can be used in place of the `git commit -m "${generatedMessage}"` command. This means that any regular flags that are used with the `git commit` command will also be applied when using `opencommit` or `oco`.
```sh
oco --no-verify
```
is translated to :
```sh
git commit -m "${generatedMessage}" --no-verify
```
To include a message in the generated message, you can utilize the template function, for instance:
```sh
oco '#205: $msg
```
> opencommit examines placeholders in the parameters, allowing you to append additional information before and after the placeholders, such as the relevant Issue or Pull Request. Similarly, you have the option to customize the OCO_MESSAGE_TEMPLATE_PLACEHOLDER configuration item, for example, simplifying it to $m!"
### Message Template Placeholder Config
#### Overview
The `OCO_MESSAGE_TEMPLATE_PLACEHOLDER` feature in the `opencommit` tool allows users to embed a custom message within the generated commit message using a template function. This configuration is designed to enhance the flexibility and customizability of commit messages, making it easier for users to include relevant information directly within their commits.
#### Implementation Details
In our codebase, the implementation of this feature can be found in the following segment:
```javascript
commitMessage = messageTemplate.replace(
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
commitMessage
);
```
This line is responsible for replacing the placeholder in the `messageTemplate` with the actual `commitMessage`.
#### Usage
For instance, using the command `oco '$msg #205`, users can leverage this feature. The provided code represents the backend mechanics of such commands, ensuring that the placeholder is replaced with the appropriate commit message.
#### Committing with the Message
Once users have generated their desired commit message, they can proceed to commit using the generated message. By understanding the feature's full potential and its implementation details, users can confidently use the generated messages for their commits.
### Ignore files
You can remove files from being sent to OpenAI by creating a `.opencommitignore` file. For example:
```ignorelang
path/to/large-asset.zip
**/*.jpg
```
This helps prevent opencommit from uploading artifacts and large files.
By default, opencommit ignores files matching: `*-lock.*` and `*.lock`
## Git hook (KILLER FEATURE)
You can set OpenCommit as Git [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) hook. Hook integrates with your IDE Source Control and allows you to edit the message before committing.
To set the hook:
```sh
oco hook set
```
To unset the hook:
```sh
oco hook unset
```
To use the hook:
```sh
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.
## Setup OpenCommit as a GitHub Action (BETA) 🔥
OpenCommit is now available as a GitHub Action which automatically improves all new commits messages when you push to remote!
This is great if you want to make sure all of the commits in all of repository branches are meaningful and not lame like `fix1` or `done2`.
This is great if you want to make sure all of the commits in all of your repository branches are meaningful and not lame like `fix1` or `done2`.
Create a file `.github/workflows/opencommit.yml` with contents below:
Create a file `.github/workflows/opencommit.yml` with the contents below:
```yml
name: 'OpenCommit Action'
@@ -64,173 +315,21 @@ jobs:
OCO_OPENAI_BASE_PATH: ''
OCO_DESCRIPTION: false
OCO_EMOJI: false
OCO_MODEL: gpt-3.5-turbo
OCO_MODEL: gpt-3.5-turbo-16k
OCO_LANGUAGE: en
OCO_PROMPT_MODULE: conventional-commit
```
That is it. Now when you push to any branch in your repo — all NEW commits are being improved by never-tired-AI.
That is it. Now when you push to any branch in your repo — all NEW commits are being improved by your never-tired AI.
Make sure you exclude public collaboration branches (`main`, `dev`, `etc`) in `branches-ignore`, so OpenCommit does not rebase commits there when improving the messages.
Make sure you exclude public collaboration branches (`main`, `dev`, `etc`) in `branches-ignore`, so OpenCommit does not rebase commits there while improving the messages.
Interactive rebase (`rebase -i`) changes commit SHA, so commit history in remote becomes different with your local branch history. It's ok when you work on the branch alone, but may be inconvenient for other collaborators.
## Setup OpenCommit as a CLI
You can use OpenCommit by simply running it via CLI like this `oco`. 2 seconds and your staged changes are committed with a meaningful message.
1. Install OpenCommit globally to use in any repository:
```sh
npm install -g opencommit
```
2. Get your API key from [OpenAI](https://platform.openai.com/account/api-keys). Make sure you add payment details, so API works.
3. Set the key to OpenCommit config:
```sh
opencommit config set OCO_OPENAI_API_KEY=<your_api_key>
```
Your api key is stored locally in `~/.opencommit` config file.
## Usage
You can call OpenCommit directly to generate a commit message for your staged changes:
```sh
git add <files...>
opencommit
```
You can also use the `oco` shortcut:
```sh
git add <files...>
oc
```
## Configuration
### Local per repo configuration
Create an `.env` file and add OpenCommit config variables there like this:
```env
OCO_OPENAI_API_KEY=<your openAI API token>
OCO_OPENAI_MAX_TOKENS=<max response tokens from openAI API>
OCO_OPENAI_BASE_PATH=<may be used to set proxy path to openAI api>
OCO_DESCRIPTION=<postface a message with ~3 sentences description>
OCO_EMOJI=<add GitMoji>
OCO_MODEL=<either gpt-3.5-turbo or gpt-4>
OCO_LANGUAGE=<locale, scroll to the bottom to see options>
```
### Global config for all repos
Local config still has more priority as Global config, but you may set `OCO_MODEL` and `OCO_LOCALE` globally and set local configs for `OCO_EMOJI` and `OCO_DESCRIPTION` per repo which is more convenient.
Simply run any of the variable above like this:
```sh
oco config set OCO_OPENAI_API_KEY=gpt-4
```
Configure [GitMoji](https://gitmoji.dev/) to preface a message.
```sh
oco config set OCO_EMOJI=true
```
To remove preface emoji:
```sh
oco config set OCO_EMOJI=false
```
### Switch to GPT-4
By default OpenCommit uses GPT-3.5-turbo (ChatGPT).
You may switch to GPT-4 which performs better, but costs ~x15 times more 🤠
```sh
oco config set OCO_MODEL=gpt-4
```
Make sure you do lowercase `gpt-4` and you have API access to the 4th model. Even if you have ChatGPT+ it doesn't necessarily mean that you have API access to GPT-4.
## Locale configuration
To globally specify the language used to generate commit messages:
```sh
# de, German ,Deutsch
oco config set OCO_LANGUAGE=de
oco config set OCO_LANGUAGE=German
oco config set OCO_LANGUAGE=Deutsch
# fr, French, française
oco config set OCO_LANGUAGE=fr
oco config set OCO_LANGUAGE=French
oco config set OCO_LANGUAGE=française
```
The default language set is **English**
All available languages are currently listed in the [i18n](https://github.com/di-sukharev/opencommit/tree/master/src/i18n) folder
### Git flags
The `opencommit` or `oco` commands can be used in place of the `git commit -m "${generatedMessage}"` command. This means that any regular flags that are used with the `git commit` command will also be applied when using `opencommit` or `oco`.
```sh
oco --no-verify
```
is translated to :
```sh
git commit -m "${generatedMessage}" --no-verify
```
### Ignore files
You can ignore files from submission to OpenAI by creating a `.opencommitignore` file. For example:
```ignorelang
path/to/large-asset.zip
**/*.jpg
```
This is useful for preventing opencommit from uploading artifacts and large files.
By default, opencommit ignores files matching: `*-lock.*` and `*.lock`
## Git hook (KILLER FEATURE)
You can set OpenCommit as Git [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) hook. Hook integrates with you IDE Source Control and allows you edit the message before commit.
To set the hook:
```sh
oco hook set
```
To unset the hook:
```sh
oco hook unset
```
To use the hook:
```sh
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.
Interactive rebase (`rebase -i`) changes commits' SHA, so the commit history in remote becomes different from your local branch history. This is okay if you work on the branch alone, but may be inconvenient for other collaborators.
## Payments
You pay for your own requests to OpenAI API. OpenCommit uses ChatGPT (3.5-turbo) official model, that is ~15x times cheaper than GPT-4.
You pay for your requests to OpenAI API on your own.
OpenCommit stores your key locally.
OpenCommit by default uses ChatGPT (3.5-turbo-16k) official model, which is a lot cheaper than gpt-4.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

61
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "opencommit",
"version": "2.2.1",
"version": "2.4.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "opencommit",
"version": "2.2.1",
"version": "2.4.2",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",
@@ -19,6 +19,7 @@
"axios": "^1.3.4",
"chalk": "^5.2.0",
"cleye": "^1.3.2",
"crypto": "^1.0.1",
"execa": "^7.0.0",
"ignore": "^5.2.4",
"ini": "^3.0.1",
@@ -30,6 +31,7 @@
"opencommit": "out/cli.cjs"
},
"devDependencies": {
"@commitlint/types": "^17.4.4",
"@types/ini": "^1.3.31",
"@types/inquirer": "^9.0.3",
"@types/node": "^16.18.14",
@@ -183,6 +185,49 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@commitlint/types": {
"version": "17.4.4",
"resolved": "https://registry.npmjs.org/@commitlint/types/-/types-17.4.4.tgz",
"integrity": "sha512-amRN8tRLYOsxRr6mTnGGGvB5EmW/4DDjLMgiwK3CCVEmN6Sr/6xePGEpWaspKkckILuUORCwe6VfDBw6uj4axQ==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0"
},
"engines": {
"node": ">=v14"
}
},
"node_modules/@commitlint/types/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@commitlint/types/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -1181,6 +1226,12 @@
"node": ">= 8"
}
},
"node_modules/crypto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -3085,9 +3136,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz",
"integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"

View File

@@ -1,6 +1,6 @@
{
"name": "opencommit",
"version": "2.2.1",
"version": "3.0.3",
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
"keywords": [
"git",
@@ -26,7 +26,8 @@
"author": "https://github.com/di-sukharev",
"license": "MIT",
"files": [
"out/**/*"
"out/cli.cjs",
"out/tiktoken_bg.wasm"
],
"release": {
"branches": [
@@ -41,12 +42,13 @@
"start": "node ./out/cli.cjs",
"dev": "ts-node ./src/cli.ts",
"build": "rimraf out && node esbuild.config.js",
"deploy": "npm run build:push && npm version patch && git push origin --tags && npm publish --tag latest",
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
"deploy": "npm run build:push && npm version patch && git push --follow-tags && npm publish --tag latest",
"lint": "eslint src --ext ts && tsc --noEmit",
"format": "prettier --write src"
},
"devDependencies": {
"@commitlint/types": "^17.4.4",
"@types/ini": "^1.3.31",
"@types/inquirer": "^9.0.3",
"@types/node": "^16.18.14",
@@ -70,6 +72,7 @@
"axios": "^1.3.4",
"chalk": "^5.2.0",
"cleye": "^1.3.2",
"crypto": "^1.0.1",
"execa": "^7.0.0",
"ignore": "^5.2.4",
"ini": "^3.0.1",

View File

@@ -1,4 +1,5 @@
export enum COMMANDS {
config = 'config',
hook = 'hook',
config = 'config'
commitlint = 'commitlint'
}

View File

@@ -1,13 +1,21 @@
import { intro, outro } from '@clack/prompts';
import axios from 'axios';
import chalk from 'chalk';
import { execa } from 'execa';
import {
ChatCompletionRequestMessage,
Configuration as OpenAiApiConfiguration,
OpenAIApi
} from 'openai';
import { CONFIG_MODES, getConfig } from './commands/config';
import { intro, outro } from '@clack/prompts';
import {
CONFIG_MODES,
DEFAULT_MODEL_TOKEN_LIMIT,
getConfig
} from './commands/config';
import { GenerateCommitMessageErrorEnum } from './generateCommitMessageFromGitDiff';
import { tokenCount } from './utils/tokenCount';
const config = getConfig();
@@ -56,6 +64,14 @@ class OpenAi {
max_tokens: maxTokens || 500
};
try {
const REQUEST_TOKENS = messages
.map((msg) => tokenCount(msg.content) + 4)
.reduce((a, b) => a + b, 0);
if (REQUEST_TOKENS > DEFAULT_MODEL_TOKEN_LIMIT - maxTokens) {
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
}
const { data } = await this.openAI.createChatCompletion(params);
const message = data.choices[0].message;
@@ -88,10 +104,8 @@ export const getOpenCommitLatestVersion = async (): Promise<
string | undefined
> => {
try {
const { data } = await axios.get(
'https://unpkg.com/opencommit/package.json'
);
return data.version;
const { stdout } = await execa('npm', ['view', 'opencommit', 'version']);
return stdout;
} catch (_) {
outro('Error while getting the latest version of opencommit');
return undefined;

View File

@@ -1,12 +1,13 @@
#!/usr/bin/env node
import { cli } from 'cleye';
import packageJSON from '../package.json' assert { type: 'json' };
import packageJSON from '../package.json';
import { commit } from './commands/commit';
import { commitlintConfigCommand } from './commands/commitlint';
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 { checkIsLatestVersion } from './utils/checkIsLatestVersion';
const extraArgs = process.argv.slice(2);
@@ -15,7 +16,7 @@ cli(
{
version: packageJSON.version,
name: 'opencommit',
commands: [configCommand, hookCommand],
commands: [configCommand, hookCommand, commitlintConfigCommand],
flags: {},
ignoreArgv: (type) => type === 'unknown-flag' || type === 'argument',
help: { description: packageJSON.description }

9
src/commands/README.md Normal file
View File

@@ -0,0 +1,9 @@
# @commitlint Module for opencommit
1. Load commitlint configuration within tree.
2. Generate a commit with commitlint prompt:
- Will not run if hash is the same.
- Infer a prompt for each commitlint rule.
- Ask OpenAI to generate consistency with embedded commitlint rules.
- Store configuration close to commitlint configuration.
3. Replace conventional-commit prompt with commitlint prompt.

View File

@@ -1,7 +1,17 @@
import chalk from 'chalk';
import { execa } from 'execa';
import {
generateCommitMessageByDiff
} from '../generateCommitMessageFromGitDiff';
confirm,
intro,
isCancel,
multiselect,
outro,
select,
spinner
} from '@clack/prompts';
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
import {
assertGitRepo,
getChangedFiles,
@@ -9,38 +19,51 @@ import {
getStagedFiles,
gitAdd
} from '../utils/git';
import {
spinner,
confirm,
outro,
isCancel,
intro,
multiselect,
select
} from '@clack/prompts';
import chalk from 'chalk';
import { trytm } from '../utils/trytm';
import { getConfig } from './config';
const config = getConfig();
const getGitRemotes = async () => {
const { stdout } = await execa('git', ['remote']);
return stdout.split('\n').filter((remote) => Boolean(remote.trim()));
};
// Check for the presence of message templates
const checkMessageTemplate = (extraArgs: string[]): string | false => {
for (const key in extraArgs) {
if (extraArgs[key].includes(config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER))
return extraArgs[key];
}
return false;
};
const generateCommitMessageFromGitDiff = async (
diff: string,
extraArgs: string[]
): Promise<void> => {
await assertGitRepo();
const commitSpinner = spinner();
commitSpinner.start('Generating the commit message');
try {
const commitMessage = await generateCommitMessageByDiff(diff);
let commitMessage = await generateCommitMessageByDiff(diff);
const messageTemplate = checkMessageTemplate(extraArgs);
if (
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER &&
typeof messageTemplate === 'string'
) {
commitMessage = messageTemplate.replace(
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
commitMessage
);
}
commitSpinner.stop('📝 Commit message generated');
outro(
`Commit message:
`Generated commit message:
${chalk.grey('——————————————————')}
${commitMessage}
${chalk.grey('——————————————————')}`
@@ -78,7 +101,7 @@ ${chalk.grey('——————————————————')}`
if (isPushConfirmedByUser && !isCancel(isPushConfirmedByUser)) {
const pushSpinner = spinner();
pushSpinner.start(`Running \`git push ${remotes[0]}\``);
pushSpinner.start(`Running 'git push ${remotes[0]}'`);
const { stdout } = await execa('git', [
'push',
@@ -106,7 +129,7 @@ ${chalk.grey('——————————————————')}`
if (!isCancel(selectedRemote)) {
const pushSpinner = spinner();
pushSpinner.start(`Running \`git push ${selectedRemote}\``);
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
const { stdout } = await execa('git', ['push', selectedRemote]);

View File

@@ -0,0 +1,46 @@
import chalk from 'chalk';
import { command } from 'cleye';
import { intro, outro } from '@clack/prompts';
import { COMMANDS } from '../CommandsEnum';
import { configureCommitlintIntegration } from '../modules/commitlint/config';
import { getCommitlintLLMConfig } from '../modules/commitlint/utils';
export enum CONFIG_MODES {
get = 'get',
force = 'force'
}
export const commitlintConfigCommand = command(
{
name: COMMANDS.commitlint,
parameters: ['<mode>']
},
async (argv) => {
intro('opencommit — configure @commitlint');
try {
const { mode } = argv._;
if (mode === CONFIG_MODES.get) {
const commitLintConfig = await getCommitlintLLMConfig();
outro(commitLintConfig.toString());
return;
}
if (mode === CONFIG_MODES.force) {
await configureCommitlintIntegration(true);
return;
}
throw new Error(
`Unsupported mode: ${mode}. Valid modes are: "force" and "get"`
);
} catch (error) {
outro(`${chalk.red('✖')} ${error}`);
process.exit(1);
}
}
);

View File

@@ -1,15 +1,16 @@
import { command } from 'cleye';
import { join as pathJoin } from 'path';
import { parse as iniParse, stringify as iniStringify } from 'ini';
import { existsSync, writeFileSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { intro, outro } from '@clack/prompts';
import chalk from 'chalk';
import { command } from 'cleye';
import * as dotenv from 'dotenv';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { parse as iniParse, stringify as iniStringify } from 'ini';
import { homedir } from 'os';
import { join as pathJoin } from 'path';
import { intro, outro } from '@clack/prompts';
import { COMMANDS } from '../CommandsEnum';
import { getI18nLocal } from '../i18n';
import * as dotenv from 'dotenv';
dotenv.config();
export enum CONFIG_KEYS {
@@ -19,9 +20,13 @@ export enum CONFIG_KEYS {
OCO_DESCRIPTION = 'OCO_DESCRIPTION',
OCO_EMOJI = 'OCO_EMOJI',
OCO_MODEL = 'OCO_MODEL',
OCO_LANGUAGE = 'OCO_LANGUAGE'
OCO_LANGUAGE = 'OCO_LANGUAGE',
OCO_MESSAGE_TEMPLATE_PLACEHOLDER = 'OCO_MESSAGE_TEMPLATE_PLACEHOLDER',
OCO_PROMPT_MODULE = 'OCO_PROMPT_MODULE'
}
export const DEFAULT_MODEL_TOKEN_LIMIT = 4096;
export enum CONFIG_MODES {
get = 'get',
set = 'set'
@@ -42,7 +47,7 @@ const validateConfig = (
};
export const configValidators = {
[CONFIG_KEYS.OCO_OPENAI_API_KEY](value: any) {
[CONFIG_KEYS.OCO_OPENAI_API_KEY](value: any, config?: any) {
validateConfig(CONFIG_KEYS.OCO_OPENAI_API_KEY, value, 'Cannot be empty');
validateConfig(
CONFIG_KEYS.OCO_OPENAI_API_KEY,
@@ -51,7 +56,7 @@ export const configValidators = {
);
validateConfig(
CONFIG_KEYS.OCO_OPENAI_API_KEY,
value.length === 51,
config[CONFIG_KEYS.OCO_OPENAI_BASE_PATH] || value.length === 51,
'Must be 51 characters long'
);
@@ -118,9 +123,32 @@ export const configValidators = {
[CONFIG_KEYS.OCO_MODEL](value: any) {
validateConfig(
CONFIG_KEYS.OCO_MODEL,
['gpt-3.5-turbo', 'gpt-4'].includes(value),
`${value} is not supported yet, use 'gpt-4' or 'gpt-3.5-turbo' (default)`
[
'gpt-3.5-turbo',
'gpt-4',
'gpt-3.5-turbo-16k',
'gpt-3.5-turbo-0613'
].includes(value),
`${value} is not supported yet, use 'gpt-4', 'gpt-3.5-turbo-16k' (default), 'gpt-3.5-turbo-0613' or 'gpt-3.5-turbo'`
);
return value;
},
[CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER](value: any) {
validateConfig(
CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
value.startsWith('$'),
`${value} must start with $, for example: '$msg'`
);
return value;
},
[CONFIG_KEYS.OCO_PROMPT_MODULE](value: any) {
validateConfig(
CONFIG_KEYS.OCO_PROMPT_MODULE,
['conventional-commit', '@commitlint'].includes(value),
`${value} is not supported yet, use '@commitlint' or 'conventional-commit' (default)`
);
return value;
}
};
@@ -134,12 +162,17 @@ const configPath = pathJoin(homedir(), '.opencommit');
export const getConfig = (): ConfigType | null => {
const configFromEnv = {
OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
OCO_OPENAI_MAX_TOKENS: process.env.OCO_OPENAI_MAX_TOKENS ? Number(process.env.OCO_OPENAI_MAX_TOKENS) : undefined,
OCO_OPENAI_MAX_TOKENS: process.env.OCO_OPENAI_MAX_TOKENS
? Number(process.env.OCO_OPENAI_MAX_TOKENS)
: undefined,
OCO_OPENAI_BASE_PATH: process.env.OCO_OPENAI_BASE_PATH,
OCO_DESCRIPTION: process.env.OCO_DESCRIPTION === 'true' ? true : false,
OCO_EMOJI: process.env.OCO_EMOJI === 'true' ? true : false,
OCO_MODEL: process.env.OCO_MODEL || 'gpt-3.5-turbo',
OCO_LANGUAGE: process.env.OCO_LANGUAGE || 'en'
OCO_MODEL: process.env.OCO_MODEL || 'gpt-3.5-turbo-16k',
OCO_LANGUAGE: process.env.OCO_LANGUAGE || 'en',
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER || '$msg',
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE || 'conventional-commit'
};
const configExists = existsSync(configPath);
@@ -149,14 +182,18 @@ export const getConfig = (): ConfigType | null => {
const config = iniParse(configFile);
for (const configKey of Object.keys(config)) {
if (!config[configKey] || ['null', 'undefined'].includes(config[configKey])) {
if (
!config[configKey] ||
['null', 'undefined'].includes(config[configKey])
) {
config[configKey] = undefined;
continue;
}
try {
const validator = configValidators[configKey as CONFIG_KEYS];
const validValue = validator(
config[configKey] ?? configFromEnv[configKey as CONFIG_KEYS]
config[configKey] ?? configFromEnv[configKey as CONFIG_KEYS],
config
);
config[configKey] = validValue;

View File

@@ -1,11 +1,13 @@
import chalk from 'chalk';
import { command } from 'cleye';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { command } from 'cleye';
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
import { existsSync } from 'fs';
import chalk from 'chalk';
import { intro, outro } from '@clack/prompts';
import { COMMANDS } from '../CommandsEnum.js';
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
const HOOK_NAME = 'prepare-commit-msg';
const DEFAULT_SYMLINK_URL = path.join('.git', 'hooks', HOOK_NAME);

View File

@@ -1,9 +1,11 @@
import fs from 'fs/promises';
import chalk from 'chalk';
import fs from 'fs/promises';
import { intro, outro, spinner } from '@clack/prompts';
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
import { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
import { getConfig } from './config';
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
const [messageFilePath, commitSource] = process.argv.slice(2);

View File

@@ -2,62 +2,20 @@ import {
ChatCompletionRequestMessage,
ChatCompletionRequestMessageRoleEnum
} from 'openai';
import { api } from './api';
import { getConfig } from './commands/config';
import { DEFAULT_MODEL_TOKEN_LIMIT, getConfig } from './commands/config';
import { getMainCommitPrompt } from './prompts';
import { mergeDiffs } from './utils/mergeDiffs';
import { i18n, I18nLocals } from './i18n';
import { tokenCount } from './utils/tokenCount';
const config = getConfig();
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
const INIT_MESSAGES_PROMPT: Array<ChatCompletionRequestMessage> = [
{
role: ChatCompletionRequestMessageRoleEnum.System,
// 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 WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.': 'Do not preface the commit with anything.'}
${config?.OCO_DESCRIPTION ? 'Add a short description of WHY the changes are 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}\`);
});`
},
{
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
${config?.OCO_EMOJI ? '✨ ' : ''}${translation.commitFeat}
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
}
];
const generateCommitMessageChatCompletionPrompt = (
const generateCommitMessageChatCompletionPrompt = async (
diff: string
): Array<ChatCompletionRequestMessage> => {
): Promise<Array<ChatCompletionRequestMessage>> => {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt();
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
chatContextAsCompletionRequest.push({
@@ -74,36 +32,47 @@ export enum GenerateCommitMessageErrorEnum {
emptyMessage = 'EMPTY_MESSAGE'
}
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
(msg) => tokenCount(msg.content) + 4
).reduce((a, b) => a + b, 0);
const MAX_REQ_TOKENS = 3000 - INIT_MESSAGES_PROMPT_LENGTH;
const ADJUSTMENT_FACTOR = 20;
export const generateCommitMessageByDiff = async (
diff: string
): Promise<string> => {
try {
if (tokenCount(diff) >= MAX_REQ_TOKENS) {
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt();
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
(msg) => tokenCount(msg.content) + 4
).reduce((a, b) => a + b, 0);
const MAX_REQUEST_TOKENS =
DEFAULT_MODEL_TOKEN_LIMIT -
ADJUSTMENT_FACTOR -
INIT_MESSAGES_PROMPT_LENGTH -
config?.OCO_OPENAI_MAX_TOKENS;
if (tokenCount(diff) >= MAX_REQUEST_TOKENS) {
const commitMessagePromises = await getCommitMsgsPromisesFromFileDiffs(
diff,
MAX_REQ_TOKENS
MAX_REQUEST_TOKENS
);
const commitMessages = await Promise.all(commitMessagePromises);
const commitMessages = [];
for (const promise of commitMessagePromises) {
commitMessages.push(await promise);
await delay(2000);
}
return commitMessages.join('\n\n');
} else {
const messages = generateCommitMessageChatCompletionPrompt(diff);
const commitMessage = await api.generateCommitMessage(messages);
if (!commitMessage)
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
return commitMessage;
}
const messages = await generateCommitMessageChatCompletionPrompt(diff);
const commitMessage = await api.generateCommitMessage(messages);
if (!commitMessage)
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
return commitMessage;
} catch (error) {
throw error;
}
@@ -123,25 +92,67 @@ function getMessagesPromisesByChangesInFile(
maxChangeLength
);
const lineDiffsWithHeader = mergedChanges.map(
(change) => fileHeader + change
const lineDiffsWithHeader = [];
for (const change of mergedChanges) {
const totalChange = fileHeader + change;
if (tokenCount(totalChange) > maxChangeLength) {
// If the totalChange is too large, split it into smaller pieces
const splitChanges = splitDiff(totalChange, maxChangeLength);
lineDiffsWithHeader.push(...splitChanges);
} else {
lineDiffsWithHeader.push(totalChange);
}
}
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map(
async (lineDiff) => {
const messages = await generateCommitMessageChatCompletionPrompt(
separator + lineDiff
);
return api.generateCommitMessage(messages);
}
);
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((lineDiff) => {
const messages = generateCommitMessageChatCompletionPrompt(
separator + lineDiff
);
return api.generateCommitMessage(messages);
});
return commitMsgsFromFileLineDiffs;
}
export function getCommitMsgsPromisesFromFileDiffs(
function splitDiff(diff: string, maxChangeLength: number) {
const lines = diff.split('\n');
const splitDiffs = [];
let currentDiff = '';
for (let line of lines) {
// If a single line exceeds maxChangeLength, split it into multiple lines
while (tokenCount(line) > maxChangeLength) {
const subLine = line.substring(0, maxChangeLength);
line = line.substring(maxChangeLength);
splitDiffs.push(subLine);
}
// Check the tokenCount of the currentDiff and the line separately
if (tokenCount(currentDiff) + tokenCount('\n' + line) > maxChangeLength) {
// If adding the next line would exceed the maxChangeLength, start a new diff
splitDiffs.push(currentDiff);
currentDiff = line;
} else {
// Otherwise, add the line to the current diff
currentDiff += '\n' + line;
}
}
// Add the last diff
if (currentDiff) {
splitDiffs.push(currentDiff);
}
return splitDiffs;
}
export const getCommitMsgsPromisesFromFileDiffs = async (
diff: string,
maxDiffLength: number
) {
) => {
const separator = 'diff --git ';
const diffByFiles = diff.split(separator).slice(1);
@@ -162,12 +173,17 @@ export function getCommitMsgsPromisesFromFileDiffs(
commitMessagePromises.push(...messagesPromises);
} else {
const messages = generateCommitMessageChatCompletionPrompt(
const messages = await generateCommitMessageChatCompletionPrompt(
separator + fileDiff
);
commitMessagePromises.push(api.generateCommitMessage(messages));
}
}
return commitMessagePromises;
};
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -1,12 +1,14 @@
import { unlinkSync, writeFileSync } from 'fs';
import core from '@actions/core';
import github from '@actions/github';
import exec from '@actions/exec';
import github from '@actions/github';
import { intro, outro } from '@clack/prompts';
import { PushEvent } from '@octokit/webhooks-types';
import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff';
import { sleep } from './utils/sleep';
import { randomIntFromInterval } from './utils/randomIntFromInterval';
import { unlinkSync, writeFileSync } from 'fs';
import { sleep } from './utils/sleep';
// This should be a token with access to your repository scoped in as a secret.
// The YML workflow will need to set GITHUB_TOKEN with the GitHub Secret Token
@@ -133,6 +135,16 @@ async function improveCommitMessages(
improvedMessagesWithSHAs
);
// Check if there are actually any changes in the commit messages
const messagesChanged = improvedMessagesWithSHAs.some(
({ sha, msg }, index) => msg !== commitsToImprove[index].message
);
if (!messagesChanged) {
console.log('No changes in commit messages detected, skipping rebase');
return;
}
const createCommitMessageFile = (message: string, index: number) =>
writeFileSync(`./commit-${index}.txt`, message);
improvedMessagesWithSHAs.forEach(({ msg }, i) =>

View File

@@ -1,22 +1,22 @@
import en from '../i18n/en.json' assert { type: 'json' };
import cs from '../i18n/cs.json' assert { type: 'json' };
import de from '../i18n/de.json' assert { type: 'json' };
import fr from '../i18n/fr.json' assert { type: 'json' };
import it from '../i18n/it.json' assert { type: 'json' };
import ko from '../i18n/ko.json' assert { type: 'json' };
import zh_CN from '../i18n/zh_CN.json' assert { type: 'json' };
import zh_TW from '../i18n/zh_TW.json' assert { type: 'json' };
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' };
import nl from '../i18n/nl.json' assert { type: 'json' };
import ru from '../i18n/ru.json' assert { type: 'json' };
import id_ID from '../i18n/id_ID.json' assert { type: 'json' };
import pl from '../i18n/pl.json' assert { type: 'json' };
import tr from '../i18n/tr.json' assert { type: 'json' };
import th from '../i18n/th.json' assert { type: 'json' };
import cs from '../i18n/cs.json';
import de from '../i18n/de.json';
import en from '../i18n/en.json';
import es_ES from '../i18n/es_ES.json';
import fr from '../i18n/fr.json';
import id_ID from '../i18n/id_ID.json';
import it from '../i18n/it.json';
import ja from '../i18n/ja.json';
import ko from '../i18n/ko.json';
import nl from '../i18n/nl.json';
import pl from '../i18n/pl.json';
import pt_br from '../i18n/pt_br.json';
import ru from '../i18n/ru.json';
import sv from '../i18n/sv.json';
import th from '../i18n/th.json';
import tr from '../i18n/tr.json';
import vi_VN from '../i18n/vi_VN.json';
import zh_CN from '../i18n/zh_CN.json';
import zh_TW from '../i18n/zh_TW.json';
export enum I18nLocals {
'en' = 'en',
@@ -36,7 +36,7 @@ export enum I18nLocals {
'id_ID' = 'id_ID',
'pl' = 'pl',
'tr' = 'tr',
'th' = 'th',
'th' = 'th'
}
export const i18n = {

View File

@@ -0,0 +1,79 @@
import { spinner } from '@clack/prompts';
import { api } from '../../api';
import { getConfig } from '../../commands/config';
import { i18n, I18nLocals } from '../../i18n';
import { COMMITLINT_LLM_CONFIG_PATH } from './constants';
import { computeHash } from './crypto';
import { commitlintPrompts, inferPromptsFromCommitlintConfig } from './prompts';
import { getCommitLintPWDConfig } from './pwd-commitlint';
import { CommitlintLLMConfig } from './types';
import * as utils from './utils';
const config = getConfig();
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
export const configureCommitlintIntegration = async (force = false) => {
const spin = spinner();
spin.start('Loading @commitlint configuration');
const fileExists = await utils.commitlintLLMConfigExists();
let commitLintConfig = await getCommitLintPWDConfig();
// debug complete @commitlint configuration
// await fs.writeFile(
// `${OPENCOMMIT_COMMITLINT_CONFIG}-commitlint-debug`,
// JSON.stringify(commitLintConfig, null, 2)
// );
const hash = await computeHash(JSON.stringify(commitLintConfig));
spin.stop(`Read @commitlint configuration (hash: ${hash})`);
if (fileExists) {
// Check if we need to update the prompts.
const { hash: existingHash } = await utils.getCommitlintLLMConfig();
if (hash === existingHash && !force) {
spin.stop(
'Hashes are the same, no need to update the config. Run "force" command to bypass.'
);
return;
}
}
spin.start('Generating consistency with given @commitlint rules');
const prompts = inferPromptsFromCommitlintConfig(commitLintConfig);
const consistencyPrompts =
commitlintPrompts.GEN_COMMITLINT_CONSISTENCY_PROMPT(prompts);
// debug prompt which will generate a consistency
// await fs.writeFile(
// `${COMMITLINT_LLM_CONFIG}-debug`,
// consistencyPrompts.map((p) => p.content)
// );
let consistency =
(await api.generateCommitMessage(consistencyPrompts)) || '{}';
// Cleanup the consistency answer. Sometimes 'gpt-3.5-turbo' sends rule's back.
prompts.forEach((prompt) => (consistency = consistency.replace(prompt, '')));
// ... remaining might be extra set of "\n"
consistency = utils.removeDoubleNewlines(consistency);
const commitlintLLMConfig: CommitlintLLMConfig = {
hash,
prompts,
consistency: {
[translation.localLanguage]: {
...JSON.parse(consistency as string)
}
}
};
await utils.writeCommitlintLLMConfig(commitlintLLMConfig);
spin.stop(`Done - please review contents of ${COMMITLINT_LLM_CONFIG_PATH}`);
};

View File

@@ -0,0 +1 @@
export const COMMITLINT_LLM_CONFIG_PATH = `${process.env.PWD}/.opencommit-commitlint`;

View File

@@ -0,0 +1,15 @@
import crypto from 'crypto';
export const computeHash = async (
content: string,
algorithm: string = 'sha256'
): Promise<string> => {
try {
const hash = crypto.createHash(algorithm);
hash.update(content);
return hash.digest('hex');
} catch (error) {
console.error('Error while computing hash:', error);
throw error;
}
};

View File

@@ -0,0 +1,283 @@
import chalk from 'chalk';
import {
ChatCompletionRequestMessage,
ChatCompletionRequestMessageRoleEnum
} from 'openai';
import { outro } from '@clack/prompts';
import {
PromptConfig,
QualifiedConfig,
RuleConfigSeverity,
RuleConfigTuple
} from '@commitlint/types';
import { getConfig } from '../../commands/config';
import { i18n, I18nLocals } from '../../i18n';
import { IDENTITY, INIT_DIFF_PROMPT } from '../../prompts';
const config = getConfig();
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
type DeepPartial<T> = {
[P in keyof T]?: {
[K in keyof T[P]]?: T[P][K];
};
};
type PromptFunction = (
applicable: string,
value: any,
prompt: DeepPartial<PromptConfig>
) => string;
type PromptResolverFunction = (
key: string,
applicable: string,
value: any,
prompt?: DeepPartial<PromptConfig>
) => string;
/**
* Extracts more contexte for each type-enum.
* IDEA: replicate the concept for scopes and refactor to a generic feature.
*/
const getTypeRuleExtraDescription = (
type: string,
prompt?: DeepPartial<PromptConfig>
) => prompt?.questions?.type?.enum?.[type]?.description;
/*
IDEA: Compress llm readable prompt for each section of commit message: one line for header, one line for scope, etc.
- The type must be in lowercase and should be one of the following values: featuring, fixing, documenting, styling, refactoring, testing, chores, perf, build, ci, revert.
- The scope should not be empty and provide context for the change (e.g., module or file changed).
- The subject should not be empty, should not end with a period, and should provide a concise description of the change. It should not be in sentence-case, start-case, pascal-case, or upper-case.
*/
const llmReadableRules: {
[ruleName: string]: PromptResolverFunction;
} = {
blankline: (key, applicable) =>
`There should ${applicable} be a blank line at the beginning of the ${key}.`,
caseRule: (key, applicable, value: string | Array<string>) =>
`The ${key} should ${applicable} be in ${
Array.isArray(value)
? `one of the following case:
- ${value.join('\n - ')}.`
: `${value} case.`
}`,
emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`,
enumRule: (key, applicable, value: string | Array<string>) =>
`The ${key} should ${applicable} be one of the following values:
- ${Array.isArray(value) ? value.join('\n - ') : value}.`,
enumTypeRule: (key, applicable, value: string | Array<string>, prompt) =>
`The ${key} should ${applicable} be one of the following values:
- ${
Array.isArray(value)
? value
.map((v) => {
const description = getTypeRuleExtraDescription(v, prompt);
if (description) {
return `${v} (${description})`;
} else return v;
})
.join('\n - ')
: value
}.`,
fullStopRule: (key, applicable, value: string) =>
`The ${key} should ${applicable} end with '${value}'.`,
maxLengthRule: (key, applicable, value: string) =>
`The ${key} should ${applicable} have ${value} characters or less.`,
minLengthRule: (key, applicable, value: string) =>
`The ${key} should ${applicable} have ${value} characters or more.`
};
/**
* TODO: Validate rules to every rule in the @commitlint configuration.
* IDEA: Plugins can extend the list of rule. Provide user with a way to infer or extend when "No prompt handler for rule".
*/
const rulesPrompts: {
[ruleName: string]: PromptFunction;
} = {
'body-case': (applicable: string, value: string | Array<string>) =>
llmReadableRules.caseRule('body', applicable, value),
'body-empty': (applicable: string) =>
llmReadableRules.emptyRule('body', applicable, undefined),
'body-full-stop': (applicable: string, value: string) =>
llmReadableRules.fullStopRule('body', applicable, value),
'body-leading-blank': (applicable: string) =>
llmReadableRules.blankline('body', applicable, undefined),
'body-max-length': (applicable: string, value: string) =>
llmReadableRules.maxLengthRule('body', applicable, value),
'body-max-line-length': (applicable: string, value: string) =>
`Each line of the body should ${applicable} have ${value} characters or less.`,
'body-min-length': (applicable: string, value: string) =>
llmReadableRules.minLengthRule('body', applicable, value),
'footer-case': (applicable: string, value: string | Array<string>) =>
llmReadableRules.caseRule('footer', applicable, value),
'footer-empty': (applicable: string) =>
llmReadableRules.emptyRule('footer', applicable, undefined),
'footer-leading-blank': (applicable: string) =>
llmReadableRules.blankline('footer', applicable, undefined),
'footer-max-length': (applicable: string, value: string) =>
llmReadableRules.maxLengthRule('footer', applicable, value),
'footer-max-line-length': (applicable: string, value: string) =>
`Each line of the footer should ${applicable} have ${value} characters or less.`,
'footer-min-length': (applicable: string, value: string) =>
llmReadableRules.minLengthRule('footer', applicable, value),
'header-case': (applicable: string, value: string | Array<string>) =>
llmReadableRules.caseRule('header', applicable, value),
'header-full-stop': (applicable: string, value: string) =>
llmReadableRules.fullStopRule('header', applicable, value),
'header-max-length': (applicable: string, value: string) =>
llmReadableRules.maxLengthRule('header', applicable, value),
'header-min-length': (applicable: string, value: string) =>
llmReadableRules.minLengthRule('header', applicable, value),
'references-empty': (applicable: string) =>
llmReadableRules.emptyRule('references section', applicable, undefined),
'scope-case': (applicable: string, value: string | Array<string>) =>
llmReadableRules.caseRule('scope', applicable, value),
'scope-empty': (applicable: string) =>
llmReadableRules.emptyRule('scope', applicable, undefined),
'scope-enum': (applicable: string, value: string | Array<string>) =>
llmReadableRules.enumRule('type', applicable, value),
'scope-max-length': (applicable: string, value: string) =>
llmReadableRules.maxLengthRule('scope', applicable, value),
'scope-min-length': (applicable: string, value: string) =>
llmReadableRules.minLengthRule('scope', applicable, value),
'signed-off-by': (applicable: string, value: string) =>
`The commit message should ${applicable} have a "Signed-off-by" line with the value "${value}".`,
'subject-case': (applicable: string, value: string | Array<string>) =>
llmReadableRules.caseRule('subject', applicable, value),
'subject-empty': (applicable: string) =>
llmReadableRules.emptyRule('subject', applicable, undefined),
'subject-full-stop': (applicable: string, value: string) =>
llmReadableRules.fullStopRule('subject', applicable, value),
'subject-max-length': (applicable: string, value: string) =>
llmReadableRules.maxLengthRule('subject', applicable, value),
'subject-min-length': (applicable: string, value: string) =>
llmReadableRules.minLengthRule('subject', applicable, value),
'type-case': (applicable: string, value: string | Array<string>) =>
llmReadableRules.caseRule('type', applicable, value),
'type-empty': (applicable: string) =>
llmReadableRules.emptyRule('type', applicable, undefined),
'type-enum': (applicable: string, value: string | Array<string>, prompt) =>
llmReadableRules.enumTypeRule('type', applicable, value, prompt),
'type-max-length': (applicable: string, value: string) =>
llmReadableRules.maxLengthRule('type', applicable, value),
'type-min-length': (applicable: string, value: string) =>
llmReadableRules.minLengthRule('type', applicable, value)
};
const getPrompt = (
ruleName: string,
ruleConfig: RuleConfigTuple<unknown>,
prompt: DeepPartial<PromptConfig>
) => {
const [severity, applicable, value] = ruleConfig;
// Should we exclude "Disabled" properties?
// Is this used to disable a subjacent rule when extending presets?
if (severity === RuleConfigSeverity.Disabled) return null;
const promptFn = rulesPrompts[ruleName];
if (promptFn) {
return promptFn(applicable, value, prompt);
}
// Plugins may add their custom rules.
// We might want to call OpenAI to build this rule's llm-readable prompt.
outro(`${chalk.red('✖')} No prompt handler for rule "${ruleName}".`);
return `Please manualy set the prompt for rule "${ruleName}".`;
};
export const inferPromptsFromCommitlintConfig = (
config: QualifiedConfig
): string[] => {
const { rules, prompt } = config;
if (!rules) return [];
return Object.keys(rules)
.map((ruleName) =>
getPrompt(ruleName, rules[ruleName] as RuleConfigTuple<unknown>, prompt)
)
.filter((prompt) => prompt !== null) as string[];
};
/**
* Breaking down commit message structure for conventional commit, and mapping bits with
* ubiquitous language from @commitlint.
* While gpt-4 does this on it self, gpt-3.5 can't map this on his own atm.
*/
const STRUCTURE_OF_COMMIT = `
- Header of commit is composed of type, scope, subject: <type-of-commit>(<scope-of-commit>): <subject-of-commit>
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`;
// Prompt to generate LLM-readable rules based on @commitlint rules.
const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
prompts: string[]
): ChatCompletionRequestMessage[] => [
{
role: ChatCompletionRequestMessageRoleEnum.Assistant,
// prettier-ignore
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
Here are the specific requirements and conventions that should be strictly followed:
Commit Message Conventions:
- The commit message consists of three parts: Header, Body, and Footer.
- Header:
- Format: \`<type>(<scope>): <subject>\`
- ${prompts.join('\n- ')}
JSON Output Format:
- The JSON output should contain the commit messages for a bug fix and a new feature in the following format:
\`\`\`json
{
"localLanguage": "${translation.localLanguage}",
"commitFix": "<Header of commit for bug fix>",
"commitFeat": "<Header of commit for feature>",
"commitDescription": "<Description of commit for both the bug fix and the feature>"
}
\`\`\`
- The "commitDescription" should not include the commit messages header, only the description.
- Description should not be more than 74 characters.
Additional Details:
- Changing the variable 'port' to uppercase 'PORT' is considered a bug fix.
- Allowing the server to listen on a port specified through the environment variable is considered a new feature.
Example Git Diff is to follow:`
},
INIT_DIFF_PROMPT
];
/**
* Prompt to have LLM generate a message using @commitlint rules.
*
* @param language
* @param prompts
* @returns
*/
const INIT_MAIN_PROMPT = (
language: string,
prompts: string[]
): ChatCompletionRequestMessage => ({
role: ChatCompletionRequestMessageRoleEnum.System,
// prettier-ignore
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.' : 'Do not preface the commit with anything.'}
${config?.OCO_DESCRIPTION ? 'Add a short description of WHY the changes are 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 ${language} to answer.
You will strictly follow the following conventions to generate the content of the commit message:
- ${prompts.join('\n- ')}
The conventions refers to the following structure of commit message:
${STRUCTURE_OF_COMMIT}
`
});
export const commitlintPrompts = {
INIT_MAIN_PROMPT,
GEN_COMMITLINT_CONSISTENCY_PROMPT
};

View File

@@ -0,0 +1,25 @@
import path from 'path';
const nodeModulesPath = path.join(
process.env.PWD || process.cwd(),
'node_modules',
'@commitlint',
'load'
);
/**
* This code is loading the configuration for the `@commitlint` package from the current working
* directory (`process.env.PWD`) by requiring the `load` module from the `@commitlint` package.
*
* @returns
*/
export const getCommitLintPWDConfig = async () => {
const load = require(nodeModulesPath).default;
if (load && typeof load === 'function') {
return await load();
}
// @commitlint/load is not a function
return null;
};

View File

@@ -0,0 +1,11 @@
import { i18n } from '../../i18n';
export type ConsistencyPrompt = (typeof i18n)[keyof typeof i18n];
export type CommitlintLLMConfig = {
hash: string;
prompts: string[];
consistency: {
[key: string]: ConsistencyPrompt;
};
};

View File

@@ -0,0 +1,47 @@
import fs from 'fs/promises';
import { COMMITLINT_LLM_CONFIG_PATH } from './constants';
import { CommitlintLLMConfig } from './types';
/**
* Removes the "\n" only if occurring twice
*/
export const removeDoubleNewlines = (input: string): string => {
const pattern = /\\n\\n/g;
if (pattern.test(input)) {
const newInput = input.replace(pattern, '');
return removeDoubleNewlines(newInput);
}
return input;
};
export const commitlintLLMConfigExists = async (): Promise<boolean> => {
let exists;
try {
await fs.access(COMMITLINT_LLM_CONFIG_PATH);
exists = true;
} catch (e) {
exists = false;
}
return exists;
};
export const writeCommitlintLLMConfig = async (
commitlintLLMConfig: CommitlintLLMConfig
): Promise<void> => {
await fs.writeFile(
COMMITLINT_LLM_CONFIG_PATH,
JSON.stringify(commitlintLLMConfig, null, 2)
);
};
export const getCommitlintLLMConfig =
async (): Promise<CommitlintLLMConfig> => {
const content = await fs.readFile(COMMITLINT_LLM_CONFIG_PATH);
const commitLintLLMConfig = JSON.parse(
content.toString()
) as CommitlintLLMConfig;
return commitLintLLMConfig;
};

110
src/prompts.ts Normal file
View File

@@ -0,0 +1,110 @@
import {
ChatCompletionRequestMessage,
ChatCompletionRequestMessageRoleEnum
} from 'openai';
import { note } from '@clack/prompts';
import { getConfig } from './commands/config';
import { i18n, I18nLocals } from './i18n';
import { configureCommitlintIntegration } from './modules/commitlint/config';
import { commitlintPrompts } from './modules/commitlint/prompts';
import { ConsistencyPrompt } from './modules/commitlint/types';
import * as utils from './modules/commitlint/utils';
const config = getConfig();
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
export const IDENTITY =
'You are to act as the author of a commit message in git.';
const INIT_MAIN_PROMPT = (language: string): ChatCompletionRequestMessage => ({
role: ChatCompletionRequestMessageRoleEnum.System,
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the conventional commit convention and explain WHAT were the changes and mainly WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message.
${
config?.OCO_EMOJI
? 'Use GitMoji convention to preface the commit.'
: 'Do not preface the commit with anything.'
}
${
config?.OCO_DESCRIPTION
? 'Add a short description of WHY the changes are 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 ${language} for the commit message.`
});
export const INIT_DIFF_PROMPT: ChatCompletionRequestMessage = {
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}\`);
});`
};
const INIT_CONSISTENCY_PROMPT = (
translation: ConsistencyPrompt
): ChatCompletionRequestMessage => ({
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
${config?.OCO_EMOJI ? '✨ ' : ''}${translation.commitFeat}
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
});
export const getMainCommitPrompt = async (): Promise<
ChatCompletionRequestMessage[]
> => {
switch (config?.OCO_PROMPT_MODULE) {
case '@commitlint':
if (!(await utils.commitlintLLMConfigExists())) {
note(
`OCO_PROMPT_MODULE is @commitlint but you haven't generated consistency for this project yet.`
);
await configureCommitlintIntegration();
}
// Replace example prompt with a prompt that's generated by OpenAI for the commitlint config.
const commitLintConfig = await utils.getCommitlintLLMConfig();
return [
commitlintPrompts.INIT_MAIN_PROMPT(
translation.localLanguage,
commitLintConfig.prompts
),
INIT_DIFF_PROMPT,
INIT_CONSISTENCY_PROMPT(
commitLintConfig.consistency[
translation.localLanguage
] as ConsistencyPrompt
)
];
default:
// conventional-commit
return [
INIT_MAIN_PROMPT(translation.localLanguage),
INIT_DIFF_PROMPT,
INIT_CONSISTENCY_PROMPT(translation)
];
}
};

View File

@@ -1,7 +1,10 @@
import { getOpenCommitLatestVersion } from '../api';
import currentPackage from '../../package.json' assert { type: 'json' };
import chalk from 'chalk';
import { outro } from '@clack/prompts';
import currentPackage from '../../package.json';
import { getOpenCommitLatestVersion } from '../api';
export const checkIsLatestVersion = async () => {
const latestVersion = await getOpenCommitLatestVersion();
@@ -9,7 +12,7 @@ export const checkIsLatestVersion = async () => {
const currentVersion = currentPackage.version;
if (currentVersion !== latestVersion) {
console.warn(
outro(
chalk.yellow(
`
You are not using the latest stable version of OpenCommit with new features and bug fixes.

View File

@@ -1,8 +1,9 @@
import { execa } from 'execa';
import { outro, spinner } from '@clack/prompts';
import { readFileSync } from 'fs';
import ignore, { Ignore } from 'ignore';
import { outro, spinner } from '@clack/prompts';
export const assertGitRepo = async () => {
try {
await execa('git', ['rev-parse']);
@@ -25,13 +26,11 @@ export const getOpenCommitIgnore = (): Ignore => {
return ig;
};
export const getCoreHooksPath = async(): Promise<string> => {
const { stdout } = await execa('git', [
'config',
'core.hooksPath']);
export const getCoreHooksPath = async (): Promise<string> => {
const { stdout } = await execa('git', ['config', 'core.hooksPath']);
return stdout;
}
};
export const getStagedFiles = async (): Promise<string[]> => {
const { stdout: gitDir } = await execa('git', [
@@ -83,12 +82,20 @@ export const gitAdd = async ({ files }: { files: string[] }) => {
export const getDiff = async ({ files }: { files: string[] }) => {
const lockFiles = files.filter(
(file) => file.includes('.lock') || file.includes('-lock.')
(file) =>
file.includes('.lock') ||
file.includes('-lock.') ||
file.includes('.svg') ||
file.includes('.png') ||
file.includes('.jpg') ||
file.includes('.jpeg') ||
file.includes('.webp') ||
file.includes('.gif')
);
if (lockFiles.length) {
outro(
`Some files are '.lock' files which are excluded by default from 'git diff'. No commit messages are generated for this files:\n${lockFiles.join(
`Some files are excluded by default from 'git diff'. No commit messages are generated for this files:\n${lockFiles.join(
'\n'
)}`
);

View File

@@ -1,4 +1,5 @@
import { tokenCount } from './tokenCount';
export function mergeDiffs(arr: string[], maxStringLength: number): string[] {
const mergedArr: string[] = [];
let currentItem: string = arr[0];

View File

@@ -1,5 +1,5 @@
import cl100k_base from '@dqbd/tiktoken/encoders/cl100k_base.json';
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(