├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main ├── kotlin └── com │ └── github │ └── blarc │ └── ai │ └── commits │ └── intellij │ └── plugin │ ├── AICommitAction.kt │ ├── AICommitsBundle.kt │ ├── AICommitsExtensions.kt │ ├── Icons.kt │ ├── OpenAIService.kt │ ├── listeners │ └── ApplicationStartupListener.kt │ ├── notifications │ ├── Notification.kt │ └── Notifier.kt │ └── settings │ ├── AppSettings.kt │ ├── AppSettingsConfigurable.kt │ ├── AppSettingsListCellRenderer.kt │ └── prompt │ ├── Prompt.kt │ └── PromptTable.kt └── resources ├── META-INF ├── plugin.xml └── pluginIcon.svg ├── icons └── commit_gpt.svg └── messages └── MyBundle.properties /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: marcr2 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | target-branch: "main" 10 | schedule: 11 | interval: "weekly" 12 | # Maintain dependencies for GitHub Actions 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | target-branch: "main" 16 | schedule: 17 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests) 4 | push: 5 | branches: [main, dev_marc] 6 | # Trigger the workflow on any pull request 7 | pull_request: 8 | 9 | jobs: 10 | # Run Gradle Wrapper Validation Action to verify the wrapper's checksum 11 | # Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks 12 | # Build plugin and provide the artifact for the next workflow jobs 13 | build: 14 | name: Build 15 | runs-on: ubuntu-latest 16 | outputs: 17 | version: ${{ steps.properties.outputs.version }} 18 | changelog: ${{ steps.properties.outputs.changelog }} 19 | steps: 20 | 21 | # Free GitHub Actions Environment Disk Space 22 | - name: Maximize Build Space 23 | run: | 24 | sudo rm -rf /usr/share/dotnet 25 | sudo rm -rf /usr/local/lib/android 26 | sudo rm -rf /opt/ghc 27 | 28 | # Check out current repository 29 | - name: Fetch Sources 30 | uses: actions/checkout@v3.5.1 31 | 32 | # Validate wrapper 33 | - name: Gradle Wrapper Validation 34 | uses: gradle/wrapper-validation-action@v1.0.6 35 | 36 | # Setup Java 11 environment for the next steps 37 | - name: Setup Java 38 | uses: actions/setup-java@v3 39 | with: 40 | distribution: adopt 41 | java-version: 17 42 | cache: gradle 43 | 44 | # Set environment variables 45 | - name: Export Properties 46 | id: properties 47 | shell: bash 48 | run: | 49 | PROPERTIES="$(./gradlew properties --console=plain -q)" 50 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 51 | NAME="$(echo "$PROPERTIES" | grep "^pluginName:" | cut -f2- -d ' ')" 52 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 53 | 54 | echo "version=$VERSION" >> $GITHUB_OUTPUT 55 | echo "name=$NAME" >> $GITHUB_OUTPUT 56 | echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT 57 | 58 | echo "changelog<> $GITHUB_OUTPUT 59 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 60 | echo "EOF" >> $GITHUB_OUTPUT 61 | 62 | ./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier 63 | 64 | # Run tests 65 | # - name: Run Tests 66 | # run: ./gradlew test 67 | 68 | # Collect Tests Result of failed tests 69 | # - name: Collect Tests Result 70 | # if: ${{ failure() }} 71 | # uses: actions/upload-artifact@v3 72 | # with: 73 | # name: tests-result 74 | # path: ${{ github.workspace }}/build/reports/tests 75 | 76 | # Cache Plugin Verifier IDEs 77 | - name: Setup Plugin Verifier IDEs Cache 78 | uses: actions/cache@v3.3.1 79 | with: 80 | path: ${{ steps.properties.outputs.pluginVerifierHomeDir }}/ides 81 | key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} 82 | 83 | # Run Verify Plugin task and IntelliJ Plugin Verifier tool 84 | - name: Run Plugin Verification tasks 85 | run: ./gradlew runPluginVerifier -Pplugin.verifier.home.dir=${{ steps.properties.outputs.pluginVerifierHomeDir }} 86 | 87 | # Collect Plugin Verifier Result 88 | - name: Collect Plugin Verifier Result 89 | if: ${{ always() }} 90 | uses: actions/upload-artifact@v3 91 | with: 92 | name: pluginVerifier-result 93 | path: ${{ github.workspace }}/build/reports/pluginVerifier 94 | 95 | # Prepare plugin archive content for creating artifact 96 | - name: Prepare Plugin Artifact 97 | id: artifact 98 | shell: bash 99 | run: | 100 | cd ${{ github.workspace }}/build/distributions 101 | FILENAME=`ls *.zip` 102 | unzip "$FILENAME" -d content 103 | 104 | echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT 105 | 106 | # Store already-built plugin as an artifact for downloading 107 | - name: Upload artifact 108 | uses: actions/upload-artifact@v3 109 | with: 110 | name: ${{ steps.artifact.outputs.filename }} 111 | path: ./build/distributions/content/*/* 112 | 113 | # Prepare a draft release for GitHub Releases page for the manual verification 114 | # If accepted and published, release workflow would be triggered 115 | releaseDraft: 116 | name: Release Draft 117 | if: github.event_name != 'pull_request' 118 | needs: build 119 | runs-on: ubuntu-latest 120 | permissions: 121 | contents: write 122 | steps: 123 | 124 | # Check out current repository 125 | - name: Fetch Sources 126 | uses: actions/checkout@v3.5.1 127 | 128 | # Remove old release drafts by using the curl request for the available releases with draft flag 129 | - name: Remove Old Release Drafts 130 | env: 131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 132 | run: | 133 | gh api repos/{owner}/{repo}/releases \ 134 | --jq '.[] | select(.draft == true) | .id' \ 135 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 136 | 137 | # Create new release draft - which is not publicly visible and requires manual acceptance 138 | - name: Create Release Draft 139 | env: 140 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 141 | run: | 142 | gh release create v${{ needs.build.outputs.version }} \ 143 | --draft \ 144 | --title "v${{ needs.build.outputs.version }}" \ 145 | --notes "$(cat << 'EOM' 146 | ${{ needs.build.outputs.changelog }} 147 | EOM 148 | )" 149 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared 2 | # with the Build workflow. Running the publishPlugin task requires the PUBLISH_TOKEN secret provided. 3 | 4 | name: Release 5 | on: 6 | release: 7 | types: [prereleased, released] 8 | 9 | jobs: 10 | 11 | # Prepare and publish the plugin to the Marketplace repository 12 | release: 13 | name: Publish Plugin 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | steps: 19 | 20 | # Check out current repository 21 | - name: Fetch Sources 22 | uses: actions/checkout@v3.5.1 23 | with: 24 | ref: ${{ github.event.release.tag_name }} 25 | 26 | # Setup Java 14 environment for the next steps 27 | - name: Setup Java 28 | uses: actions/setup-java@v3 29 | with: 30 | distribution: adopt 31 | java-version: 17 32 | cache: gradle 33 | 34 | # Set environment variables 35 | - name: Export Properties 36 | id: properties 37 | shell: bash 38 | run: | 39 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 40 | ${{ github.event.release.body }} 41 | EOM 42 | )" 43 | 44 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 45 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 46 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 47 | 48 | echo "changelog=$CHANGELOG" >> $GITHUB_OUTPUT 49 | 50 | # Update Unreleased section with the current release note 51 | - name: Patch Changelog 52 | if: ${{ steps.properties.outputs.changelog != '' }} 53 | env: 54 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 55 | run: | 56 | ./gradlew patchChangelog --release-note="$CHANGELOG" 57 | 58 | # Publish the plugin to the Marketplace 59 | - name: Publish Plugin 60 | env: 61 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 62 | run: ./gradlew publishPlugin 63 | 64 | # Upload artifact as a release asset 65 | - name: Upload Release Asset 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 69 | 70 | # Create pull request 71 | - name: Create Pull Request 72 | if: ${{ steps.properties.outputs.changelog != '' }} 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | run: | 76 | VERSION="${{ github.event.release.tag_name }}" 77 | BRANCH="changelog-update-$VERSION" 78 | 79 | git config user.email "action@github.com" 80 | git config user.name "GitHub Action" 81 | 82 | git checkout -b $BRANCH 83 | git commit -am "chore: Changelog update - $VERSION" 84 | git push --set-upstream origin $BRANCH 85 | 86 | gh pr create \ 87 | --title "Changelog update - \`$VERSION\`" \ 88 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 89 | --base main \ 90 | --head $BRANCH 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store 43 | /.idea/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.7.0] - 2023-04-12 6 | 7 | ### Added 8 | - Open AI proxy setting. 9 | 10 | ## [0.6.2] - 2023-04-11 11 | 12 | ### Fixed 13 | - Locale is not used in prompt. 14 | 15 | ## [0.6.1] - 2023-04-10 16 | 17 | ### Fixed 18 | - Commit workflow handler can be null. 19 | 20 | ## [0.6.0] - 2023-04-08 21 | 22 | ### Added 23 | - Table for setting prompts. 24 | - Different prompts to choose from. 25 | - Bug report link to settings. 26 | - Add generate commit action progress indicator. 27 | 28 | ### Changed 29 | - Sort locales alphabetically. 30 | 31 | ### Fixed 32 | - Changing token does not work. 33 | 34 | ## [0.5.1] - 2023-04-05 35 | 36 | ### Fixed 37 | - Use prompt instead of diff when making request to Open AI API. 38 | 39 | ## [0.5.0] - 2023-04-04 40 | 41 | ### Added 42 | - Add button for verifying Open AI token in settings. 43 | - Check if prompt is too large for Open AI API. 44 | - Welcome and star notification. 45 | 46 | ### Changed 47 | - Set default Locale to English. 48 | - Target latest intellij version (2023.1). 49 | - Improve error handling. 50 | 51 | ### Fixed 52 | - Properly serialize Locale when saving settings. 53 | 54 | ## [0.4.0] - 2023-03-29 55 | 56 | ### Changed 57 | - Removed unused `org.jetbrains.plugins.yaml` platform plugin. 58 | 59 | ### Fix 60 | - Plugin should not have until build number. 61 | 62 | ## [0.3.0] - 2023-03-28 63 | 64 | ### Added 65 | - Show notification when diff is empty. 66 | - - This allows to compute diff only from files and **lines** selected in the commit dialog. 67 | 68 | ## [0.2.0] - 2023-03-27 69 | 70 | ### Added 71 | - Basic action for generating commit message. 72 | - Settings for locale and OpenAI token. 73 | - Create commit message only for selected files. 74 | 75 | [Unreleased]: https://github.com/Blarc/ai-commits-intellij-plugin/compare/v0.7.0...HEAD 76 | [0.7.0]: https://github.com/Blarc/ai-commits-intellij-plugin/compare/v0.6.2...v0.7.0 77 | [0.6.2]: https://github.com/Blarc/ai-commits-intellij-plugin/compare/v0.6.1...v0.6.2 78 | [0.6.1]: https://github.com/Blarc/ai-commits-intellij-plugin/compare/v0.6.0...v0.6.1 79 | [0.6.0]: https://github.com/Blarc/ai-commits-intellij-plugin/compare/v0.5.1...v0.6.0 80 | [0.5.1]: https://github.com/Blarc/ai-commits-intellij-plugin/compare/v0.5.0...v0.5.1 81 | [0.5.0]: https://github.com/Blarc/ai-commits-intellij-plugin/compare/v0.4.0...v0.5.0 82 | [0.4.0]: https://github.com/Blarc/ai-commits-intellij-plugin/compare/v0.3.0...v0.4.0 83 | [0.3.0]: https://github.com/Blarc/ai-commits-intellij-plugin/compare/v0.2.0...v0.3.0 84 | [0.2.0]: https://github.com/Blarc/ai-commits-intellij-plugin/commits/v0.2.0 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions and Issues are **welcome**. 4 | 5 | ## Pull Requests 6 | 7 | - Fork the repository and make changes on your fork in a new feature branch. 8 | 9 | - Make sure your fork is up-to-date with the latest changes from this repository. 10 | 11 | - Once you're satisfied with your feature, send a pull request with some details about what you've done. 12 | 13 | - Thanks for your contribution! 14 | 15 | We recommend to use this Plugin to generate the commit message for your contribution. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Jakob Maležič 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | logo 4 | 5 |
6 |

