commitgpt 0.3.10 → 0.3.11

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: bc50a5ed7cfb5cc59703014b3b6f58370028dfe2c1bc5c7186e3c6d43a755dd7
4
- data.tar.gz: e9dd5c01f7c07062fc803710c79a31ce40ab9a9625918c2753bf59d25b4f9d4b
3
+ metadata.gz: b8a2b39ee34a81a322ba56a693b002e1a8b29bc6adc432b4f41992c71ff3bd4a
4
+ data.tar.gz: cd4289fee106ee2feb1e89c71b29889b6fe0232c542452588fd0564b4b49593f
5
5
  SHA512:
6
- metadata.gz: 2005518993b77ff65ab764075016d6c1a8bb1259efff645017ab3f0a23a83f03e1b18fc7df257f497a8ecf5a705d4a0c91c29a5e044d00e9896437faed7cf675
7
- data.tar.gz: d290dac27335967df2b7dfdc8309509f7f228ede0d50c7e0b86e99c28cb16b1551d2bb5004ce3d846a183f1fb85beda193d5fec2873ad9e397211fa6e93ceb26
6
+ metadata.gz: 77b3f3fe14333b001537c52482b3424f2b5c679434b7ea52bb06e001e64cd5cc0f0c61782ead4d8416bf01473a7099398ba2d4eea84c268f84a14e6a62e5448b
7
+ data.tar.gz: 073de1a61a918be54e7ca2f2bb750b094aa1f0e1d56e195ad6239a8e4eb0431570ff4dfaedd22255e843d8c994df8c248d74f7cb1d306c6644e145e064c306ca
data/README.md CHANGED
@@ -17,9 +17,12 @@ The easiest way to install and keep CommitGPT updated.
17
17
 
18
18
  ```bash
19
19
  brew tap ZPVIP/commitgpt https://github.com/ZPVIP/commitgpt
20
+ brew trust zpvip/commitgpt
20
21
  brew install commitgpt
21
22
  ```
22
23
 
24
+ > **Note**: Recent Homebrew versions refuse to load formulae from third-party taps until you trust them, so `brew trust` is required before installing.
25
+
23
26
  **Upgrade:**
24
27
 
25
28
  ```bash
@@ -194,8 +197,41 @@ docs: update API documentation
194
197
  šŸ“ update API documentation
195
198
  ```
196
199
 
200
+ ### Choose Commit Message Detail Level
201
+ Select how much the commit message says:
202
+ ```bash
203
+ $ aicm -d
204
+ # or
205
+ $ aicm --detail
206
+ ```
207
+
208
+ - **Concise** - Subject line only (default)
209
+ - **Detailed** - Subject line, an explanatory paragraph, and a bullet list of notable changes
210
+
211
+ This is independent of the format above: the format decides how the subject line is written, the detail level decides how much follows it. Your selection is saved as `commit_detail` in `~/.config/commitgpt/config.yml`.
212
+
213
+ #### Detail Examples
214
+
215
+ **Concise:**
216
+ ```
217
+ feat(dfb): store and expand DFB event and speed logs
218
+ ```
219
+
220
+ **Detailed:**
221
+ ```
222
+ feat(dfb): store and expand DFB event and speed logs
223
+
224
+ Persist DFB device logs ingested from the proactive `evt` (event log) and
225
+ `lge` (speed stats) topics, one record per message.
226
+
227
+ - Add `dfb_event_logs` and `dfb_speed_stats` tables and models, each with an
228
+ idempotent unique index so re-delivery and overlapping backfills are safe.
229
+ - Associate both log tables on `Product`.
230
+ - Update `README_DFB.md` to document the cloud tables.
231
+ ```
232
+
197
233
  ### Check Configuration
198
- View your current configuration (Provider, Model, Format, Base URL, Diff Len):
234
+ View your current configuration (Provider, Model, Format, Detail, Base URL, Diff Len):
199
235
  ```bash
200
236
  $ aicm help
201
237
  ```
data/lib/commitgpt/cli.rb CHANGED
@@ -14,6 +14,7 @@ module CommitGpt
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
16
  method_option :format, aliases: '-f', type: :boolean, desc: 'Choose commit message format'
17
+ method_option :detail, aliases: '-d', type: :boolean, desc: 'Choose commit message detail level'
17
18
  def generate
