commitgpt 0.3.3 ā 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +37 -1
- data/commitgpt.gemspec +2 -2
- data/lib/commitgpt/cli.rb +6 -0
- data/lib/commitgpt/commit_ai.rb +136 -130
- data/lib/commitgpt/config_manager.rb +16 -0
- data/lib/commitgpt/diff_helpers.rb +149 -0
- data/lib/commitgpt/provider_presets.rb +8 -7
- data/lib/commitgpt/setup_wizard.rb +16 -0
- data/lib/commitgpt/string.rb +19 -0
- data/lib/commitgpt/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b47c936f4c64c0d4cb2fb5214d1bf28b188b1c14525d5984be2fb2a0f84fb287
|
|
4
|
+
data.tar.gz: 540d6f74110a1d86e1a34a366456224f56b78888ddbafc070fb5ef0594697e59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60c0ea07f58567bee161b6037fe6db60d92344b1098c88e33c9e4d4908ca08b36b66f722d29cc743308c1bc1f4d4c16f70fb5a7e2a65db1d8c3c0a8adafd5ef2
|
|
7
|
+
data.tar.gz: 664fa8dc1665d983485c97d064e440e434b2645ff578954d2b762ceec4a741649bb20af3c32dbd180560cd67838e6a4eee1662ef75c56b53c790216d9738f2af
|
data/README.md
CHANGED
|
@@ -155,8 +155,44 @@ $ aicm -m
|
|
|
155
155
|
$ aicm --models
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
### Choose Commit Message Format
|
|
159
|
+
Select your preferred commit message format:
|
|
160
|
+
```bash
|
|
161
|
+
$ aicm -f
|
|
162
|
+
# or
|
|
163
|
+
$ aicm --format
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
CommitGPT supports three commit message formats:
|
|
167
|
+
- **Simple** - Concise commit message (default)
|
|
168
|
+
- **Conventional** - Follow [Conventional Commits](https://www.conventionalcommits.org/) specification
|
|
169
|
+
- **Gitmoji** - Use [Gitmoji](https://gitmoji.dev/) emoji standard
|
|
170
|
+
|
|
171
|
+
Your selection will be saved in `~/.config/commitgpt/config.yml` and used for all future commits until changed.
|
|
172
|
+
|
|
173
|
+
#### Format Examples
|
|
174
|
+
|
|
175
|
+
**Simple:**
|
|
176
|
+
```
|
|
177
|
+
Add user authentication feature
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Conventional:**
|
|
181
|
+
```
|
|
182
|
+
feat: add user authentication feature
|
|
183
|
+
fix: resolve login timeout issue
|
|
184
|
+
docs: update API documentation
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Gitmoji:**
|
|
188
|
+
```
|
|
189
|
+
⨠add user authentication feature
|
|
190
|
+
š resolve login timeout issue
|
|
191
|
+
š update API documentation
|
|
192
|
+
```
|
|
193
|
+
|
|
158
194
|
### Check Configuration
|
|
159
|
-
View your current configuration (Provider, Model, Base URL, Diff Len):
|
|
195
|
+
View your current configuration (Provider, Model, Format, Base URL, Diff Len):
|
|
160
196
|
```bash
|
|
161
197
|
$ aicm help
|
|
162
198
|
```
|
data/commitgpt.gemspec
CHANGED
|
@@ -27,8 +27,8 @@ Gem::Specification.new do |spec|
|
|
|
27
27
|
spec.require_paths = ['lib']
|
|
28
28
|
|
|
29
29
|
# Uncomment to register a new dependency of your gem
|
|
30
|
-
spec.add_dependency
|
|
31
|
-
spec.add_dependency
|
|
30
|
+
spec.add_dependency 'httparty', '~> 0.24'
|
|
31
|
+
spec.add_dependency 'thor', '~> 1.4'
|
|
32
32
|
spec.add_dependency 'tty-prompt', '~> 0.23'
|
|
33
33
|
|
|
34
34
|
# For more information and examples about making a new gem, checkout our
|
data/lib/commitgpt/cli.rb
CHANGED
|
@@ -13,11 +13,14 @@ module CommitGpt
|
|
|
13
13
|
method_option :models, aliases: '-m', type: :boolean, desc: 'List/Select available models'
|
|
14
14
|
method_option :verbose, aliases: '-v', type: :boolean, desc: 'Show git diff being sent to AI'
|
|
15
15
|
method_option :provider, aliases: '-p', type: :boolean, desc: 'Switch active provider'
|
|
16
|
+
method_option :format, aliases: '-f', type: :boolean, desc: 'Choose commit message format'
|
|
16
17
|
def generate
|
|
17
18
|
if options[:provider]
|
|
18
19
|
CommitGpt::SetupWizard.new.switch_provider
|
|
19
20
|
elsif options[:models]
|
|
20
21
|
CommitGpt::SetupWizard.new.change_model
|
|
22
|
+
elsif options[:format]
|
|
23
|
+
CommitGpt::SetupWizard.new.choose_format
|
|
21
24
|
else
|
|
22
25
|
CommitGpt::CommitAi.new.aicm(verbose: options[:verbose])
|
|
23
26
|
end
|
|
@@ -38,6 +41,7 @@ module CommitGpt
|
|
|
38
41
|
shell.say 'Options:'
|
|
39
42
|
shell.say ' -m, --models # Interactive model selection'
|
|
40
43
|
shell.say ' -p, --provider # Switch active provider'
|
|
44
|
+
shell.say ' -f, --format # Choose commit message format'
|
|
41
45
|
shell.say ' -v, --verbose # Show git diff being sent to AI'
|
|
42
46
|
shell.say ''
|
|
43
47
|
|
|
@@ -52,9 +56,11 @@ module CommitGpt
|
|
|
52
56
|
shell.say "Bin Path: #{File.realpath($PROGRAM_NAME)}".gray
|
|
53
57
|
shell.say ''
|
|
54
58
|
|
|
59
|
+
format = CommitGpt::ConfigManager.get_commit_format
|
|
55
60
|
shell.say 'Current Configuration:'
|
|
56
61
|
shell.say " Provider: #{config['name'].green}"
|
|
57
62
|
shell.say " Model: #{config['model'].cyan}"
|
|
63
|
+
shell.say " Format: #{format.capitalize.yellow}"
|
|
58
64
|
shell.say " Base URL: #{config['base_url']}"
|
|
59
65
|
shell.say " Diff Len: #{config['diff_len']}"
|
|
60
66
|
shell.say ''
|
data/lib/commitgpt/commit_ai.rb
CHANGED
|
@@ -8,12 +8,114 @@ require 'io/console'
|
|
|
8
8
|
require 'tty-prompt'
|
|
9
9
|
require_relative 'string'
|
|
10
10
|
require_relative 'config_manager'
|
|
11
|
+
require_relative 'diff_helpers'
|
|
11
12
|
|
|
12
13
|
# CommitGpt based on GPT-3
|
|
13
14
|
module CommitGpt
|
|
14
15
|
# Commit AI roboter based on GPT-3
|
|
15
|
-
class CommitAi
|
|
16
|
-
|
|
16
|
+
class CommitAi # rubocop:disable Metrics/ClassLength
|
|
17
|
+
include DiffHelpers
|
|
18
|
+
|
|
19
|
+
attr_reader :api_key, :base_url, :model, :diff_len, :commit_format
|
|
20
|
+
|
|
21
|
+
# Commit format templates
|
|
22
|
+
COMMIT_FORMATS = {
|
|
23
|
+
'simple' => '<commit message>',
|
|
24
|
+
'conventional' => '<type>[optional (<scope>)]: <commit message>',
|
|
25
|
+
'gitmoji' => ':emoji: <commit message>'
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Conventional commit types based on aicommits implementation
|
|
29
|
+
CONVENTIONAL_TYPES = {
|
|
30
|
+
'docs' => 'Documentation only changes',
|
|
31
|
+
'style' => 'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
|
|
32
|
+
'refactor' => 'A code change that improves code structure without changing functionality (renaming, restructuring classes/methods, extracting functions, etc)',
|
|
33
|
+
'perf' => 'A code change that improves performance',
|
|
34
|
+
'test' => 'Adding missing tests or correcting existing tests',
|
|
35
|
+
'build' => 'Changes that affect the build system or external dependencies',
|
|
36
|
+
'ci' => 'Changes to our CI configuration files and scripts',
|
|
37
|
+
'chore' => "Other changes that don't modify src or test files",
|
|
38
|
+
'revert' => 'Reverts a previous commit',
|
|
39
|
+
'feat' => 'A new feature',
|
|
40
|
+
'fix' => 'A bug fix'
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# Gitmoji mappings based on gitmoji.dev
|
|
44
|
+
GITMOJI_TYPES = {
|
|
45
|
+
'šØ' => 'Improve structure / format of the code',
|
|
46
|
+
'ā”' => 'Improve performance',
|
|
47
|
+
'š„' => 'Remove code or files',
|
|
48
|
+
'š' => 'Fix a bug',
|
|
49
|
+
'š' => 'Critical hotfix',
|
|
50
|
+
'āØ' => 'Introduce new features',
|
|
51
|
+
'š' => 'Add or update documentation',
|
|
52
|
+
'š' => 'Deploy stuff',
|
|
53
|
+
'š' => 'Add or update the UI and style files',
|
|
54
|
+
'š' => 'Begin a project',
|
|
55
|
+
'ā
' => 'Add, update, or pass tests',
|
|
56
|
+
'š' => 'Fix security or privacy issues',
|
|
57
|
+
'š' => 'Add or update secrets',
|
|
58
|
+
'š' => 'Release / Version tags',
|
|
59
|
+
'šØ' => 'Fix compiler / linter warnings',
|
|
60
|
+
'š§' => 'Work in progress',
|
|
61
|
+
'š' => 'Fix CI Build',
|
|
62
|
+
'ā¬ļø' => 'Downgrade dependencies',
|
|
63
|
+
'ā¬ļø' => 'Upgrade dependencies',
|
|
64
|
+
'š' => 'Pin dependencies to specific versions',
|
|
65
|
+
'š·' => 'Add or update CI build system',
|
|
66
|
+
'š' => 'Add or update analytics or track code',
|
|
67
|
+
'ā»ļø' => 'Refactor code',
|
|
68
|
+
'ā' => 'Add a dependency',
|
|
69
|
+
'ā' => 'Remove a dependency',
|
|
70
|
+
'š§' => 'Add or update configuration files',
|
|
71
|
+
'šØ' => 'Add or update development scripts',
|
|
72
|
+
'š' => 'Internationalization and localization',
|
|
73
|
+
'āļø' => 'Fix typos',
|
|
74
|
+
'š©' => 'Write bad code that needs to be improved',
|
|
75
|
+
'āŖ' => 'Revert changes',
|
|
76
|
+
'š' => 'Merge branches',
|
|
77
|
+
'š¦' => 'Add or update compiled files or packages',
|
|
78
|
+
'š½' => 'Update code due to external API changes',
|
|
79
|
+
'š' => 'Move or rename resources (e.g.: files, paths, routes)',
|
|
80
|
+
'š' => 'Add or update license',
|
|
81
|
+
'š„' => 'Introduce breaking changes',
|
|
82
|
+
'š±' => 'Add or update assets',
|
|
83
|
+
'āæ' => 'Improve accessibility',
|
|
84
|
+
'š”' => 'Add or update comments in source code',
|
|
85
|
+
'š»' => 'Write code drunkenly',
|
|
86
|
+
'š¬' => 'Add or update text and literals',
|
|
87
|
+
'š' => 'Perform database related changes',
|
|
88
|
+
'š' => 'Add or update logs',
|
|
89
|
+
'š' => 'Remove logs',
|
|
90
|
+
'š„' => 'Add or update contributor(s)',
|
|
91
|
+
'šø' => 'Improve user experience / usability',
|
|
92
|
+
'š' => 'Make architectural changes',
|
|
93
|
+
'š±' => 'Work on responsive design',
|
|
94
|
+
'š¤”' => 'Mock things',
|
|
95
|
+
'š„' => 'Add or update an easter egg',
|
|
96
|
+
'š' => 'Add or update a .gitignore file',
|
|
97
|
+
'šø' => 'Add or update snapshots',
|
|
98
|
+
'ā' => 'Perform experiments',
|
|
99
|
+
'š' => 'Improve SEO',
|
|
100
|
+
'š·' => 'Add or update types',
|
|
101
|
+
'š±' => 'Add or update seed files',
|
|
102
|
+
'š©' => 'Add, update, or remove feature flags',
|
|
103
|
+
'š„
' => 'Catch errors',
|
|
104
|
+
'š«' => 'Add or update animations and transitions',
|
|
105
|
+
'š' => 'Deprecate code that needs to be cleaned up',
|
|
106
|
+
'š' => 'Work on code related to authorization, roles and permissions',
|
|
107
|
+
'š©¹' => 'Simple fix for a non-critical issue',
|
|
108
|
+
'š§' => 'Data exploration/inspection',
|
|
109
|
+
'ā°' => 'Remove dead code',
|
|
110
|
+
'š§Ŗ' => 'Add a failing test',
|
|
111
|
+
'š' => 'Add or update business logic',
|
|
112
|
+
'š©ŗ' => 'Add or update healthcheck',
|
|
113
|
+
'š§±' => 'Infrastructure related changes',
|
|
114
|
+
'š§āš»' => 'Improve developer experience',
|
|
115
|
+
'šø' => 'Add sponsorships or money related infrastructure',
|
|
116
|
+
'š§µ' => 'Add or update code related to multithreading or concurrency',
|
|
117
|
+
'š¦ŗ' => 'Add or update code related to validation'
|
|
118
|
+
}.freeze
|
|
17
119
|
|
|
18
120
|
def initialize
|
|
19
121
|
provider_config = ConfigManager.get_active_provider_config
|
|
@@ -29,6 +131,8 @@ module CommitGpt
|
|
|
29
131
|
@model = nil
|
|
30
132
|
@diff_len = 32_768
|
|
31
133
|
end
|
|
134
|
+
|
|
135
|
+
@commit_format = ConfigManager.get_commit_format
|
|
32
136
|
end
|
|
33
137
|
|
|
34
138
|
def aicm(verbose: false)
|
|
@@ -127,126 +231,6 @@ module CommitGpt
|
|
|
127
231
|
generate_commit(diff)
|
|
128
232
|
end
|
|
129
233
|
|
|
130
|
-
def git_diff
|
|
131
|
-
exclusions = '":(exclude)Gemfile.lock" ":(exclude)package-lock.json" ":(exclude)yarn.lock" ":(exclude)pnpm-lock.yaml"'
|
|
132
|
-
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
133
|
-
diff_unstaged = `git diff . #{exclusions}`.chomp
|
|
134
|
-
|
|
135
|
-
if !diff_unstaged.empty?
|
|
136
|
-
if diff_cached.empty?
|
|
137
|
-
# Scenario: Only unstaged changes
|
|
138
|
-
choice = prompt_no_staged_changes
|
|
139
|
-
case choice
|
|
140
|
-
when :add_all
|
|
141
|
-
puts 'ā² Running git add .'.yellow
|
|
142
|
-
system('git add .')
|
|
143
|
-
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
144
|
-
if diff_cached.empty?
|
|
145
|
-
puts 'ā² Still no changes to commit.'.red
|
|
146
|
-
return nil
|
|
147
|
-
end
|
|
148
|
-
when :exit
|
|
149
|
-
return nil
|
|
150
|
-
end
|
|
151
|
-
else
|
|
152
|
-
# Scenario: Mixed state (some staged, some not)
|
|
153
|
-
puts 'ā² You have both staged and unstaged changes:'.yellow
|
|
154
|
-
|
|
155
|
-
staged_files = `git diff --cached --name-status . #{exclusions}`.chomp
|
|
156
|
-
unstaged_files = `git diff --name-status . #{exclusions}`.chomp
|
|
157
|
-
|
|
158
|
-
puts "\n #{'Staged changes:'.green}"
|
|
159
|
-
puts staged_files.gsub(/^/, ' ')
|
|
160
|
-
|
|
161
|
-
puts "\n #{'Unstaged changes:'.red}"
|
|
162
|
-
puts unstaged_files.gsub(/^/, ' ')
|
|
163
|
-
puts ''
|
|
164
|
-
|
|
165
|
-
prompt = TTY::Prompt.new
|
|
166
|
-
choice = prompt.select('How to proceed?') do |menu|
|
|
167
|
-
menu.choice 'Include unstaged changes (git add .)', :add_all
|
|
168
|
-
menu.choice 'Use staged changes only', :staged_only
|
|
169
|
-
menu.choice 'Exit', :exit
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
case choice
|
|
173
|
-
when :add_all
|
|
174
|
-
puts 'ā² Running git add .'.yellow
|
|
175
|
-
system('git add .')
|
|
176
|
-
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
177
|
-
when :exit
|
|
178
|
-
return nil
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
elsif diff_cached.empty?
|
|
182
|
-
# Scenario: No changes at all (staged or unstaged)
|
|
183
|
-
# Check if there are ANY unstaged files (maybe untracked?)
|
|
184
|
-
# git status --porcelain includes untracked files
|
|
185
|
-
git_status = `git status --porcelain`.chomp
|
|
186
|
-
if git_status.empty?
|
|
187
|
-
puts 'ā² No changes to commit. Working tree clean.'.yellow
|
|
188
|
-
return nil
|
|
189
|
-
else
|
|
190
|
-
# Only untracked files? Or ignored files?
|
|
191
|
-
# If diff_unstaged is empty but git status is not, it usually means untracked files.
|
|
192
|
-
# Let's offer to add them too.
|
|
193
|
-
choice = prompt_no_staged_changes
|
|
194
|
-
case choice
|
|
195
|
-
when :add_all
|
|
196
|
-
puts 'ā² Running git add .'.yellow
|
|
197
|
-
system('git add .')
|
|
198
|
-
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
199
|
-
when :exit
|
|
200
|
-
return nil
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
diff = diff_cached
|
|
206
|
-
|
|
207
|
-
if diff.length > @diff_len
|
|
208
|
-
choice = prompt_diff_handling(diff.length, @diff_len)
|
|
209
|
-
case choice
|
|
210
|
-
when :truncate
|
|
211
|
-
puts "ā² Truncating diff to #{@diff_len} chars...".yellow
|
|
212
|
-
diff = diff[0...@diff_len]
|
|
213
|
-
when :unlimited
|
|
214
|
-
puts "ā² Using full diff (#{diff.length} chars)...".yellow
|
|
215
|
-
when :exit
|
|
216
|
-
return nil
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
diff
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def prompt_no_staged_changes
|
|
224
|
-
puts 'ā² No staged changes found (but unstaged/untracked files exist).'.yellow
|
|
225
|
-
prompt = TTY::Prompt.new
|
|
226
|
-
begin
|
|
227
|
-
prompt.select('Choose an option:') do |menu|
|
|
228
|
-
menu.choice "Run 'git add .' to stage all changes", :add_all
|
|
229
|
-
menu.choice 'Exit (stage files manually)', :exit
|
|
230
|
-
end
|
|
231
|
-
rescue TTY::Reader::InputInterrupt, Interrupt
|
|
232
|
-
:exit
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def prompt_diff_handling(current_len, max_len)
|
|
237
|
-
puts "ā² The diff is too large (#{current_len} chars, max #{max_len}).".yellow
|
|
238
|
-
prompt = TTY::Prompt.new
|
|
239
|
-
begin
|
|
240
|
-
prompt.select('Choose an option:') do |menu|
|
|
241
|
-
menu.choice "Use first #{max_len} characters to generate commit message", :truncate
|
|
242
|
-
menu.choice 'Use unlimited characters (may fail or be slow)', :unlimited
|
|
243
|
-
menu.choice 'Exit', :exit
|
|
244
|
-
end
|
|
245
|
-
rescue TTY::Reader::InputInterrupt, Interrupt
|
|
246
|
-
:exit
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
|
|
250
234
|
def welcome
|
|
251
235
|
puts "\nā² Welcome to AI Commits!".green
|
|
252
236
|
|
|
@@ -275,21 +259,42 @@ module CommitGpt
|
|
|
275
259
|
puts 'ā² This is not a git repository'.red
|
|
276
260
|
return false
|
|
277
261
|
end
|
|
278
|
-
|
|
279
262
|
true
|
|
280
263
|
end
|
|
281
264
|
|
|
265
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
282
266
|
def generate_commit(diff = '')
|
|
267
|
+
# Build format-specific prompt
|
|
268
|
+
base_prompt = 'Generate a concise git commit message title in present tense that precisely describes the key changes in the following code diff. Focus on what was changed, not just file names. Provide only the title, no description or body.'
|
|
269
|
+
|
|
270
|
+
format_instruction = case @commit_format
|
|
271
|
+
when 'conventional'
|
|
272
|
+
"Choose a type from the type-to-description JSON below that best describes the git diff:\n#{JSON.pretty_generate(CONVENTIONAL_TYPES)}"
|
|
273
|
+
when 'gitmoji'
|
|
274
|
+
"Choose an emoji from the emoji-to-description JSON below that best describes the git diff:\n#{JSON.pretty_generate(GITMOJI_TYPES)}"
|
|
275
|
+
else
|
|
276
|
+
''
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
format_spec = "The output response must be in format:\n#{COMMIT_FORMATS[@commit_format]}"
|
|
280
|
+
|
|
281
|
+
system_content = [
|
|
282
|
+
base_prompt,
|
|
283
|
+
'Message language: English.',
|
|
284
|
+
'Rules:',
|
|
285
|
+
'- Commit message must be a maximum of 100 characters.',
|
|
286
|
+
'- Exclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.',
|
|
287
|
+
'- IMPORTANT: Do not include any explanations, introductions, or additional text. Do not wrap the commit message in quotes or any other formatting. The commit message must not exceed 100 characters. Respond with ONLY the commit message text.',
|
|
288
|
+
'- Be specific: include concrete details (package names, versions, functionality) rather than generic statements.',
|
|
289
|
+
'- Return ONLY the commit message, nothing else.',
|
|
290
|
+
format_instruction,
|
|
291
|
+
format_spec
|
|
292
|
+
].reject(&:empty?).join("\n")
|
|
293
|
+
|
|
283
294
|
messages = [
|
|
284
295
|
{
|
|
285
296
|
role: 'system',
|
|
286
|
-
content:
|
|
287
|
-
"Message language: English. Rules:\n" \
|
|
288
|
-
"- Commit message must be a maximum of 100 characters.\n" \
|
|
289
|
-
"- Exclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.\n" \
|
|
290
|
-
"- IMPORTANT: Do not include any explanations, introductions, or additional text. Do not wrap the commit message in quotes or any other formatting. The commit message must not exceed 100 characters. Respond with ONLY the commit message text. \n" \
|
|
291
|
-
"- Be specific: include concrete details (package names, versions, functionality) rather than generic statements. \n" \
|
|
292
|
-
'- Return ONLY the commit message, nothing else.'
|
|
297
|
+
content: system_content
|
|
293
298
|
},
|
|
294
299
|
{
|
|
295
300
|
role: 'user',
|
|
@@ -490,5 +495,6 @@ module CommitGpt
|
|
|
490
495
|
first_line = full_content.split("\n").map(&:strip).reject(&:empty?).first
|
|
491
496
|
first_line&.gsub(/\A["']|["']\z/, '') || ''
|
|
492
497
|
end
|
|
498
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
493
499
|
end
|
|
494
500
|
end
|
|
@@ -141,6 +141,22 @@ module CommitGpt
|
|
|
141
141
|
save_main_config(main_config)
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
+
# Get commit format configuration
|
|
145
|
+
def get_commit_format
|
|
146
|
+
return 'simple' unless config_exists?
|
|
147
|
+
|
|
148
|
+
main_config = YAML.load_file(main_config_path)
|
|
149
|
+
main_config['commit_format'] || 'simple'
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Set commit format configuration
|
|
153
|
+
def set_commit_format(format)
|
|
154
|
+
ensure_config_dir
|
|
155
|
+
main_config = config_exists? ? YAML.load_file(main_config_path) : { 'providers' => [], 'active_provider' => '' }
|
|
156
|
+
main_config['commit_format'] = format
|
|
157
|
+
save_main_config(main_config)
|
|
158
|
+
end
|
|
159
|
+
|
|
144
160
|
private
|
|
145
161
|
|
|
146
162
|
# Merge main config with local config (local overrides main)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommitGpt
|
|
4
|
+
# Helper methods for handling git diffs
|
|
5
|
+
# rubocop:disable Metrics/ModuleLength
|
|
6
|
+
module DiffHelpers
|
|
7
|
+
# Lock files to exclude from diff but detect changes
|
|
8
|
+
LOCK_FILES = %w[Gemfile.lock package-lock.json yarn.lock pnpm-lock.yaml].freeze
|
|
9
|
+
|
|
10
|
+
def git_diff
|
|
11
|
+
exclusions = LOCK_FILES.map { |f| "\":(exclude)#{f}\"" }.join(' ')
|
|
12
|
+
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
13
|
+
diff_unstaged = `git diff . #{exclusions}`.chomp
|
|
14
|
+
|
|
15
|
+
# Detect lock file changes and build summary
|
|
16
|
+
@lock_file_summary = detect_lock_file_changes
|
|
17
|
+
|
|
18
|
+
if !diff_unstaged.empty?
|
|
19
|
+
if diff_cached.empty?
|
|
20
|
+
# Scenario: Only unstaged changes
|
|
21
|
+
choice = prompt_no_staged_changes
|
|
22
|
+
case choice
|
|
23
|
+
when :add_all
|
|
24
|
+
puts 'ā² Running git add .'.yellow
|
|
25
|
+
system('git add .')
|
|
26
|
+
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
27
|
+
if diff_cached.empty?
|
|
28
|
+
puts 'ā² Still no changes to commit.'.red
|
|
29
|
+
return nil
|
|
30
|
+
end
|
|
31
|
+
when :exit
|
|
32
|
+
return nil
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
# Scenario: Mixed state (some staged, some not)
|
|
36
|
+
puts 'ā² You have both staged and unstaged changes:'.yellow
|
|
37
|
+
|
|
38
|
+
staged_files = `git diff --cached --name-status . #{exclusions}`.chomp
|
|
39
|
+
unstaged_files = `git diff --name-status . #{exclusions}`.chomp
|
|
40
|
+
|
|
41
|
+
puts "\n #{'Staged changes:'.green}"
|
|
42
|
+
puts staged_files.gsub(/^/, ' ')
|
|
43
|
+
|
|
44
|
+
puts "\n #{'Unstaged changes:'.red}"
|
|
45
|
+
puts unstaged_files.gsub(/^/, ' ')
|
|
46
|
+
puts ''
|
|
47
|
+
|
|
48
|
+
prompt = TTY::Prompt.new
|
|
49
|
+
choice = prompt.select('How to proceed?') do |menu|
|
|
50
|
+
menu.choice 'Include unstaged changes (git add .)', :add_all
|
|
51
|
+
menu.choice 'Use staged changes only', :staged_only
|
|
52
|
+
menu.choice 'Exit', :exit
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
case choice
|
|
56
|
+
when :add_all
|
|
57
|
+
puts 'ā² Running git add .'.yellow
|
|
58
|
+
system('git add .')
|
|
59
|
+
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
60
|
+
when :exit
|
|
61
|
+
return nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
elsif diff_cached.empty?
|
|
65
|
+
# Scenario: No changes at all (staged or unstaged)
|
|
66
|
+
# Check if there are ANY unstaged files (maybe untracked?)
|
|
67
|
+
# git status --porcelain includes untracked files
|
|
68
|
+
git_status = `git status --porcelain`.chomp
|
|
69
|
+
if git_status.empty?
|
|
70
|
+
puts 'ā² No changes to commit. Working tree clean.'.yellow
|
|
71
|
+
return nil
|
|
72
|
+
else
|
|
73
|
+
# Only untracked files? Or ignored files?
|
|
74
|
+
# If diff_unstaged is empty but git status is not, it usually means untracked files.
|
|
75
|
+
# Let's offer to add them too.
|
|
76
|
+
choice = prompt_no_staged_changes
|
|
77
|
+
case choice
|
|
78
|
+
when :add_all
|
|
79
|
+
puts 'ā² Running git add .'.yellow
|
|
80
|
+
system('git add .')
|
|
81
|
+
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
82
|
+
when :exit
|
|
83
|
+
return nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
diff = diff_cached
|
|
89
|
+
|
|
90
|
+
# Prepend lock file summary to diff if present
|
|
91
|
+
diff = "#{@lock_file_summary}\n\n#{diff}" if @lock_file_summary
|
|
92
|
+
|
|
93
|
+
if diff.length > diff_len
|
|
94
|
+
choice = prompt_diff_handling(diff.length, diff_len)
|
|
95
|
+
case choice
|
|
96
|
+
when :truncate
|
|
97
|
+
puts "ā² Truncating diff to #{diff_len} chars...".yellow
|
|
98
|
+
diff = diff[0...diff_len]
|
|
99
|
+
when :unlimited
|
|
100
|
+
puts "ā² Using full diff (#{diff.length} chars)...".yellow
|
|
101
|
+
when :exit
|
|
102
|
+
return nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
diff
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def prompt_no_staged_changes
|
|
110
|
+
puts 'ā² No staged changes found (but unstaged/untracked files exist).'.yellow
|
|
111
|
+
prompt = TTY::Prompt.new
|
|
112
|
+
begin
|
|
113
|
+
prompt.select('Choose an option:') do |menu|
|
|
114
|
+
menu.choice "Run 'git add .' to stage all changes", :add_all
|
|
115
|
+
menu.choice 'Exit (stage files manually)', :exit
|
|
116
|
+
end
|
|
117
|
+
rescue TTY::Reader::InputInterrupt, Interrupt
|
|
118
|
+
:exit
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def prompt_diff_handling(current_len, max_len)
|
|
123
|
+
puts "ā² The diff is too large (#{current_len} chars, max #{max_len}).".yellow
|
|
124
|
+
prompt = TTY::Prompt.new
|
|
125
|
+
begin
|
|
126
|
+
prompt.select('Choose an option:') do |menu|
|
|
127
|
+
menu.choice "Use first #{max_len} characters to generate commit message", :truncate
|
|
128
|
+
menu.choice 'Use unlimited characters (may fail or be slow)', :unlimited
|
|
129
|
+
menu.choice 'Exit', :exit
|
|
130
|
+
end
|
|
131
|
+
rescue TTY::Reader::InputInterrupt, Interrupt
|
|
132
|
+
:exit
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def detect_lock_file_changes
|
|
137
|
+
# Check both staged and unstaged changes for lock files
|
|
138
|
+
staged_files = `git diff --cached --name-only`.chomp.split("\n")
|
|
139
|
+
unstaged_files = `git diff --name-only`.chomp.split("\n")
|
|
140
|
+
changed_files = (staged_files + unstaged_files).uniq
|
|
141
|
+
|
|
142
|
+
updated_locks = LOCK_FILES.select { |lock| changed_files.include?(lock) }
|
|
143
|
+
return nil if updated_locks.empty?
|
|
144
|
+
|
|
145
|
+
updated_locks.map { |f| "#{f} updated (dependency changes)" }.join(', ')
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
# rubocop:enable Metrics/ModuleLength
|
|
149
|
+
end
|
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
module CommitGpt
|
|
4
4
|
# Provider presets for common AI providers
|
|
5
5
|
PROVIDER_PRESETS = [
|
|
6
|
+
{ label: 'Anthropic Claude', value: 'anthropic', base_url: 'https://api.anthropic.com/v1' },
|
|
6
7
|
{ label: 'Cerebras', value: 'cerebras', base_url: 'https://api.cerebras.ai/v1' },
|
|
7
|
-
{ label: '
|
|
8
|
-
{ label: '
|
|
8
|
+
{ label: 'DeepSeek', value: 'deepseek', base_url: 'https://api.deepseek.com' },
|
|
9
|
+
{ label: 'Google AI', value: 'gemini', base_url: 'https://generativelanguage.googleapis.com/v1beta/openai' },
|
|
10
|
+
{ label: 'Groq', value: 'groq', base_url: 'https://api.groq.com/openai/v1' },
|
|
9
11
|
{ label: 'LLaMa.cpp', value: 'llamacpp', base_url: 'http://127.0.0.1:8080/v1' },
|
|
10
12
|
{ label: 'LM Studio', value: 'lmstudio', base_url: 'http://127.0.0.1:1234/v1' },
|
|
11
13
|
{ label: 'Llamafile', value: 'llamafile', base_url: 'http://127.0.0.1:8080/v1' },
|
|
12
|
-
{ label: 'DeepSeek', value: 'deepseek', base_url: 'https://api.deepseek.com' },
|
|
13
|
-
{ label: 'Groq', value: 'groq', base_url: 'https://api.groq.com/openai/v1' },
|
|
14
14
|
{ label: 'Mistral', value: 'mistral', base_url: 'https://api.mistral.ai/v1' },
|
|
15
|
-
{ label: '
|
|
16
|
-
{ label: '
|
|
17
|
-
{ label: '
|
|
15
|
+
{ label: 'NVIDIA NIM', value: 'nvidia_nim', base_url: 'https://integrate.api.nvidia.com/v1' },
|
|
16
|
+
{ label: 'Ollama', value: 'ollama', base_url: 'http://127.0.0.1:11434/v1' },
|
|
17
|
+
{ label: 'OpenAI', value: 'openai', base_url: 'https://api.openai.com/v1' },
|
|
18
|
+
{ label: 'OpenRouter', value: 'openrouter', base_url: 'https://openrouter.ai/api/v1' }
|
|
18
19
|
].freeze
|
|
19
20
|
end
|
|
@@ -86,6 +86,22 @@ module CommitGpt
|
|
|
86
86
|
puts "\nModel selected: #{model}".green
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# Choose commit message format
|
|
90
|
+
def choose_format
|
|
91
|
+
prompt = TTY::Prompt.new
|
|
92
|
+
|
|
93
|
+
puts "\nā² Choose git commit message format:\n".green
|
|
94
|
+
|
|
95
|
+
format = prompt.select('Select format:') do |menu|
|
|
96
|
+
menu.choice 'Simple - Concise commit message', 'simple'
|
|
97
|
+
menu.choice 'Conventional - Follow Conventional Commits specification', 'conventional'
|
|
98
|
+
menu.choice 'Gitmoji - Use Gitmoji emoji standard', 'gitmoji'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
ConfigManager.set_commit_format(format)
|
|
102
|
+
puts "\nā² Commit format set to: #{format}".green
|
|
103
|
+
end
|
|
104
|
+
|
|
89
105
|
private
|
|
90
106
|
|
|
91
107
|
# Select provider from list
|
data/lib/commitgpt/string.rb
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tty-prompt'
|
|
4
|
+
|
|
5
|
+
# Monkey patch TTY::Prompt::List to add padding at the bottom of menus
|
|
6
|
+
module TTY
|
|
7
|
+
class Prompt
|
|
8
|
+
# Monkey patch to add padding
|
|
9
|
+
class List
|
|
10
|
+
alias original_render_menu render_menu
|
|
11
|
+
|
|
12
|
+
# Override render_menu to add empty lines at the bottom
|
|
13
|
+
def render_menu
|
|
14
|
+
output = original_render_menu
|
|
15
|
+
# Add 2 empty lines at the bottom so menu doesn't stick to terminal edge
|
|
16
|
+
"#{output}\n\n"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
3
22
|
# Open String to add color
|
|
4
23
|
class String
|
|
5
24
|
def red
|
data/lib/commitgpt/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: commitgpt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peng Zhang
|
|
@@ -68,6 +68,7 @@ files:
|
|
|
68
68
|
- lib/commitgpt/cli.rb
|
|
69
69
|
- lib/commitgpt/commit_ai.rb
|
|
70
70
|
- lib/commitgpt/config_manager.rb
|
|
71
|
+
- lib/commitgpt/diff_helpers.rb
|
|
71
72
|
- lib/commitgpt/provider_presets.rb
|
|
72
73
|
- lib/commitgpt/setup_wizard.rb
|
|
73
74
|
- lib/commitgpt/string.rb
|