CommitGPT

7 |

CommitGPT for IntelliJ based IDEs/Android Studio.

8 | 9 |

10 | Build Status 11 | 12 | 13 | 14 |

15 |
16 | 17 | - [Description](#description) 18 | - [Features](#features) 19 | - [Hint for the AI](#hint-the-ai) 20 | - [Custom prompt](#custom-prompt) 21 | - [Compatibility](#compatibility) 22 | - [Install](#install) 23 | - [Installation from zip](#installation-from-zip) 24 | 25 | [//]: # (- [Demo](#demo)) 26 | 27 | ## Description 28 | CommitGPT is a plugin that generates your commit messages with ChatGPT. To get started, 29 | install the plugin and set OpenAI private token in plugin's settings: 30 | Settings > Tools > CommitGPT 31 | 32 | ## Features 33 | - Generate commit message from diff using OpenAI ChatGPT API 34 | - Compute diff only from the selected files and lines in the commit dialog 35 | - Create your own prompt for commit message generation 36 | - Choose your own base prompt 37 | - Include a hint in the prompt for the AI to generate a better commit message. 38 | 39 | ## Hint the AI 40 | 41 | You can provide a hint for the AI to generate a better commit message by 42 | writing a sentence in the commit dialog starting with a `!`. 43 | 44 | This hint will be included in the prompt for the AI. 45 | 46 | *Note:* Your custom prompt have to include `{hint}` at some point in order for the hint to be included. 47 | By default, this is the case. 48 | 49 | ## Custom prompt 50 | 51 | You can choose your own base prompt for the AI to generate the commit message from in the settings. 52 | 53 | Your custom prompt have to include `{diffs}`, otherwise the AI will not be able to generate a commit message based on 54 | your changes. 55 | The prompt can also include `{hint}` to include a hint in the prompt for the AI to generate a better commit message. 56 | 57 | *Note:* The custom prompt will not be saved in the settings if it does not include `{diffs}`. 58 | `{hint}` is optional. 59 | 60 | ## Compatibility 61 | IntelliJ IDEA, PhpStorm, WebStorm, PyCharm, RubyMine, AppCode, CLion, GoLand, DataGrip, Rider, MPS, Android Studio, 62 | DataSpell, Code With Me 63 | 64 | ## Install 65 | 66 | 67 | 68 | 69 | 70 | Or you could install it inside your IDE: 71 | 72 | - For Windows & Linux: File > Settings > Plugins > Marketplace > Search 73 | for "CommitGPT" > Install Plugin > Restart IntelliJ IDEA 74 | 75 | - For Mac: IntelliJ IDEA > Preferences > Plugins > Marketplace > Search 76 | for "CommitGPT" > Install Plugin > Restart IntelliJ IDEA 77 | 78 | Remember to set your OpenAI private token in plugin's settings: Settings > Tools > 79 | CommitGPT 80 | 81 | ### Installation from zip 82 | 1. Download zip from [releases](https://github.com/Marc-R2/ai-commits-intellij-plugin/releases) 83 | 2. Import to IntelliJ: Settings > Plugins > Cog > Install plugin from 84 | disk... 85 | 3. Set OpenAI private token in plugin's settings: Settings > Tools > CommitGPT 86 | 87 | ## Support 88 | 89 | * Star the repository 90 | * [Buy me a coffee](https://ko-fi.com/marcr2) 91 | * [Rate the plugin](https://plugins.jetbrains.com/plugin/21412-commitgpt) 92 | * [Share the plugin](https://plugins.jetbrains.com/plugin/21412-commitgpt) 93 | 94 | ## Change log 95 | 96 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 97 | This may not be complete, so please check 98 | the [commit history](https://github.com/Marc-R2/ai-commits-intellij-plugin/commits) 99 | as well. 100 | 101 | ## Contributing 102 | 103 | We welcome contributions of all kinds. 104 | 105 | If you find a bug, have a question or a feature request, please file an issue. 106 | 107 | If you'd like to contribute code, fork the repository, make your changes and feel free to submit a pull request. 108 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 109 | 110 | ## Acknowledgements 111 | 112 | - Fork from Blarc's [AI Commits](https://github.com/Blarc/ai-commits-intellij-plugin) 113 | - Originally inspired by Nutlope's [AICommits](https://github.com/Nutlope/aicommits). 114 | - [openai-kotlin](https://github.com/aallam/openai-kotlin) for OpenAI API client. 115 | 116 | ## License 117 | 118 | Please see [LICENSE](LICENSE) for details. 119 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.Changelog 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | fun properties(key: String) = project.findProperty(key).toString() 5 | 6 | plugins { 7 | id("org.jetbrains.kotlin.jvm") version "1.8.20" 8 | id("org.jetbrains.intellij") version "1.13.3" 9 | 10 | // Gradle Changelog Plugin 11 | id("org.jetbrains.changelog") version "2.0.0" 12 | } 13 | 14 | group = properties("pluginGroup") 15 | version = properties("pluginVersion") 16 | 17 | // Configure project's dependencies 18 | repositories { 19 | mavenCentral() 20 | } 21 | 22 | // Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin 23 | intellij { 24 | pluginName.set(properties("pluginName")) 25 | version.set(properties("platformVersion")) 26 | type.set(properties("platformType")) 27 | updateSinceUntilBuild.set(false) 28 | 29 | plugins.set( 30 | properties("platformPlugins").split(',') 31 | .map(String::trim) 32 | .filter(String::isNotEmpty) 33 | ) 34 | } 35 | 36 | changelog { 37 | // version.set(properties("pluginVersion")) 38 | groups.set(emptyList()) 39 | repositoryUrl.set(properties("pluginRepositoryUrl")) 40 | } 41 | 42 | tasks { 43 | // Set the JVM compatibility versions 44 | properties("javaVersion").let { 45 | withType { 46 | sourceCompatibility = it 47 | targetCompatibility = it 48 | } 49 | withType { 50 | kotlinOptions.jvmTarget = it 51 | } 52 | } 53 | 54 | wrapper { 55 | gradleVersion = properties("gradleVersion") 56 | } 57 | 58 | patchPluginXml { 59 | version.set(properties("pluginVersion")) 60 | sinceBuild.set(properties("pluginSinceBuild")) 61 | // untilBuild.set(properties("pluginUntilBuild")) 62 | 63 | // Get the latest available change notes from the changelog file 64 | changeNotes.set(provider { 65 | with(changelog) { 66 | renderItem( 67 | getOrNull(properties("pluginVersion")) ?: getUnreleased() 68 | .withHeader(false) 69 | .withEmptySections(false), 70 | Changelog.OutputType.HTML, 71 | ) 72 | } 73 | }) 74 | } 75 | 76 | signPlugin { 77 | certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) 78 | privateKey.set(System.getenv("PRIVATE_KEY")) 79 | password.set(System.getenv("PRIVATE_KEY_PASSWORD")) 80 | } 81 | 82 | publishPlugin { 83 | dependsOn("patchChangelog") 84 | token.set(System.getenv("PUBLISH_TOKEN")) 85 | channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first())) 86 | } 87 | 88 | buildPlugin { 89 | exclude { "coroutines" in it.name } 90 | } 91 | prepareSandbox { 92 | exclude { "coroutines" in it.name } 93 | } 94 | } 95 | 96 | tasks.withType().configureEach { 97 | kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" 98 | } 99 | 100 | dependencies { 101 | implementation("com.aallam.openai:openai-client:3.2.0") { 102 | exclude(group = "org.slf4j", module = "slf4j-api") 103 | // Prevents java.lang.LinkageError: java.lang.LinkageError: loader constraint violation:when resolving method 'long kotlin.time.Duration.toLong-impl(long, kotlin.time.DurationUnit)' 104 | exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") 105 | exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-common") 106 | exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7") 107 | exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8") 108 | } 109 | implementation("io.ktor:ktor-client-cio:2.2.4") { 110 | exclude(group = "org.slf4j", module = "slf4j-api") 111 | // Prevents java.lang.LinkageError: java.lang.LinkageError: loader constraint violation: when resolving method 'long kotlin.time.Duration.toLong-impl(long, kotlin.time.DurationUnit)' 112 | exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") 113 | exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-common") 114 | exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7") 115 | exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8") 116 | } 117 | 118 | implementation("com.knuddels:jtokkit:0.1.0") 119 | } 120 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | 2 | pluginGroup = com.github.marc-r2 3 | pluginName = CommitGPT 4 | pluginRepositoryUrl = https://github.com/Marc-R2/ai-commits-intellij-plugin 5 | # SemVer format -> https://semver.org 6 | pluginVersion = 0.7.2 7 | 8 | # https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 9 | pluginSinceBuild = 231 10 | # pluginUntilBuild = 222.* 11 | 12 | # https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties 13 | platformType = IC 14 | platformVersion = 2023.1 15 | 16 | # https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 17 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 18 | platformPlugins = org.jetbrains.kotlin, org.jetbrains.plugins.github, Git4Idea 19 | 20 | # If targeting 2022.3+, Java 17 is required. 21 | javaVersion = 17 22 | 23 | # https://github.com/gradle/gradle/releases 24 | gradleVersion = 8.0.2 25 | 26 | # Opt-out flag for bundling Kotlin standard library. 27 | # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. 28 | # suppress inspection "UnusedProperty" 29 | kotlin.stdlib.default.dependency = false 30 | 31 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 32 | org.gradle.unsafe.configuration-cache = true 33 | 34 | # Prevent OOM: https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#incremental-compilation 35 | kotlin.incremental.useClasspathSnapshot=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-R2/ai-commits-intellij-plugin/8eeb093179dd9bbfb8a8403e77f96336bc341df3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ai-commits-intellij-plugin" -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitAction.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin 2 | 3 | import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message 4 | import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification 5 | import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification 6 | import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.diff.impl.patch.IdeaTextPatchBuilder 10 | import com.intellij.openapi.diff.impl.patch.UnifiedDiffWriter 11 | import com.intellij.openapi.progress.runBackgroundableTask 12 | import com.intellij.openapi.project.DumbAware 13 | import com.intellij.openapi.project.Project 14 | import com.intellij.openapi.vcs.VcsDataKeys 15 | import com.intellij.openapi.vcs.changes.Change 16 | import com.intellij.openapi.vcs.ui.CommitMessage 17 | import com.intellij.vcs.commit.AbstractCommitWorkflowHandler 18 | import com.knuddels.jtokkit.Encodings 19 | import com.knuddels.jtokkit.api.EncodingType 20 | import git4idea.repo.GitRepositoryManager 21 | import kotlinx.coroutines.DelicateCoroutinesApi 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.runBlocking 24 | import java.io.StringWriter 25 | 26 | class AICommitAction : AnAction(), DumbAware { 27 | override fun actionPerformed(e: AnActionEvent) { 28 | val project = e.project ?: return 29 | 30 | val commitWorkflowHandler = e.getData(VcsDataKeys.COMMIT_WORKFLOW_HANDLER) as AbstractCommitWorkflowHandler<*, *>? 31 | if (commitWorkflowHandler == null) { 32 | sendNotification(Notification.noCommitMessage()) 33 | return 34 | } 35 | 36 | val includedChanges = commitWorkflowHandler.ui.getIncludedChanges() 37 | val commitMessageField = VcsDataKeys.COMMIT_MESSAGE_CONTROL.getData(e.dataContext) 38 | 39 | runBackgroundableTask(message("action.background"), project) { 40 | val diff = computeDiff(includedChanges, project) 41 | 42 | val hintMessageField = VcsDataKeys.COMMIT_MESSAGE_CONTROL.getData(e.dataContext) as CommitMessage 43 | val currentCommitMessage = hintMessageField.text 44 | 45 | // if currentCommitMessage starts with : then it is a hint 46 | val hint = if (currentCommitMessage.startsWith("!")) { 47 | currentCommitMessage.substring(1) 48 | } else { 49 | null 50 | } 51 | 52 | if (diff.isBlank()) { 53 | sendNotification(Notification.emptyDiff()) 54 | return@runBackgroundableTask 55 | } 56 | 57 | if (commitMessageField == null) { 58 | sendNotification(Notification.noCommitMessageField()) 59 | return@runBackgroundableTask 60 | } 61 | 62 | val openAIService = OpenAIService.instance 63 | runBlocking(Dispatchers.Main) { 64 | commitMessageField.setCommitMessage("Generating commit message...") 65 | try { 66 | val generatedCommitMessage = openAIService.generateCommitMessage(diff, hint, 1) 67 | commitMessageField.setCommitMessage(generatedCommitMessage) 68 | AppSettings.instance.recordHit() 69 | } catch (e: Exception) { 70 | commitMessageField.setCommitMessage("Error generating commit message") 71 | sendNotification(Notification.unsuccessfulRequest(e.message ?: "Unknown error")) 72 | } 73 | } 74 | } 75 | } 76 | 77 | private fun computeDiff( 78 | includedChanges: List, 79 | project: Project 80 | ): String { 81 | 82 | val gitRepositoryManager = GitRepositoryManager.getInstance(project) 83 | 84 | // go through included changes, create a map of repository to changes and discard nulls 85 | val changesByRepository = includedChanges 86 | .mapNotNull { change -> 87 | change.virtualFile?.let { file -> 88 | gitRepositoryManager.getRepositoryForFileQuick( 89 | file 90 | ) to change 91 | } 92 | } 93 | .groupBy({ it.first }, { it.second }) 94 | 95 | 96 | // compute diff for each repository 97 | return changesByRepository 98 | .map { (repository, changes) -> 99 | repository?.let { 100 | val filePatches = IdeaTextPatchBuilder.buildPatch( 101 | project, 102 | changes, 103 | repository.root.toNioPath(), false, true 104 | ) 105 | 106 | val stringWriter = StringWriter() 107 | stringWriter.write("Repository: ${repository.root.path}\n") 108 | UnifiedDiffWriter.write(project, filePatches, stringWriter, "\n", null) 109 | stringWriter.toString() 110 | } 111 | } 112 | .joinToString("\n") 113 | } 114 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitsBundle.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin 2 | 3 | import com.intellij.DynamicBundle 4 | import com.intellij.ide.browsers.BrowserLauncher 5 | import com.intellij.ide.plugins.PluginManagerCore 6 | import com.intellij.openapi.extensions.PluginId 7 | import com.intellij.openapi.options.ShowSettingsUtil 8 | import com.intellij.openapi.project.Project 9 | import org.jetbrains.annotations.NonNls 10 | import org.jetbrains.annotations.PropertyKey 11 | import java.net.URL 12 | 13 | @NonNls 14 | private const val BUNDLE = "messages.MyBundle" 15 | 16 | object AICommitsBundle : DynamicBundle(BUNDLE) { 17 | 18 | public val URL_BUG_REPORT = URL("https://github.com/Marc-R2/ai-commits-intellij-plugin/issues") 19 | 20 | @Suppress("SpreadOperator") 21 | @JvmStatic 22 | fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 23 | getMessage(key, *params) 24 | 25 | @Suppress("SpreadOperator", "unused") 26 | @JvmStatic 27 | fun messagePointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 28 | getLazyMessage(key, *params) 29 | 30 | fun openPluginSettings(project: Project) { 31 | ShowSettingsUtil.getInstance().showSettingsDialog(project, message("settings.general.group.title")) 32 | } 33 | 34 | fun openRepository() { 35 | BrowserLauncher.instance.open("https://github.com/Marc-R2/ai-commits-intellij-plugin"); 36 | } 37 | 38 | fun plugin() = PluginManagerCore.getPlugin(PluginId.getId("com.github.marc-r2.commit_gpt-intellij-plugin")) 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitsExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin 2 | 3 | import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message 4 | import com.intellij.openapi.ui.ValidationInfo 5 | import com.intellij.ui.layout.ValidationInfoBuilder 6 | import com.intellij.util.ui.ColumnInfo 7 | 8 | fun createColumn(name: String, formatter: (T) -> String) : ColumnInfo { 9 | return object : ColumnInfo(name) { 10 | override fun valueOf(item: T): String { 11 | return formatter(item) 12 | } 13 | } 14 | } 15 | 16 | fun ValidationInfoBuilder.notBlank(value: String): ValidationInfo? = 17 | if (value.isBlank()) error(message("validation.required")) else null 18 | 19 | fun ValidationInfoBuilder.unique(value: String, existingValues: Set): ValidationInfo? = 20 | if (existingValues.contains(value)) error(message("validation.unique")) else null 21 | 22 | fun ValidationInfoBuilder.isLong(value: String): ValidationInfo? { 23 | if (value.isBlank()){ 24 | return null 25 | } 26 | 27 | value.toLongOrNull().let { 28 | if (it == null) { 29 | return error(message("validation.number")) 30 | } else { 31 | return null 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/Icons.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin 2 | 3 | import com.intellij.openapi.util.IconLoader 4 | 5 | object Icons { 6 | val AI_COMMITS = IconLoader.getIcon("/icons/commit_gpt.svg", javaClass) 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/OpenAIService.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin 2 | 3 | import com.aallam.openai.api.BetaOpenAI 4 | import com.aallam.openai.api.chat.* 5 | import com.aallam.openai.api.model.ModelId 6 | import com.aallam.openai.client.OpenAI 7 | import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification 8 | import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification 9 | import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings 10 | import com.intellij.openapi.application.ApplicationManager 11 | import com.intellij.openapi.components.Service 12 | 13 | import com.knuddels.jtokkit.Encodings 14 | import com.knuddels.jtokkit.api.EncodingType 15 | 16 | @Service 17 | class OpenAIService { 18 | 19 | companion object { 20 | const val model = "gpt-4o" 21 | val instance: OpenAIService 22 | get() = ApplicationManager.getApplication().getService(OpenAIService::class.java) 23 | } 24 | 25 | @OptIn(BetaOpenAI::class) 26 | suspend fun generateCommitMessage(diff: String, hint: String?, completions: Int): String { 27 | val openAiToken = AppSettings.instance.getOpenAIToken() ?: throw Exception("OpenAI Token is not set") 28 | 29 | val openAI = OpenAI(openAiToken) 30 | 31 | val prompt = AppSettings.instance.getPrompt(diff, hint) 32 | 33 | if (isPromptTooLarge(prompt)) { 34 | sendNotification(Notification.promptTooLarge()) 35 | return "Prompt is too large" 36 | } 37 | 38 | sendNotification(Notification.usedPrompt(prompt)) 39 | 40 | val chatCompletionRequest = ChatCompletionRequest( 41 | ModelId(model), 42 | listOf( 43 | ChatMessage( 44 | role = ChatRole.User, 45 | content = prompt 46 | ) 47 | ), 48 | temperature = 0.7, 49 | topP = 1.0, 50 | frequencyPenalty = 0.0, 51 | presencePenalty = 0.0, 52 | maxTokens = 2048, 53 | n = completions 54 | ) 55 | 56 | val completion: ChatCompletion = openAI.chatCompletion(chatCompletionRequest) 57 | return completion.choices[0].message!!.content 58 | } 59 | @Throws(Exception::class) 60 | suspend fun verifyToken(token: String){ 61 | OpenAI(token).models() 62 | } 63 | 64 | private fun isPromptTooLarge(prompt: String): Boolean { 65 | val registry = Encodings.newDefaultEncodingRegistry() 66 | val encoding = registry.getEncoding(EncodingType.CL100K_BASE) 67 | return encoding.countTokens(prompt) > 4000 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/listeners/ApplicationStartupListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin.listeners 2 | 3 | import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle 4 | import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification 5 | import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification 6 | import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.startup.ProjectActivity 9 | 10 | class ApplicationStartupListener : ProjectActivity { 11 | 12 | private var firstTime = true 13 | override suspend fun execute(project: Project) { 14 | showVersionNotification(project) 15 | } 16 | private fun showVersionNotification(project: Project) { 17 | val settings = AppSettings.instance 18 | val version = AICommitsBundle.plugin()?.version 19 | 20 | if (version == settings.lastVersion) return 21 | 22 | settings.lastVersion = version 23 | if (firstTime && version != null) { 24 | sendNotification(Notification.welcome(version), project) 25 | } 26 | firstTime = false 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/notifications/Notification.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin.notifications 2 | 3 | import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle 4 | import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message 5 | import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.openPluginSettings 6 | import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings 7 | import com.intellij.ide.browsers.BrowserLauncher 8 | import com.intellij.openapi.components.service 9 | import com.intellij.openapi.project.Project 10 | import java.net.URI 11 | 12 | data class Notification( 13 | val title: String? = null, 14 | val message: String, 15 | val actions: Set = setOf(), 16 | val type: Type = Type.TRANSIENT 17 | ) { 18 | enum class Type { 19 | PERSISTENT, 20 | TRANSIENT 21 | } 22 | 23 | companion object { 24 | private val DEFAULT_TITLE = message("notifications.title") 25 | 26 | fun welcome(version: String) = Notification(message = message("notifications.welcome", version), type = Type.TRANSIENT) 27 | 28 | fun star() = Notification( 29 | message = """ 30 | Finding CommitGPT useful? Show your support 💖 and ⭐ the repository 🙏. 31 | """.trimIndent(), 32 | actions = setOf( 33 | NotificationAction.openRepository { 34 | service().requestSupport = false 35 | }, 36 | NotificationAction.doNotAskAgain { 37 | service().requestSupport = false 38 | } 39 | ) 40 | ) 41 | 42 | fun noCommitMessageField() = Notification(DEFAULT_TITLE, message = message("notifications.no-field")) 43 | 44 | fun emptyDiff() = Notification(DEFAULT_TITLE, message = message("notifications.empty-diff")) 45 | 46 | fun promptTooLarge() = Notification(DEFAULT_TITLE, message = message("notifications.prompt-too-large")) 47 | 48 | fun unsuccessfulRequest(message: String) = Notification( 49 | message = message("notifications.unsuccessful-request", message) 50 | ) 51 | 52 | fun noCommitMessage(): Notification = Notification(message = message("notifications.no-commit-message")) 53 | 54 | fun unableToSaveToken() = Notification(message = message("notifications.unable-to-save-token")) 55 | 56 | fun usedPrompt(diff: String) = Notification ( 57 | message = message("notifications.uses-prompt", diff) 58 | ) 59 | } 60 | 61 | fun isTransient() = type == Type.TRANSIENT 62 | 63 | fun isPersistent() = !isTransient() 64 | } 65 | 66 | data class NotificationAction(val title: String, val run: (dismiss: () -> Unit) -> Unit) { 67 | companion object { 68 | fun settings(project: Project, title: String = message("settings.title")) = NotificationAction(title) { dismiss -> 69 | dismiss() 70 | openPluginSettings(project) 71 | } 72 | 73 | fun openRepository(onComplete: () -> Unit) = NotificationAction(message("actions.sure-take-me-there")) { dismiss -> 74 | AICommitsBundle.openRepository() 75 | dismiss() 76 | onComplete() 77 | } 78 | 79 | fun doNotAskAgain(onComplete: () -> Unit) = NotificationAction(message("actions.do-not-ask-again")) { dismiss -> 80 | dismiss() 81 | onComplete() 82 | } 83 | 84 | fun openUrl(url: URI, title: String = message("actions.take-me-there")) = NotificationAction(title) { dismiss -> 85 | dismiss() 86 | BrowserLauncher.instance.open(url.toString()) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/notifications/Notifier.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin.notifications 2 | 3 | import com.github.blarc.ai.commits.intellij.plugin.Icons 4 | import com.intellij.notification.NotificationGroupManager 5 | import com.intellij.notification.NotificationType 6 | import com.intellij.openapi.project.DumbAwareAction 7 | import com.intellij.openapi.project.Project 8 | 9 | private const val IMPORTANT_GROUP_ID = "ai.commits.notification.important" 10 | private const val GENERAL_GROUP_ID = "ai.commits.notification.general" 11 | 12 | fun sendNotification(notification : Notification, project : Project? = null) { 13 | val groupId = when(notification.type) { 14 | Notification.Type.PERSISTENT -> IMPORTANT_GROUP_ID 15 | Notification.Type.TRANSIENT -> GENERAL_GROUP_ID 16 | } 17 | 18 | val notificationManager = NotificationGroupManager 19 | .getInstance() 20 | .getNotificationGroup(groupId) 21 | 22 | val intellijNotification = notificationManager.createNotification( 23 | notification.title ?: "", 24 | notification.message, 25 | NotificationType.INFORMATION 26 | ) 27 | 28 | intellijNotification.icon = Icons.AI_COMMITS 29 | 30 | notification.actions.forEach { action -> 31 | intellijNotification.addAction(DumbAwareAction.create(action.title) { 32 | action.run() { 33 | intellijNotification.expire() 34 | } 35 | }) 36 | } 37 | 38 | intellijNotification.notify(project) 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin.settings 2 | 3 | import com.aallam.openai.client.OpenAIConfig 4 | import com.aallam.openai.client.ProxyConfig 5 | import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification 6 | import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification 7 | import com.github.blarc.ai.commits.intellij.plugin.settings.prompt.Prompt 8 | import com.intellij.credentialStore.CredentialAttributes 9 | import com.intellij.credentialStore.Credentials 10 | import com.intellij.ide.passwordSafe.PasswordSafe 11 | import com.intellij.openapi.application.ApplicationManager 12 | import com.intellij.openapi.components.PersistentStateComponent 13 | import com.intellij.openapi.components.State 14 | import com.intellij.openapi.components.Storage 15 | import com.intellij.util.xmlb.Converter 16 | import com.intellij.util.xmlb.XmlSerializerUtil 17 | import com.intellij.util.xmlb.annotations.OptionTag 18 | import java.util.* 19 | 20 | @State( 21 | name = AppSettings.SERVICE_NAME, 22 | storages = [Storage("CommitGPT.xml")] 23 | ) 24 | class AppSettings : PersistentStateComponent { 25 | 26 | private val openAITokenTitle = "OpenAIToken" 27 | 28 | private var hits = 0 29 | 30 | @OptionTag(converter = LocaleConverter::class) 31 | var locale: Locale = Locale.ENGLISH 32 | var requestSupport = true 33 | var lastVersion: String? = null 34 | var proxyUrl: String? = null 35 | 36 | var prompts: MutableMap = initPrompts() 37 | var currentPrompt: Prompt = prompts["basic"]!! 38 | 39 | companion object { 40 | const val SERVICE_NAME = "com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings" 41 | val instance: AppSettings 42 | get() = ApplicationManager.getApplication().getService(AppSettings::class.java) 43 | } 44 | 45 | fun getPrompt(diff: String, hint: String?): String { 46 | var content = currentPrompt.content 47 | content = content.replace("{locale}", locale.displayName) 48 | 49 | val hintReplacement = hint ?: "No hint provided - Ignore this" 50 | content = content.replace("{hint}", hintReplacement) 51 | 52 | return if (content.contains("{diff}")) { 53 | content.replace("{diff}", diff) 54 | } else { 55 | "$content\n$diff" 56 | } 57 | } 58 | 59 | fun saveOpenAIToken(token: String) { 60 | try { 61 | PasswordSafe.instance.setPassword(getCredentialAttributes(openAITokenTitle), token) 62 | } catch (e: Exception) { 63 | sendNotification(Notification.unableToSaveToken()) 64 | } 65 | } 66 | 67 | fun getOpenAIConfig(): OpenAIConfig { 68 | val token = getOpenAIToken() ?: throw Exception("OpenAI Token is not set.") 69 | return OpenAIConfig(token, proxy = proxyUrl?.takeIf { it.isNotBlank() }?.let { ProxyConfig.Http(it) }) 70 | } 71 | 72 | fun getOpenAIToken(): String? { 73 | val credentialAttributes = getCredentialAttributes(openAITokenTitle) 74 | val credentials: Credentials = PasswordSafe.instance.get(credentialAttributes) ?: return null 75 | return credentials.getPasswordAsString() 76 | } 77 | 78 | private fun getCredentialAttributes(title: String): CredentialAttributes { 79 | return CredentialAttributes( 80 | title, 81 | null, 82 | this.javaClass, 83 | false 84 | ) 85 | } 86 | 87 | override fun getState() = this 88 | 89 | override fun loadState(state: AppSettings) { 90 | XmlSerializerUtil.copyBean(state, this) 91 | } 92 | 93 | fun recordHit() { 94 | hits++ 95 | if (requestSupport && (hits == 50 || hits % 100 == 0)) { 96 | sendNotification(Notification.star()) 97 | } 98 | } 99 | 100 | private fun initPrompts() = mutableMapOf( 101 | // Generate UUIDs for game objects in Mine.py and call the function in start_game(). 102 | "basic" to Prompt( 103 | "Basic", 104 | "Basic prompt that generates a decent commit message.", 105 | "Write an insightful but concise Git commit message in a complete sentence in present tense for the " + 106 | "following diff without prefacing it with anything, the response must be in the language {locale} and must " + 107 | "NOT be longer than 74 characters. The sent text will be the differences between files, where deleted lines" + 108 | " are prefixed with a single minus sign and added lines are prefixed with a single plus sign.\n" + 109 | "{diff}", 110 | false 111 | ), 112 | // feat: generate unique UUIDs for game objects on Mine game start 113 | "conventional" to Prompt( 114 | "Conventional", 115 | "Prompt for commit message in the conventional commit convention.", 116 | "Write a clean and comprehensive commit message in the conventional commit convention. " + 117 | "I'll send you an output of 'git diff --staged' command, and you convert " + 118 | "it into a commit message. " + 119 | "Do NOT preface the commit with anything. " + 120 | "Do NOT add any descriptions to the commit, only commit message. " + 121 | "Use the present tense. " + 122 | "Lines must not be longer than 74 characters. " + 123 | "Use {locale} language to answer.\n" + 124 | "{diff}", 125 | false 126 | ), 127 | // ✨ feat(mine): Generate objects UUIDs and start team timers on game start 128 | "emoji" to Prompt( 129 | "Emoji", 130 | "Prompt for commit message in the conventional commit convention with GitMoji convention.", 131 | "Write a clean and comprehensive commit message in the conventional commit convention. " + 132 | "I'll send you an output of 'git diff --staged' command, and you convert " + 133 | "it into a commit message. " + 134 | "Use GitMoji convention to preface the commit. " + 135 | "Do NOT add any descriptions to the commit, only commit message. " + 136 | "Use the present tense. " + 137 | "Lines must not be longer than 74 characters. " + 138 | "Use {locale} language to answer.\n" + 139 | "{diff}", 140 | false 141 | ) 142 | ) 143 | 144 | class LocaleConverter : Converter() { 145 | override fun toString(value: Locale): String? { 146 | return value.toLanguageTag() 147 | } 148 | 149 | override fun fromString(value: String): Locale? { 150 | return Locale.forLanguageTag(value) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettingsConfigurable.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin.settings 2 | 3 | import com.aallam.openai.api.exception.OpenAIAPIException 4 | import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle 5 | import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message 6 | import com.github.blarc.ai.commits.intellij.plugin.OpenAIService 7 | import com.github.blarc.ai.commits.intellij.plugin.settings.prompt.Prompt 8 | import com.github.blarc.ai.commits.intellij.plugin.settings.prompt.PromptTable 9 | import com.intellij.icons.AllIcons 10 | import com.intellij.openapi.actionSystem.AnAction 11 | import com.intellij.openapi.actionSystem.AnActionEvent 12 | import com.intellij.openapi.options.BoundConfigurable 13 | import com.intellij.openapi.progress.runBackgroundableTask 14 | import com.intellij.openapi.ui.ComboBox 15 | import com.intellij.ui.CommonActionsPanel 16 | import com.intellij.ui.ToolbarDecorator 17 | import com.intellij.ui.components.JBLabel 18 | import com.intellij.ui.dsl.builder.* 19 | import com.intellij.ui.util.minimumWidth 20 | import com.intellij.ui.util.preferredWidth 21 | import kotlinx.coroutines.DelicateCoroutinesApi 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.GlobalScope 24 | import kotlinx.coroutines.launch 25 | import java.util.* 26 | import javax.swing.JComponent 27 | import javax.swing.JPasswordField 28 | import javax.swing.JScrollPane 29 | 30 | class AppSettingsConfigurable : BoundConfigurable(message("settings.general.group.title")) { 31 | 32 | private val tokenPasswordField = JPasswordField() 33 | private val verifyLabel = JBLabel() 34 | private val promptTable = PromptTable() 35 | private lateinit var toolbarDecorator: ToolbarDecorator 36 | private lateinit var promptComboBox: Cell> 37 | 38 | override fun createPanel() = panel { 39 | 40 | group(JBLabel("OpenAI")) { 41 | // Set OpenAI token 42 | row { 43 | cell(tokenPasswordField) 44 | .label(message("settings.openAIToken")) 45 | .bindText( 46 | { AppSettings.instance.getOpenAIToken().orEmpty() }, 47 | { AppSettings.instance.saveOpenAIToken(it) } 48 | ) 49 | .align(Align.FILL) 50 | .resizableColumn() 51 | .focused() 52 | button(message("settings.verifyToken")) { 53 | verifyToken() 54 | }.align(AlignX.RIGHT) 55 | } 56 | 57 | // Link to OpenAI API page 58 | row { 59 | comment(message("settings.openAITokenComment")) 60 | .align(AlignX.LEFT) 61 | cell(verifyLabel) 62 | .align(AlignX.RIGHT) 63 | } 64 | 65 | // Set proxy 66 | row { 67 | textField() 68 | .label(message("settings.openAIProxy")) 69 | .bindText(AppSettings.instance::proxyUrl.toNonNullableProperty("")) 70 | .resizableColumn() 71 | .applyToComponent { minimumWidth = 300 } 72 | } 73 | } 74 | 75 | group(JBLabel("Prompt")) { 76 | // Set locale 77 | row { 78 | comboBox( 79 | Locale.getAvailableLocales().toList().sortedBy { it.displayName }, 80 | AppSettingsListCellRenderer() 81 | ) 82 | .label(message("settings.locale")) 83 | .bindItem(AppSettings.instance::locale.toNullableProperty()) 84 | } 85 | 86 | /// Current Prompt selection 87 | row { 88 | promptComboBox = comboBox(AppSettings.instance.prompts.values, AppSettingsListCellRenderer()) 89 | .label(message("settings.prompt")) 90 | .bindItem(AppSettings.instance::currentPrompt.toNullableProperty()) 91 | } 92 | 93 | // Prompt table 94 | row { 95 | toolbarDecorator = ToolbarDecorator.createDecorator(promptTable.table) 96 | .setAddAction { 97 | promptTable.addPrompt().let { 98 | promptComboBox.component.addItem(it) 99 | } 100 | } 101 | .setEditAction { 102 | promptTable.editPrompt()?.let { 103 | promptComboBox.component.removeItem(it.first) 104 | promptComboBox.component.addItem(it.second) 105 | } 106 | } 107 | .setEditActionUpdater { 108 | updateActionAvailability(CommonActionsPanel.Buttons.EDIT) 109 | true 110 | } 111 | .setRemoveAction { 112 | promptTable.removePrompt()?.let { 113 | promptComboBox.component.removeItem(it) 114 | } 115 | } 116 | .setRemoveActionUpdater { 117 | updateActionAvailability(CommonActionsPanel.Buttons.REMOVE) 118 | true 119 | } 120 | .disableUpDownActions() 121 | 122 | cell(toolbarDecorator.createPanel()) 123 | .align(Align.FILL) 124 | }.resizableRow() 125 | }.resizableRow() 126 | 127 | // Report Bug 128 | row { 129 | browserLink(message("settings.report-bug"), AICommitsBundle.URL_BUG_REPORT.toString()) 130 | } 131 | } 132 | 133 | private fun updateActionAvailability(action: CommonActionsPanel.Buttons) { 134 | val selectedRow = promptTable.table.selectedRow 135 | val selectedPrompt = promptTable.table.items[selectedRow] 136 | toolbarDecorator.actionsPanel.setEnabled(action, selectedPrompt.canBeChanged) 137 | } 138 | 139 | override fun isModified(): Boolean { 140 | return super.isModified() || promptTable.isModified() 141 | } 142 | 143 | override fun apply() { 144 | promptTable.apply() 145 | super.apply() 146 | } 147 | 148 | override fun reset() { 149 | promptTable.reset() 150 | super.reset() 151 | } 152 | 153 | @OptIn(DelicateCoroutinesApi::class) 154 | private fun verifyToken() { 155 | runBackgroundableTask(message("settings.verify.running")) { 156 | if (tokenPasswordField.password.isEmpty()) { 157 | verifyLabel.icon = AllIcons.General.InspectionsError 158 | verifyLabel.text = message("settings.verify.token-is-empty") 159 | } else { 160 | verifyLabel.icon = AllIcons.General.InlineRefreshHover 161 | verifyLabel.text = message("settings.verify.running") 162 | 163 | GlobalScope.launch(Dispatchers.IO) { 164 | try { 165 | OpenAIService.instance.verifyToken(String(tokenPasswordField.password)) 166 | verifyLabel.text = message("settings.verify.valid") 167 | verifyLabel.icon = AllIcons.General.InspectionsOK 168 | } catch (e: OpenAIAPIException) { 169 | verifyLabel.text = message("settings.verify.invalid", e.statusCode) 170 | verifyLabel.icon = AllIcons.General.InspectionsError 171 | } catch (e: Exception) { 172 | verifyLabel.text = message("settings.verify.invalid", "Unknown") 173 | verifyLabel.icon = AllIcons.General.InspectionsError 174 | } 175 | } 176 | } 177 | } 178 | 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettingsListCellRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin.settings 2 | 3 | import ai.grazie.utils.capitalize 4 | import com.github.blarc.ai.commits.intellij.plugin.settings.prompt.Prompt 5 | import java.awt.Component 6 | import java.util.* 7 | import javax.swing.DefaultListCellRenderer 8 | import javax.swing.JList 9 | 10 | class AppSettingsListCellRenderer : DefaultListCellRenderer() { 11 | override fun getListCellRendererComponent( 12 | list: JList<*>?, 13 | value: Any?, 14 | index: Int, 15 | isSelected: Boolean, 16 | cellHasFocus: Boolean 17 | ): Component { 18 | val component = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus) 19 | if (value is Locale) text = value.displayName 20 | if (value is Prompt) text = value.name 21 | return component 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/prompt/Prompt.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin.settings.prompt 2 | 3 | data class Prompt( 4 | var name: String = "", 5 | var description: String = "", 6 | var content: String = "", 7 | var canBeChanged: Boolean = true, 8 | ) 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/prompt/PromptTable.kt: -------------------------------------------------------------------------------- 1 | package com.github.blarc.ai.commits.intellij.plugin.settings.prompt 2 | 3 | import ai.grazie.utils.applyIf 4 | import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message 5 | import com.github.blarc.ai.commits.intellij.plugin.createColumn 6 | import com.github.blarc.ai.commits.intellij.plugin.notBlank 7 | import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings 8 | import com.github.blarc.ai.commits.intellij.plugin.unique 9 | import com.intellij.openapi.ui.DialogWrapper 10 | import com.intellij.ui.components.JBTextArea 11 | import com.intellij.ui.components.JBTextField 12 | import com.intellij.ui.dsl.builder.Align 13 | import com.intellij.ui.dsl.builder.bindText 14 | import com.intellij.ui.dsl.builder.panel 15 | import com.intellij.ui.table.TableView 16 | import com.intellij.util.ui.ListTableModel 17 | import java.awt.event.MouseAdapter 18 | import java.awt.event.MouseEvent 19 | import javax.swing.ListSelectionModel.SINGLE_SELECTION 20 | 21 | class PromptTable { 22 | private var prompts = AppSettings.instance.prompts 23 | private val tableModel = createTableModel() 24 | 25 | val table = TableView(tableModel).apply { 26 | setShowColumns(true) 27 | setSelectionMode(SINGLE_SELECTION) 28 | 29 | columnModel.getColumn(0).preferredWidth = 150 30 | columnModel.getColumn(0).maxWidth = 250 31 | 32 | addMouseListener(object : MouseAdapter() { 33 | override fun mouseClicked(e: MouseEvent?) { 34 | if (e?.clickCount == 2) { 35 | editPrompt() 36 | } 37 | } 38 | }) 39 | } 40 | 41 | private fun createTableModel(): ListTableModel = ListTableModel( 42 | arrayOf( 43 | createColumn(message("settings.prompt.name")) { prompt -> prompt.name }, 44 | createColumn(message("settings.prompt.description")) { prompt -> prompt.description }, 45 | ), 46 | prompts.values.toList() 47 | ) 48 | 49 | fun addPrompt(): Prompt? { 50 | val dialog = PromptDialog(prompts.keys.toSet()) 51 | 52 | if (dialog.showAndGet()) { 53 | prompts = prompts.plus(dialog.prompt.name.lowercase() to dialog.prompt).toMutableMap() 54 | refreshTableModel() 55 | return dialog.prompt 56 | } 57 | return null 58 | } 59 | 60 | fun removePrompt(): Prompt? { 61 | val selectedPrompt = table.selectedObject ?: return null 62 | prompts = prompts.minus(selectedPrompt.name.lowercase()).toMutableMap() 63 | refreshTableModel() 64 | return selectedPrompt 65 | } 66 | 67 | fun editPrompt(): Pair? { 68 | val selectedPrompt = table.selectedObject ?: return null 69 | val dialog = PromptDialog(prompts.keys.toSet(), selectedPrompt.copy()) 70 | 71 | if (dialog.showAndGet()) { 72 | prompts = prompts.minus(selectedPrompt.name.lowercase()).toMutableMap() 73 | prompts[dialog.prompt.name.lowercase()] = dialog.prompt 74 | refreshTableModel() 75 | return selectedPrompt to dialog.prompt 76 | } 77 | return null 78 | } 79 | 80 | private fun refreshTableModel() { 81 | tableModel.items = prompts.values.toList() 82 | } 83 | 84 | fun reset() { 85 | prompts = AppSettings.instance.prompts 86 | refreshTableModel() 87 | } 88 | 89 | fun isModified() = prompts != AppSettings.instance.prompts 90 | 91 | fun apply() { 92 | AppSettings.instance.prompts = prompts 93 | } 94 | 95 | private class PromptDialog(val prompts: Set, val newPrompt: Prompt? = null) : DialogWrapper(true) { 96 | 97 | val prompt = newPrompt ?: Prompt("") 98 | val promptNameTextField = JBTextField() 99 | val promptDescriptionTextField = JBTextField() 100 | val promptContentTextArea = JBTextArea() 101 | 102 | init { 103 | title = newPrompt?.let { message("settings.prompt.edit.title") } ?: message("settings.prompt.add.title") 104 | setOKButtonText(newPrompt?.let { message("actions.update") } ?: message("actions.add")) 105 | setSize(700, 500) 106 | 107 | promptContentTextArea.wrapStyleWord = true 108 | promptContentTextArea.lineWrap = true 109 | 110 | if (!prompt.canBeChanged) { 111 | isOKActionEnabled = false 112 | promptNameTextField.isEditable = false 113 | promptDescriptionTextField.isEditable = false 114 | promptContentTextArea.isEditable = false 115 | } 116 | 117 | init() 118 | } 119 | 120 | override fun createCenterPanel() = panel { 121 | row(message("settings.prompt.name")) { 122 | cell(promptNameTextField) 123 | .align(Align.FILL) 124 | .bindText(prompt::name) 125 | .applyIf(prompt.canBeChanged) { focused() } 126 | .validationOnApply { notBlank(it.text) } 127 | .applyIf(newPrompt == null) { validationOnApply { unique(it.text.lowercase(), prompts) } } 128 | } 129 | row(message("settings.prompt.description")) { 130 | cell(promptDescriptionTextField) 131 | .align(Align.FILL) 132 | .bindText(prompt::description) 133 | .validationOnApply { notBlank(it.text) } 134 | } 135 | row { 136 | label(message("settings.prompt.content")) 137 | } 138 | row() { 139 | cell(promptContentTextArea) 140 | .align(Align.FILL) 141 | .bindText(prompt::content) 142 | .validationOnApply { notBlank(it.text) } 143 | .resizableColumn() 144 | }.resizableRow() 145 | row { 146 | comment(message("settings.prompt.comment")) 147 | } 148 | } 149 | 150 | } 151 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.marc-r2.commit_gpt-intellij-plugin 5 | 6 | 8 | CommitGPT 9 | 10 | 11 | Marc-R2 12 | 13 | 16 | CommitGPT is a plugin that generates a commit messages based on you changes with ChatGPT.

18 |

Features

19 |
    20 |
  • Generate commit message from diff using OpenAI API
  • 21 |
  • Compute diff only from the selected files and lines in the commit dialog
  • 22 |
  • Create your own prompt for commit message generation
  • 23 |
  • Customize the AI prompt
  • 24 |
  • Give the AI a hint for the commit message (optional)
  • 25 |
26 |

Usage

27 |

To get started, install the plugin and set OpenAI private token in plugin's settings: 28 |
29 | Settings > Tools > CommitGPT

30 |

Troubleshooting

31 |

We'd love to hear from you if you have any issues or feature requests. Please report them 32 | over on GitHub.

33 | 34 |

Hints for the AI

35 |

36 | Just use the commit message field in the commit dialog to give the AI a hint for the commit message by 37 | adding a '!' as the first character of the hint. "!". This is optional. 38 |

39 | ]]>
40 | 41 | 43 | com.intellij.modules.platform 44 | Git4Idea 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | messages.MyBundle 55 | 56 | 58 | 59 | 60 | 61 | 64 | 65 | 70 | 75 | 77 | 78 | 79 | 80 | 81 | 82 | 88 | 89 | 90 | 91 | 92 | 93 |
94 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/icons/commit_gpt.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/messages/MyBundle.properties: -------------------------------------------------------------------------------- 1 | name=CommitGPT 2 | settings.title=Settings 3 | settings.general.group.title=CommitGPT 4 | settings.openAIToken=OpenAI token 5 | settings.locale=Locale 6 | settings.prompt=Prompt 7 | settings.openAITokenComment=\ 8 |