18
19
  if options[:provider]
19
20
  CommitGpt::SetupWizard.new.switch_provider
@@ -21,6 +22,8 @@ module CommitGpt
21
22
  CommitGpt::SetupWizard.new.change_model
22
23
  elsif options[:format]
23
24
  CommitGpt::SetupWizard.new.choose_format
25
+ elsif options[:detail]
26
+ CommitGpt::SetupWizard.new.choose_detail
24
27
  else
25
28
  CommitGpt::CommitAi.new.aicm(verbose: options[:verbose])
26
29
  end
@@ -42,6 +45,7 @@ module CommitGpt
42
45
  shell.say ' -m, --models # Interactive model selection'
43
46
  shell.say ' -p, --provider # Switch active provider'
44
47
  shell.say ' -f, --format # Choose commit message format'
48
+ shell.say ' -d, --detail # Choose commit message detail level'
45
49
  shell.say ' -v, --verbose # Show git diff being sent to AI'
46
50
  shell.say ''
47
51
 
@@ -61,6 +65,7 @@ module CommitGpt
61
65
  shell.say " Provider: #{config['name'].green}"
62
66
  shell.say " Model: #{config['model'].cyan}"
63
67
  shell.say " Format: #{format.capitalize.yellow}"
68
+ shell.say " Detail: #{CommitGpt::ConfigManager.get_commit_detail.capitalize.yellow}"
64
69
  shell.say " Base URL: #{config['base_url']}"
65
70
  shell.say " Diff Len: #{config['diff_len']}"
66
71
  shell.say " Timeout: #{CommitGpt::ConfigManager.get_model_fetch_timeout}s"
@@ -133,6 +133,51 @@ module CommitGpt
133
133
  end
134
134
 
135
135
  @commit_format = ConfigManager.get_commit_format
