├── .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 |
6 | CommitGPT
7 | CommitGPT for IntelliJ based IDEs/Android Studio.
8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------