You can get your token here.

9 | settings.report-bug=Report bug 10 | settings.verifyToken=Verify 11 | settings.verify.valid=Open AI token is valid. 12 | settings.verify.invalid=Open AI token is not valid ({0}). 13 | settings.verify.running=Verifying Open AI token... 14 | settings.verify.token-is-empty=Open AI token is empty. 15 | action.background=Generating commit message 16 | notifications.title=CommitGPT 17 | notifications.welcome=Thanks for installing CommitGPT {0} 18 | notifications.unsuccessful-request=Error occurred: {0} 19 | notification.group.important.name=CommitGPT important 20 | notification.group.general.name=CommitGPT general 21 | notifications.empty-diff=Git diff is empty. 22 | notifications.no-field=Missing commit field 23 | actions.do-not-ask-again=Do not ask me again. 24 | actions.take-me-there=Take me there. 25 | actions.sure-take-me-there=Sure, take me there. 26 | notifications.prompt-too-large=The diff is too large for the OpenAI API. Try reducing the number of staged changes, or write your own commit message. 27 | notifications.no-commit-message=Commit field has not been initialized correctly. 28 | notifications.uses-prompt=Prompt:\n{0} 29 | notifications.unable-to-save-token=Token could not be saved. 30 | settings.prompt.name=Name 31 | settings.prompt.content=Content 32 | validation.required=This value is required. 33 | validation.number=Value is not a number. 34 | settings.prompt.comment=You can use {locale}, {diff} and {hint} variables to customise your prompt.\ 35 | \n{diff} will be replaced with the diff of the staged changes.\ 36 | \n{locale} will be replaced with the selected locale.\ 37 | \n{hint} will be replaced with the commit message hint, if you provided one. 38 | actions.update=Update 39 | actions.add=Add 40 | settings.prompt.edit.title=Edit Prompt 41 | settings.prompt.add.title=Add Prompt 42 | settings.prompt.description=Description 43 | validation.unique=Value already exists. 44 | settings.openAIProxy=OpenAI proxy url 45 | 46 | --------------------------------------------------------------------------------