136
+ @commit_detail = ConfigManager.get_commit_detail
137
+ end
138
+
139
+ # True when the message should carry a body, not just a subject line
140
+ def detailed?
141
+ @commit_detail == 'detailed'
142
+ end
143
+
144
+ # Streaming cutoff and completion budget, both of which have to grow to fit a body
145
+ def max_content_chars
146
+ detailed? ? 2000 : 300
147
+ end
148
+
149
+ def reasoning_disabled_max_tokens
150
+ detailed? ? 1000 : 300
151
+ end
152
+
153
+ # Header printed before the streamed message starts arriving
154
+ def content_prefix
155
+ detailed? ? "✦ Commit message:\n" : '✦ Commit message: git commit -am "'
156
+ end
157
+
158
+ # Closing quote that matches content_prefix; a body has no quote to close
159
+ def content_suffix
160
+ detailed? ? '' : '"'
161
+ end
162
+
163
+ # Editor git will actually use, resolved through its own precedence chain
164
+ def git_editor
165
+ editor = `git var GIT_EDITOR 2>/dev/null`.strip
166
+ editor.empty? ? 'your editor' : editor
167
+ end
168
+
169
+ # Trim the model output down to what git should receive
170
+ def finalize_message(full_content)
171
+ unless detailed?
172
+ # Take only the first non-empty line to avoid repetition or multi-line garbage
173
+ first_line = full_content.split("\n").map(&:strip).reject(&:empty?).first
174
+ return first_line&.gsub(/\A["']|["']\z/, '') || ''
175
+ end
176
+
177
+ body = full_content.strip.gsub(/\A["']|["']\z/, '').strip
178
+ # Keep the subject/body/bullets structure but drop trailing spaces and
179
+ # collapse the extra blank lines models like to emit between sections
180
+ body.split("\n").map(&:rstrip).join("\n").gsub(/\n{3,}/, "\n\n")
136
181
  end
137
182
 
138
183
  def aicm(verbose: false)
@@ -150,9 +195,10 @@ module CommitGpt
150
195
 
151
196
  case action
152
197
  when :commit
153
- commit_command = "git commit -m \"#{ai_commit_message}\""
154
- puts "\n→ Executing: #{commit_command}".yellow
155
- system(commit_command)
198
+ # Pass argv directly: a body with quotes, backticks or newlines must
199
+ # not be re-interpreted by the shell
200
+ puts "\n→ Executing: git commit -m <message>".yellow
201
+ system('git', 'commit', '-m', ai_commit_message)
156
202
  puts "\n\n"
157
203
  puts `git log -1`
158
204
  break
@@ -160,15 +206,15 @@ module CommitGpt
160
206
  puts "\n"
161
207
  next
162
208
  when :edit
163
- prompt = TTY::Prompt.new
164
- new_message = prompt.ask('Enter your commit message:')
165
- if new_message && !new_message.strip.empty?
166
- commit_command = "git commit -m \"#{new_message}\""
167
- system(commit_command)
168
- puts "\n"
169
- puts `git log -1`
209
+ # Let git open the message in the user's editor: it honours the
210
+ # GIT_EDITOR/core.editor/VISUAL/EDITOR chain, handles multi-line
211
+ # bodies, and aborts by itself if the message is left empty
212
+ puts "\n→ Opening #{git_editor} with the generated message...".yellow
213
+ if system('git', 'commit', '-e', '-m', ai_commit_message)
214
+ puts "\n"
215
+ puts `git log -1`
170
216
  else
171
- puts 'āœ– Commit aborted (empty message).'.red
217
+ puts 'āœ– Commit aborted.'.red
172
218
  end
173
219
  break
174
220
  when :exit
@@ -219,7 +265,7 @@ module CommitGpt
219
265
  prompt.select('Action:') do |menu|
220
266
  menu.choice 'Commit', :commit
221
267
  menu.choice 'Regenerate', :regenerate
222
- menu.choice 'Edit', :edit
268
+ menu.choice 'Edit in editor', :edit
223
269
  menu.choice 'Exit without commit', :exit
224
270
  end
225
271
  rescue TTY::Reader::InputInterrupt, Interrupt
@@ -279,7 +325,11 @@ module CommitGpt
279
325
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
280
326
  def generate_commit(diff = '', chunk_label: nil)
281
327
  # Build format-specific prompt
282
- 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.'
328
+ base_prompt = if detailed?
329
+ 'Generate a git commit message in present tense that precisely describes the key changes in the following code diff. Focus on what was changed and why, not just file names.'
330
+ else
331
+ '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.'
332
+ end
283
333
 
284
334
  format_instruction = case @commit_format
285
335
  when 'conventional'
@@ -290,17 +340,38 @@ module CommitGpt
290
340
  ''
291
341
  end
292
342
 
293
- format_spec = "The output response must be in format:\n#{COMMIT_FORMATS[@commit_format]}"
343
+ format_spec = if detailed?
344
+ "The subject line must be in format:\n#{COMMIT_FORMATS[@commit_format]}"
345
+ else
346
+ "The output response must be in format:\n#{COMMIT_FORMATS[@commit_format]}"
347
+ end
348
+
349
+ rules = if detailed?
350
+ [
351
+ '- The first line is the subject: a maximum of 100 characters, no trailing period.',
352
+ '- Then one blank line, then a body paragraph of 1-3 sentences explaining what changed and why.',
353
+ '- Then one blank line, then a bullet list where each bullet starts with "- " and describes one notable change.',
354
+ '- Wrap body and bullet text at 72 characters.',
355
+ '- Be specific: include concrete details (file, table, method, package names) rather than generic statements.',
356
+ '- Do not use markdown headings, bold, or code fences. Plain text only.',
357
+ '- Exclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.',
358
+ '- Return ONLY the commit message, nothing else. No explanations, no introductions, no surrounding quotes.'
359
+ ]
360
+ else
361
+ [
362
+ '- Commit message must be a maximum of 100 characters.',
363
+ '- Exclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.',
364
+ '- 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.',
365
+ '- Be specific: include concrete details (package names, versions, functionality) rather than generic statements.',
366
+ '- Return ONLY the commit message, nothing else.'
367
+ ]
368
+ end
294
369
 
295
370
  system_content = [
296
371
  base_prompt,
297
372
  'Message language: English.',
298
373
  'Rules:',
299
- '- Commit message must be a maximum of 100 characters.',
300
- '- Exclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.',
301
- '- 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.',
302
- '- Be specific: include concrete details (package names, versions, functionality) rather than generic statements.',
303
- '- Return ONLY the commit message, nothing else.',
374
+ *rules,
304
375
  format_instruction,
305
376
  format_spec
306
377
  ].reject(&:empty?).join("\n")
@@ -331,7 +402,7 @@ module CommitGpt
331
402
 
332
403
  if can_disable_reasoning
333
404
  payload[:disable_reasoning] = true
334
- payload[:max_tokens] = 300
405
+ payload[:max_tokens] = reasoning_disabled_max_tokens
335
406
  else
336
407
  payload[:max_tokens] = configured_max_tokens
337
408
  end
@@ -430,13 +501,13 @@ module CommitGpt
430
501
  if chunk_label
431
502
  print "ā—† #{chunk_label}: ".magenta
432
503
  else
433
- print '✦ Commit message: git commit -am "'.green
504
+ print content_prefix.green
434
505
  end
435
506
  printed_content_prefix = true
436
507
  end
437
508
 
438
509
  # Prevent infinite loops/repetitive garbage
439
- if full_content.length + content_chunk.length > 300
510
+ if full_content.length + content_chunk.length > max_content_chars
440
511
  stop_stream = true
441
512
  break
442
513
  end
@@ -460,7 +531,7 @@ module CommitGpt
460
531
  end
461
532
 
462
533
  # Close the quote
463
- puts(chunk_label ? '' : '"'.green) if printed_content_prefix
534
+ puts(chunk_label ? '' : content_suffix.green) if printed_content_prefix
464
535
 
465
536
  # Post-processing Logic (Retry if empty content)
466
537
  if (full_content.nil? || full_content.strip.empty?) && (full_reasoning && !full_reasoning.strip.empty?)
@@ -511,9 +582,7 @@ module CommitGpt
511
582
  # Reset retrying flag
512
583
  @is_retrying = false
513
584
 
514
- # Take only the first non-empty line to avoid repetition or multi-line garbage
515
- first_line = full_content.split("\n").map(&:strip).reject(&:empty?).first
516
- first_line&.gsub(/\A["']|["']\z/, '') || ''
585
+ finalize_message(full_content)
517
586
  end
518
587
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
519
588
 
@@ -530,14 +599,31 @@ module CommitGpt
530
599
  ''
531
600
  end
532
601
 
602
+ rules = if detailed?
603
+ [
604
+ '- The first line is the subject: a maximum of 100 characters, no trailing period.',
605
+ '- Then one blank line, then a body paragraph of 1-3 sentences explaining what changed and why.',
606
+ '- Then one blank line, then a bullet list where each bullet starts with "- " and describes one notable change.',
607
+ '- Wrap body and bullet text at 72 characters.',
608
+ '- Present tense.',
609
+ '- Be specific: include concrete details rather than generic statements.',
610
+ '- Do not use markdown headings, bold, or code fences. Plain text only.',
611
+ '- Return ONLY the commit message, nothing else. No quotes, no explanations.'
612
+ ]
613
+ else
614
+ [
615
+ '- Maximum 100 characters.',
616
+ '- Present tense.',
617
+ '- Be specific: include concrete details rather than generic statements.',
618
+ '- Return ONLY the commit message, nothing else. No quotes, no explanations.'
619
+ ]
620
+ end
621
+
533
622
  system_content = [
534
623
  'You are given multiple commit messages generated from different segments of a single large git diff.',
535
- 'Synthesize them into ONE concise, unified git commit message that captures the overall change.',
624
+ "Synthesize them into ONE unified git commit message that captures the overall change.#{' Keep it concise.' unless detailed?}",
536
625
  'Rules:',
537
- '- Maximum 100 characters.',
538
- '- Present tense.',
539
- '- Be specific: include concrete details rather than generic statements.',
540
- '- Return ONLY the commit message, nothing else. No quotes, no explanations.',
626
+ *rules,
541
627
  format_instruction
542
628
  ].reject(&:empty?).join("\n")
543
629
 
@@ -558,7 +644,7 @@ module CommitGpt
558
644
 
559
645
  if can_disable_reasoning
560
646
  payload[:disable_reasoning] = true
561
- payload[:max_tokens] = 300
647
+ payload[:max_tokens] = reasoning_disabled_max_tokens
562
648
  else
563
649
  payload[:max_tokens] = provider_config['max_tokens'] || 2000
564
650
  end
@@ -606,11 +692,11 @@ module CommitGpt
606
692
  content_chunk = delta['content']
607
693
  if content_chunk && !content_chunk.empty?
608
694
  unless printed_content_prefix
609
- print "\n✦ Commit message: git commit -am \"".green
695
+ print "\n#{content_prefix}".green
610
696
  printed_content_prefix = true
611
697
  end
612
698
 
613
- if full_content.length + content_chunk.length > 300
699
+ if full_content.length + content_chunk.length > max_content_chars
614
700
  stop_stream = true
615
701
  break
616
702
  end
@@ -630,15 +716,14 @@ module CommitGpt
630
716
  return nil
631
717
  end
632
718
 
633
- puts '"'.green if printed_content_prefix
719
+ puts content_suffix.green if printed_content_prefix
634
720
 
635
721
  if full_content.strip.empty?
636
722
  puts 'āœ– No response from AI during synthesis.'.red
637
723
  return nil
638
724
  end
639
725
 
640
- first_line = full_content.split("\n").map(&:strip).reject(&:empty?).first
641
- first_line&.gsub(/\A["']|["']\z/, '') || ''
726
+ finalize_message(full_content)
642
727
  end
643
728
  end
644
729
  end
@@ -11,6 +11,11 @@ module CommitGpt
11
11
  # Seconds to wait when fetching a provider's model list
12
12
  DEFAULT_MODEL_FETCH_TIMEOUT = 30
13
13
 
14
+ # How much the commit message says: a subject line only, or a subject
15
+ # followed by an explanatory body
16
+ COMMIT_DETAILS = %w[concise detailed].freeze
17
+ DEFAULT_COMMIT_DETAIL = 'concise'
18
+
14
19
  class << self
15
20
  # Get the config directory path
16
21
  def config_dir
@@ -168,6 +173,23 @@ module CommitGpt
168
173
  save_main_config(main_config)
169
174
  end
170
175
 
176
+ # Get commit detail level ('concise' or 'detailed')
177
+ def get_commit_detail
178
+ return DEFAULT_COMMIT_DETAIL unless config_exists?
179
+
180
+ main_config = YAML.load_file(main_config_path)
181
+ detail = main_config['commit_detail']
182
+ COMMIT_DETAILS.include?(detail) ? detail : DEFAULT_COMMIT_DETAIL
183
+ end
184
+
185
+ # Set commit detail level
186
+ def set_commit_detail(detail)
187
+ ensure_config_dir
188
+ main_config = config_exists? ? YAML.load_file(main_config_path) : { 'providers' => [], 'active_provider' => '' }
189
+ main_config['commit_detail'] = detail
190
+ save_main_config(main_config)
191
+ end
192
+
171
193
  # Get the model list fetch timeout (seconds)
172
194
  def get_model_fetch_timeout
173
195
  return DEFAULT_MODEL_FETCH_TIMEOUT unless config_exists?
@@ -116,8 +116,26 @@ module CommitGpt
116
116
  puts "\nāœ” Commit format set to: #{format}".green
117
117
  end
118
118
 
119
+ # Choose how much the commit message says
120
+ def choose_detail
121
+ puts "\n→ Choose git commit message detail level:\n".green
122
+
123
+ detail = @prompt.select('Select detail level:', default: default_detail_index) do |menu|
124
+ menu.choice 'Concise - Subject line only', 'concise'
125
+ menu.choice 'Detailed - Subject line, explanatory paragraph, and bullet list of changes', 'detailed'
126
+ end
127
+
128
+ ConfigManager.set_commit_detail(detail)
129
+ puts "\nāœ” Commit detail set to: #{detail}".green
130
+ end
131
+
119
132
  private
120
133
 
134
+ # Menu position of the currently configured detail level (1-based)
135
+ def default_detail_index
136
+ ConfigManager::COMMIT_DETAILS.index(ConfigManager.get_commit_detail).to_i + 1
137
+ end
138
+
121
139
  # Select provider from list
122
140
  def select_provider
123
141
  config = ConfigManager.load_config
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CommitGpt
4
- VERSION = '0.3.10'
4
+ VERSION = '0.3.11'
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.10
4
+ version: 0.3.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peng Zhang