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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc31b958101709cf5acae4d52106fd56ede0dea448fbc8332d60f4865315adeb
4
- data.tar.gz: d60c821b82edd35a28caa2254a72722208af8019af86bb5e6f30d326f6a77845
3
+ metadata.gz: b47c936f4c64c0d4cb2fb5214d1bf28b188b1c14525d5984be2fb2a0f84fb287
4
+ data.tar.gz: 540d6f74110a1d86e1a34a366456224f56b78888ddbafc070fb5ef0594697e59
5
5
  SHA512:
6
- metadata.gz: ee1adb84767305fb84d0b2371685498ebcb28efaaf50920425483c3db563a7e1917fa69735f08203f6d990d071d3b5c0536c28992cf655f3a75db5b49bb8fb99
7
- data.tar.gz: 77e6f1cb82eecfe6e30f947fe3dd2fc1a0c4a456f104bee0119f459ae56296297183e16dcb4858bcef2a2857207cc4ecd2d7865df7a57760820e23df6ea21706
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 "httparty", "~> 0.24"
31
- spec.add_dependency "thor", "~> 1.4"
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 ''
@@ -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
- attr_reader :api_key, :base_url, :model, :diff_len
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: '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. ' \
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: 'Ollama', value: 'ollama', base_url: 'http://127.0.0.1:11434/v1' },
8
- { label: 'OpenAI', value: 'openai', base_url: 'https://api.openai.com/v1' },
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: 'Anthropic (Claude)', value: 'anthropic', base_url: 'https://api.anthropic.com/v1' },
16
- { label: 'OpenRouter', value: 'openrouter', base_url: 'https://openrouter.ai/api/v1' },
17
- { label: 'Google AI', value: 'gemini', base_url: 'https://generativelanguage.googleapis.com/v1beta/openai' }
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CommitGpt
4
- VERSION = '0.3.3'
4
+ VERSION = '0.3.5'
5
5
  end
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.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