commitgpt 0.3.5 → 0.3.6
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 +5 -0
- data/lib/commitgpt/commit_ai.rb +176 -32
- data/lib/commitgpt/config_manager.rb +2 -2
- data/lib/commitgpt/diff_helpers.rb +29 -10
- data/lib/commitgpt/setup_wizard.rb +8 -8
- data/lib/commitgpt/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1e1fcc474c778dff2c10f370b5a291483793d0ded5c8d43beaab1b7e377b4ff
|
|
4
|
+
data.tar.gz: 17ec9d1daaf3ad085adbd8566173411db284995188b4c97d5eaf4d69a634763d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91ab99f89df2861a5731a3692e88d989b733a5e88edbd55ccdb654e109778b778f02dfc2af547ee6d12dcb8da8858ade012fd3150df381a6a771eecc036b04f1
|
|
7
|
+
data.tar.gz: aee42f5617f25c523aa9ed3c134eaae5b92c3e9d1e7d19cb9e7de089d79c010d792c4eb12c836db138160076f787dbe30e4219c104476e0c806f2de8135085bb
|
data/README.md
CHANGED
|
@@ -216,6 +216,7 @@ $ gem update commitgpt
|
|
|
216
216
|
We support any OpenAI-compatible API. Presets available for:
|
|
217
217
|
- **Cerebras** (Fast & Recommended)
|
|
218
218
|
- **OpenAI** (Official)
|
|
219
|
+
- **Apple** (Local via apple-to-openai)
|
|
219
220
|
- **Ollama** (Local)
|
|
220
221
|
- **Groq**
|
|
221
222
|
- **DeepSeek**
|
|
@@ -246,6 +247,10 @@ llama-3.3-70b-versatile
|
|
|
246
247
|
llama-3.1-8b-instant
|
|
247
248
|
```
|
|
248
249
|
|
|
250
|
+
**Apple Local Models** (via [apple-to-openai](https://github.com/ZPVIP/apple-to-openai))
|
|
251
|
+
|
|
252
|
+
> **Note**: Due to the context window limits of Apple's local models, it is highly recommended to set your max diff length (`diff_len`) to `10000` during setup. When prompted for large diffs, select the **Smart chunked mode** to avoid errors.
|
|
253
|
+
|
|
249
254
|
## How It Works
|
|
250
255
|
This CLI tool runs a `git diff` command to grab all staged changes, sends this to OpenAI's GPT API (or compatible endpoint), and returns an AI-generated commit message. The tool uses the `/v1/chat/completions` endpoint with optimized prompts/system instructions for generating conventional commit messages.
|
|
251
256
|
|
data/lib/commitgpt/commit_ai.rb
CHANGED
|
@@ -139,7 +139,7 @@ module CommitGpt
|
|
|
139
139
|
exit(1) unless welcome
|
|
140
140
|
diff = git_diff || exit(1)
|
|
141
141
|
if verbose
|
|
142
|
-
puts "
|
|
142
|
+
puts "→ Git diff (#{diff.length} chars):".cyan
|
|
143
143
|
puts diff
|
|
144
144
|
puts "\n"
|
|
145
145
|
end
|
|
@@ -151,7 +151,7 @@ module CommitGpt
|
|
|
151
151
|
case action
|
|
152
152
|
when :commit
|
|
153
153
|
commit_command = "git commit -m \"#{ai_commit_message}\""
|
|
154
|
-
puts "\n
|
|
154
|
+
puts "\n→ Executing: #{commit_command}".yellow
|
|
155
155
|
system(commit_command)
|
|
156
156
|
puts "\n\n"
|
|
157
157
|
puts `git log -1`
|
|
@@ -168,11 +168,11 @@ module CommitGpt
|
|
|
168
168
|
puts "\n"
|
|
169
169
|
puts `git log -1`
|
|
170
170
|
else
|
|
171
|
-
puts '
|
|
171
|
+
puts '✖ Commit aborted (empty message).'.red
|
|
172
172
|
end
|
|
173
173
|
break
|
|
174
174
|
when :exit
|
|
175
|
-
puts '
|
|
175
|
+
puts '⚠ Exit without commit.'.yellow
|
|
176
176
|
break
|
|
177
177
|
end
|
|
178
178
|
end
|
|
@@ -190,7 +190,7 @@ module CommitGpt
|
|
|
190
190
|
models = response['data'] || []
|
|
191
191
|
models.each { |m| puts m['id'] }
|
|
192
192
|
rescue StandardError => e
|
|
193
|
-
puts "
|
|
193
|
+
puts "✖ Failed to list models: #{e.message}".red
|
|
194
194
|
end
|
|
195
195
|
end
|
|
196
196
|
|
|
@@ -206,7 +206,7 @@ module CommitGpt
|
|
|
206
206
|
models = response['data'] || []
|
|
207
207
|
models.each { |m| puts m['id'] }
|
|
208
208
|
rescue StandardError => e
|
|
209
|
-
puts "
|
|
209
|
+
puts "✖ Failed to list models: #{e.message}".red
|
|
210
210
|
end
|
|
211
211
|
end
|
|
212
212
|
|
|
@@ -228,42 +228,56 @@ module CommitGpt
|
|
|
228
228
|
end
|
|
229
229
|
|
|
230
230
|
def message(diff = nil)
|
|
231
|
-
generate_commit(diff)
|
|
231
|
+
return generate_commit(diff) unless @chunked_mode
|
|
232
|
+
|
|
233
|
+
# Reserve space for system prompt overhead (~20% of diff_len)
|
|
234
|
+
chunk_size = [(@diff_len * 0.8).to_i, 4000].max
|
|
235
|
+
chunks = split_diff_by_length(diff, chunk_size)
|
|
236
|
+
segment_messages = []
|
|
237
|
+
|
|
238
|
+
puts "→ Splitting into #{chunks.length} segments (#{chunk_size} chars each)...".cyan
|
|
239
|
+
|
|
240
|
+
chunks.each_with_index do |chunk, idx|
|
|
241
|
+
puts "\n◆ Generating message for segment #{idx + 1}/#{chunks.length}...".magenta
|
|
242
|
+
msg = generate_commit(chunk, chunk_label: "Segment #{idx + 1}/#{chunks.length}")
|
|
243
|
+
return nil if msg.nil?
|
|
244
|
+
|
|
245
|
+
segment_messages << msg
|
|
246
|
+
puts ''
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
puts "\n→ Synthesizing final commit message from #{segment_messages.length} segments...".cyan
|
|
250
|
+
synthesize_commit(segment_messages)
|
|
232
251
|
end
|
|
233
252
|
|
|
234
253
|
def welcome
|
|
235
|
-
puts "\n
|
|
254
|
+
puts "\n✦ Welcome to AI Commits!".green
|
|
236
255
|
|
|
237
256
|
# Check if config exists
|
|
238
257
|
unless ConfigManager.config_exists?
|
|
239
|
-
puts '
|
|
258
|
+
puts '⚠ Configuration not found. Generating default config...'.yellow
|
|
240
259
|
ConfigManager.generate_default_configs
|
|
241
|
-
puts "
|
|
260
|
+
puts "✖ Please run 'aicm setup' to configure your provider.".red
|
|
242
261
|
return false
|
|
243
262
|
end
|
|
244
263
|
|
|
245
264
|
# Check if active provider is configured
|
|
246
|
-
if @api_key.nil? || @api_key.empty?
|
|
247
|
-
puts "▲ No active provider configured. Please run 'aicm setup'.".red
|
|
248
|
-
return false
|
|
249
|
-
end
|
|
250
|
-
|
|
251
265
|
if @model.nil? || @model.empty?
|
|
252
|
-
puts "
|
|
266
|
+
puts "✖ No model selected. Please run 'aicm setup'.".red
|
|
253
267
|
return false
|
|
254
268
|
end
|
|
255
269
|
|
|
256
270
|
begin
|
|
257
271
|
`git rev-parse --is-inside-work-tree`
|
|
258
272
|
rescue StandardError
|
|
259
|
-
puts '
|
|
273
|
+
puts '✖ This is not a git repository'.red
|
|
260
274
|
return false
|
|
261
275
|
end
|
|
262
276
|
true
|
|
263
277
|
end
|
|
264
278
|
|
|
265
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
266
|
-
def generate_commit(diff = '')
|
|
279
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
|
|
280
|
+
def generate_commit(diff = '', chunk_label: nil)
|
|
267
281
|
# Build format-specific prompt
|
|
268
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.'
|
|
269
283
|
|
|
@@ -323,7 +337,9 @@ module CommitGpt
|
|
|
323
337
|
end
|
|
324
338
|
|
|
325
339
|
# Initial UI feedback (only on first try)
|
|
326
|
-
|
|
340
|
+
total_chars = system_content.length + diff.to_s.length
|
|
341
|
+
puts " ....... System prompt: #{system_content.length} chars, Diff chunk: #{diff.to_s.length} chars, Total: #{total_chars} chars".gray
|
|
342
|
+
puts ' ....... Generating your AI commit message'.gray unless defined?(@is_retrying) && @is_retrying
|
|
327
343
|
|
|
328
344
|
full_content = ''
|
|
329
345
|
full_reasoning = ''
|
|
@@ -364,12 +380,12 @@ module CommitGpt
|
|
|
364
380
|
end
|
|
365
381
|
|
|
366
382
|
if can_disable_reasoning && (error_msg =~ /parameter|reasoning|unsupported/i || response.code == '400')
|
|
367
|
-
puts "
|
|
383
|
+
puts "⚠ Provider does not support 'disable_reasoning'. Updating config and retrying...".yellow
|
|
368
384
|
ConfigManager.update_provider(provider_config['name'], { 'can_disable_reasoning' => false })
|
|
369
385
|
@is_retrying = true
|
|
370
386
|
return generate_commit(diff)
|
|
371
387
|
else
|
|
372
|
-
puts "
|
|
388
|
+
puts "✖ API Error: #{error_msg}".red
|
|
373
389
|
return nil
|
|
374
390
|
end
|
|
375
391
|
end
|
|
@@ -411,7 +427,11 @@ module CommitGpt
|
|
|
411
427
|
end
|
|
412
428
|
|
|
413
429
|
unless printed_content_prefix
|
|
414
|
-
|
|
430
|
+
if chunk_label
|
|
431
|
+
print "◆ #{chunk_label}: ".magenta
|
|
432
|
+
else
|
|
433
|
+
print '✦ Commit message: git commit -am "'.green
|
|
434
|
+
end
|
|
415
435
|
printed_content_prefix = true
|
|
416
436
|
end
|
|
417
437
|
|
|
@@ -421,7 +441,7 @@ module CommitGpt
|
|
|
421
441
|
break
|
|
422
442
|
end
|
|
423
443
|
|
|
424
|
-
print content_chunk.green
|
|
444
|
+
print chunk_label ? content_chunk.magenta : content_chunk.green
|
|
425
445
|
full_content += content_chunk
|
|
426
446
|
$stdout.flush
|
|
427
447
|
end
|
|
@@ -435,22 +455,22 @@ module CommitGpt
|
|
|
435
455
|
end
|
|
436
456
|
end
|
|
437
457
|
rescue StandardError => e
|
|
438
|
-
puts "
|
|
458
|
+
puts "✖ Error: #{e.message}".red
|
|
439
459
|
return nil
|
|
440
460
|
end
|
|
441
461
|
|
|
442
462
|
# Close the quote
|
|
443
|
-
puts '"'.green if printed_content_prefix
|
|
463
|
+
puts(chunk_label ? '' : '"'.green) if printed_content_prefix
|
|
444
464
|
|
|
445
465
|
# Post-processing Logic (Retry if empty content)
|
|
446
466
|
if (full_content.nil? || full_content.strip.empty?) && (full_reasoning && !full_reasoning.strip.empty?)
|
|
447
467
|
if can_disable_reasoning
|
|
448
|
-
puts "\n
|
|
468
|
+
puts "\n⚠ Model returned reasoning despite 'disable_reasoning: true'. Updating config and retrying...".yellow
|
|
449
469
|
ConfigManager.update_provider(provider_config['name'], { 'can_disable_reasoning' => false })
|
|
450
470
|
@is_retrying = true
|
|
451
471
|
return generate_commit(diff)
|
|
452
472
|
else
|
|
453
|
-
puts "\n
|
|
473
|
+
puts "\n✖ Model output truncated (Reasoning consumed all #{configured_max_tokens} tokens).".red
|
|
454
474
|
prompt = TTY::Prompt.new
|
|
455
475
|
choice = prompt.select('Choose an action:') do |menu|
|
|
456
476
|
menu.choice "Double max_tokens to #{configured_max_tokens * 2}", :double
|
|
@@ -468,7 +488,7 @@ module CommitGpt
|
|
|
468
488
|
end
|
|
469
489
|
|
|
470
490
|
if new_max
|
|
471
|
-
puts "
|
|
491
|
+
puts "→ Updating max_tokens to #{new_max} and retrying...".yellow
|
|
472
492
|
ConfigManager.update_provider(provider_config['name'], { 'max_tokens' => new_max })
|
|
473
493
|
@is_retrying = true
|
|
474
494
|
return generate_commit(diff)
|
|
@@ -478,13 +498,13 @@ module CommitGpt
|
|
|
478
498
|
end
|
|
479
499
|
|
|
480
500
|
if full_content.empty? && full_reasoning.empty?
|
|
481
|
-
puts '
|
|
501
|
+
puts '✖ No response from AI.'.red
|
|
482
502
|
return nil
|
|
483
503
|
end
|
|
484
504
|
|
|
485
505
|
# Print usage info if available (saved from stream or approximated)
|
|
486
506
|
if defined?(@last_usage) && @last_usage
|
|
487
|
-
puts "
|
|
507
|
+
puts " ....... Tokens: #{@last_usage['total_tokens']} (Prompt: #{@last_usage['prompt_tokens']}, Completion: #{@last_usage['completion_tokens']})".gray
|
|
488
508
|
@last_usage = nil
|
|
489
509
|
end
|
|
490
510
|
|
|
@@ -495,6 +515,130 @@ module CommitGpt
|
|
|
495
515
|
first_line = full_content.split("\n").map(&:strip).reject(&:empty?).first
|
|
496
516
|
first_line&.gsub(/\A["']|["']\z/, '') || ''
|
|
497
517
|
end
|
|
498
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
518
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
|
|
519
|
+
|
|
520
|
+
# Synthesize a final commit message from multiple segment messages
|
|
521
|
+
def synthesize_commit(segment_messages)
|
|
522
|
+
numbered = segment_messages.each_with_index.map { |msg, i| "#{i + 1}. #{msg}" }.join("\n")
|
|
523
|
+
|
|
524
|
+
format_instruction = case @commit_format
|
|
525
|
+
when 'conventional'
|
|
526
|
+
"The output must follow Conventional Commits format:\n#{COMMIT_FORMATS['conventional']}"
|
|
527
|
+
when 'gitmoji'
|
|
528
|
+
"The output must use Gitmoji format:\n#{COMMIT_FORMATS['gitmoji']}"
|
|
529
|
+
else
|
|
530
|
+
''
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
system_content = [
|
|
534
|
+
'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.',
|
|
536
|
+
'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.',
|
|
541
|
+
format_instruction
|
|
542
|
+
].reject(&:empty?).join("\n")
|
|
543
|
+
|
|
544
|
+
messages = [
|
|
545
|
+
{ role: 'system', content: system_content },
|
|
546
|
+
{ role: 'user', content: "Synthesize these segment commit messages into one:\n\n#{numbered}" }
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
provider_config = ConfigManager.get_active_provider_config
|
|
550
|
+
can_disable_reasoning = provider_config.key?('can_disable_reasoning') ? provider_config['can_disable_reasoning'] : true
|
|
551
|
+
|
|
552
|
+
payload = {
|
|
553
|
+
model: @model,
|
|
554
|
+
messages: messages,
|
|
555
|
+
temperature: 0.5,
|
|
556
|
+
stream: true
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if can_disable_reasoning
|
|
560
|
+
payload[:disable_reasoning] = true
|
|
561
|
+
payload[:max_tokens] = 300
|
|
562
|
+
else
|
|
563
|
+
payload[:max_tokens] = provider_config['max_tokens'] || 2000
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
full_content = ''
|
|
567
|
+
printed_content_prefix = false
|
|
568
|
+
stop_stream = false
|
|
569
|
+
|
|
570
|
+
uri = URI("#{@base_url}/chat/completions")
|
|
571
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
572
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
573
|
+
http.read_timeout = 120
|
|
574
|
+
|
|
575
|
+
request = Net::HTTP::Post.new(uri)
|
|
576
|
+
request['Content-Type'] = 'application/json'
|
|
577
|
+
request['Authorization'] = "Bearer #{@api_key}" if @api_key
|
|
578
|
+
request.body = payload.to_json
|
|
579
|
+
|
|
580
|
+
begin
|
|
581
|
+
http.request(request) do |response|
|
|
582
|
+
if response.code != '200'
|
|
583
|
+
error_body = response.read_body
|
|
584
|
+
puts "✖ API Error: #{error_body}".red
|
|
585
|
+
return nil
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
buffer = ''
|
|
589
|
+
response.read_body do |chunk|
|
|
590
|
+
break if stop_stream
|
|
591
|
+
|
|
592
|
+
buffer += chunk
|
|
593
|
+
while (line_end = buffer.index("\n"))
|
|
594
|
+
line = buffer.slice!(0, line_end + 1).strip
|
|
595
|
+
next if line.empty?
|
|
596
|
+
next unless line.start_with?('data: ')
|
|
597
|
+
|
|
598
|
+
data_str = line[6..]
|
|
599
|
+
next if data_str == '[DONE]'
|
|
600
|
+
|
|
601
|
+
begin
|
|
602
|
+
data = JSON.parse(data_str)
|
|
603
|
+
delta = data.dig('choices', 0, 'delta')
|
|
604
|
+
next unless delta
|
|
605
|
+
|
|
606
|
+
content_chunk = delta['content']
|
|
607
|
+
if content_chunk && !content_chunk.empty?
|
|
608
|
+
unless printed_content_prefix
|
|
609
|
+
print "\n✦ Commit message: git commit -am \"".green
|
|
610
|
+
printed_content_prefix = true
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
if full_content.length + content_chunk.length > 300
|
|
614
|
+
stop_stream = true
|
|
615
|
+
break
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
print content_chunk.green
|
|
619
|
+
full_content += content_chunk
|
|
620
|
+
$stdout.flush
|
|
621
|
+
end
|
|
622
|
+
rescue JSON::ParserError
|
|
623
|
+
# Partial JSON, wait for more data
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
rescue StandardError => e
|
|
629
|
+
puts "✖ Error: #{e.message}".red
|
|
630
|
+
return nil
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
puts '"'.green if printed_content_prefix
|
|
634
|
+
|
|
635
|
+
if full_content.strip.empty?
|
|
636
|
+
puts '✖ No response from AI during synthesis.'.red
|
|
637
|
+
return nil
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
first_line = full_content.split("\n").map(&:strip).reject(&:empty?).first
|
|
641
|
+
first_line&.gsub(/\A["']|["']\z/, '') || ''
|
|
642
|
+
end
|
|
499
643
|
end
|
|
500
644
|
end
|
|
@@ -102,8 +102,8 @@ module CommitGpt
|
|
|
102
102
|
save_local_config(local_config)
|
|
103
103
|
|
|
104
104
|
# Remind user to add config.local.yml to .gitignore
|
|
105
|
-
puts '
|
|
106
|
-
puts '
|
|
105
|
+
puts '✔ Generated default configuration files.'.green
|
|
106
|
+
puts '⚠ Remember to add ~/.config/commitgpt/config.local.yml to your .gitignore'.yellow
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# Get list of configured providers (with API keys)
|
|
@@ -21,11 +21,11 @@ module CommitGpt
|
|
|
21
21
|
choice = prompt_no_staged_changes
|
|
22
22
|
case choice
|
|
23
23
|
when :add_all
|
|
24
|
-
puts '
|
|
24
|
+
puts '→ Running git add .'.yellow
|
|
25
25
|
system('git add .')
|
|
26
26
|
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
27
27
|
if diff_cached.empty?
|
|
28
|
-
puts '
|
|
28
|
+
puts '✖ Still no changes to commit.'.red
|
|
29
29
|
return nil
|
|
30
30
|
end
|
|
31
31
|
when :exit
|
|
@@ -33,7 +33,7 @@ module CommitGpt
|
|
|
33
33
|
end
|
|
34
34
|
else
|
|
35
35
|
# Scenario: Mixed state (some staged, some not)
|
|
36
|
-
puts '
|
|
36
|
+
puts '⚠ You have both staged and unstaged changes:'.yellow
|
|
37
37
|
|
|
38
38
|
staged_files = `git diff --cached --name-status . #{exclusions}`.chomp
|
|
39
39
|
unstaged_files = `git diff --name-status . #{exclusions}`.chomp
|
|
@@ -54,7 +54,7 @@ module CommitGpt
|
|
|
54
54
|
|
|
55
55
|
case choice
|
|
56
56
|
when :add_all
|
|
57
|
-
puts '
|
|
57
|
+
puts '→ Running git add .'.yellow
|
|
58
58
|
system('git add .')
|
|
59
59
|
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
60
60
|
when :exit
|
|
@@ -67,7 +67,7 @@ module CommitGpt
|
|
|
67
67
|
# git status --porcelain includes untracked files
|
|
68
68
|
git_status = `git status --porcelain`.chomp
|
|
69
69
|
if git_status.empty?
|
|
70
|
-
puts '
|
|
70
|
+
puts '⚠ No changes to commit. Working tree clean.'.yellow
|
|
71
71
|
return nil
|
|
72
72
|
else
|
|
73
73
|
# Only untracked files? Or ignored files?
|
|
@@ -76,7 +76,7 @@ module CommitGpt
|
|
|
76
76
|
choice = prompt_no_staged_changes
|
|
77
77
|
case choice
|
|
78
78
|
when :add_all
|
|
79
|
-
puts '
|
|
79
|
+
puts '→ Running git add .'.yellow
|
|
80
80
|
system('git add .')
|
|
81
81
|
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
82
82
|
when :exit
|
|
@@ -93,11 +93,14 @@ module CommitGpt
|
|
|
93
93
|
if diff.length > diff_len
|
|
94
94
|
choice = prompt_diff_handling(diff.length, diff_len)
|
|
95
95
|
case choice
|
|
96
|
+
when :chunked
|
|
97
|
+
@chunked_mode = true
|
|
98
|
+
puts "→ Smart chunked mode: splitting #{diff.length} chars into ~#{(diff.length.to_f / diff_len).ceil} segments...".yellow
|
|
96
99
|
when :truncate
|
|
97
|
-
puts "
|
|
100
|
+
puts "→ Truncating diff to #{diff_len} chars...".yellow
|
|
98
101
|
diff = diff[0...diff_len]
|
|
99
102
|
when :unlimited
|
|
100
|
-
puts "
|
|
103
|
+
puts "→ Using full diff (#{diff.length} chars)...".yellow
|
|
101
104
|
when :exit
|
|
102
105
|
return nil
|
|
103
106
|
end
|
|
@@ -107,7 +110,7 @@ module CommitGpt
|
|
|
107
110
|
end
|
|
108
111
|
|
|
109
112
|
def prompt_no_staged_changes
|
|
110
|
-
puts '
|
|
113
|
+
puts '⚠ No staged changes found (but unstaged/untracked files exist).'.yellow
|
|
111
114
|
prompt = TTY::Prompt.new
|
|
112
115
|
begin
|
|
113
116
|
prompt.select('Choose an option:') do |menu|
|
|
@@ -120,10 +123,11 @@ module CommitGpt
|
|
|
120
123
|
end
|
|
121
124
|
|
|
122
125
|
def prompt_diff_handling(current_len, max_len)
|
|
123
|
-
puts "
|
|
126
|
+
puts "⚠ The diff is too large (#{current_len} chars, max #{max_len}).".yellow
|
|
124
127
|
prompt = TTY::Prompt.new
|
|
125
128
|
begin
|
|
126
129
|
prompt.select('Choose an option:') do |menu|
|
|
130
|
+
menu.choice 'Smart chunked: split into segments and synthesize (recommended)', :chunked
|
|
127
131
|
menu.choice "Use first #{max_len} characters to generate commit message", :truncate
|
|
128
132
|
menu.choice 'Use unlimited characters (may fail or be slow)', :unlimited
|
|
129
133
|
menu.choice 'Exit', :exit
|
|
@@ -133,6 +137,21 @@ module CommitGpt
|
|
|
133
137
|
end
|
|
134
138
|
end
|
|
135
139
|
|
|
140
|
+
def split_diff_by_length(diff, max_len)
|
|
141
|
+
chunks = []
|
|
142
|
+
current_chunk = ''
|
|
143
|
+
|
|
144
|
+
diff.each_line do |line|
|
|
145
|
+
if current_chunk.length + line.length > max_len && !current_chunk.empty?
|
|
146
|
+
chunks << current_chunk
|
|
147
|
+
current_chunk = ''
|
|
148
|
+
end
|
|
149
|
+
current_chunk += line
|
|
150
|
+
end
|
|
151
|
+
chunks << current_chunk unless current_chunk.empty?
|
|
152
|
+
chunks
|
|
153
|
+
end
|
|
154
|
+
|
|
136
155
|
def detect_lock_file_changes
|
|
137
156
|
# Check both staged and unstaged changes for lock files
|
|
138
157
|
staged_files = `git diff --cached --name-only`.chomp.split("\n")
|
|
@@ -28,7 +28,7 @@ module CommitGpt
|
|
|
28
28
|
configured = ConfigManager.configured_providers
|
|
29
29
|
|
|
30
30
|
if configured.empty?
|
|
31
|
-
puts "
|
|
31
|
+
puts "✖ No providers configured. Please run 'aicm setup' first.".red
|
|
32
32
|
return
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -69,7 +69,7 @@ module CommitGpt
|
|
|
69
69
|
provider_config = ConfigManager.get_active_provider_config
|
|
70
70
|
|
|
71
71
|
if provider_config.nil? || provider_config['api_key'].nil? || provider_config['api_key'].empty?
|
|
72
|
-
puts "
|
|
72
|
+
puts "✖ No active provider configured. Please run 'aicm setup'.".red
|
|
73
73
|
return
|
|
74
74
|
end
|
|
75
75
|
|
|
@@ -90,7 +90,7 @@ module CommitGpt
|
|
|
90
90
|
def choose_format
|
|
91
91
|
prompt = TTY::Prompt.new
|
|
92
92
|
|
|
93
|
-
puts "\n
|
|
93
|
+
puts "\n→ Choose git commit message format:\n".green
|
|
94
94
|
|
|
95
95
|
format = prompt.select('Select format:') do |menu|
|
|
96
96
|
menu.choice 'Simple - Concise commit message', 'simple'
|
|
@@ -99,7 +99,7 @@ module CommitGpt
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
ConfigManager.set_commit_format(format)
|
|
102
|
-
puts "\n
|
|
102
|
+
puts "\n✔ Commit format set to: #{format}".green
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
private
|
|
@@ -264,20 +264,20 @@ module CommitGpt
|
|
|
264
264
|
models = response['data'] || []
|
|
265
265
|
models = models.map { |m| m['id'] }.compact.sort
|
|
266
266
|
else
|
|
267
|
-
puts "
|
|
267
|
+
puts "✖ Failed to fetch models: HTTP #{response.code}".red
|
|
268
268
|
return nil
|
|
269
269
|
end
|
|
270
270
|
end
|
|
271
271
|
rescue Timeout::Error
|
|
272
|
-
puts '
|
|
272
|
+
puts '✖ Connection timeout (5s). Please check your network, base_url, and api_key.'.red
|
|
273
273
|
exit(0)
|
|
274
274
|
rescue StandardError => e
|
|
275
|
-
puts "
|
|
275
|
+
puts "✖ Error fetching models: #{e.message}".red
|
|
276
276
|
exit(0)
|
|
277
277
|
end
|
|
278
278
|
|
|
279
279
|
if models.nil? || models.empty?
|
|
280
|
-
puts '
|
|
280
|
+
puts '✖ No models found. Please check your configuration.'.red
|
|
281
281
|
exit(0)
|
|
282
282
|
end
|
|
283
283
|
|
data/lib/commitgpt/version.rb
CHANGED